视觉SLAM-光流法

news2024/10/7 16:23:23

文章目录

目录

前言

🌟光流法的基本原理

🌟OpenCV中的光流法实现

🌟光流法的应用

🌟代码实现

🍉OpenCV代码

🍉OpenCV代码

🌟总结


前言

        当我们观察一个视频或连续帧的图像时,我们经常想知道图像中的物体是如何移动的。光流法(Optical Flow)就是一种用于估计图像中像素点在时间上的运动的计算机视觉技术。它可以帮助我们跟踪物体的移动,并提供关于物体速度和运动方向的信息。

源码在后面,需要的自行下载。


🌟光流法的基本原理

光流法基于以下假设:在相邻帧之间,一个像素点的亮度不会发生大的变化。基于这个假设,光流法通过比较两帧之间像素的亮度变化,推断像素点的运动。

在实际应用中,光流法通常使用Lucas-Kanade算法,该算法假设图像中的运动是局部的,即在一个小的邻域内,像素点的运动是相似的。该算法通过以下步骤计算光流:

  1. 检测特征点:在第一帧图像中,通过特征点检测算法(如Harris角点检测)找到一些具有显著变化的点,作为跟踪的起点。
  2. 选择跟踪窗口:对于每个特征点,在第一帧图像中选择一个小的窗口作为跟踪窗口。
  3. 在相邻帧中跟踪特征点:在第二帧图像中,根据第一帧图像中的特征点位置,通过最小化特征点窗口内的亮度变化,计算第二帧中对应的特征点位置。
  4. 计算光流:根据第一帧和第二帧中特征点的位置差异,计算特征点的光流向量,表示特征点的运动方向和速度。

🌟OpenCV中的光流法实现

OpenCV是一个流行的计算机视觉库,提供了用于光流估计的函数cv::calcOpticalFlowPyrLK。该函数使用了基于Lucas-Kanade算法的金字塔方法,可以在图像金字塔的不同尺度上进行光流估计,提高了算法的鲁棒性和准确性。

使用OpenCV中的cv::calcOpticalFlowPyrLK函数,你可以传入两帧图像、特征点位置,并计算出特征点的光流向量。这个函数还会返回一个状态向量,指示光流是否成功计算,并提供一个误差向量,表示光流估计的精度。

🌟光流法的应用

光流法在计算机视觉和计算机图形学中有广泛的应用。以下是一些常见的应用领域:

  1. 物体跟踪:光流法可以用于跟踪视频序列中的物体。通过跟踪物体的光流向量,可以实现目标检测、运动分析和行为识别等任务。

  2. 视频稳定:通过计算视频序列中的光流,可以检测到相机的运动和抖动,并进行图像稳定化,以提高视频质量和观看体验。

  3. 动作分析:通过跟踪人体的光流,可以分析人的动作和姿态。这在行为分析、运动捕捉和虚拟现实等领域具有重要意义。

  4. 三维重建:通过光流法估计图像中的运动,可以从多个视角的图像中恢复场景的三维结构,实现三维重建和立体视觉。

  5. 视频编码:光流法在视频编码中扮演着重要的角色。通过估计图像序列之间的运动,可以提供更高的压缩率和更好的视频质量。

  6. 虚拟现实和增强现实:光流法可以用于虚拟现实和增强现实应用中,实现对虚拟对象或信息的准确定位和交互。

总结起来,光流法是一种用于估计图像中像素点运动的技术。它在计算机视觉领域有广泛的应用,包括物体跟踪、视频稳定、动作分析、三维重建、视频编码以及虚拟现实和增强现实等方面。通过OpenCV提供的函数,我们可以方便地实现光流法,并应用于各种视觉任务中。

🌟代码实现

🍉OpenCV代码

代码如下:

#include <opencv2/opencv.hpp>

int main() {
    cv::Mat prevImg, nextImg;
    std::vector<cv::Point2f> prevPts, nextPts;
    std::vector<uchar> status;
    std::vector<float> err;

    // Load previous and current frames

    // Detect features in the previous frame and store them in prevPts

    // Calculate optical flow
    cv::calcOpticalFlowPyrLK(prevImg, nextImg, prevPts, nextPts, status, err);

    // Print the tracked points
    for (int i = 0; i < prevPts.size(); i++) {
        if (status[i]) {
            std::cout << "Tracked point: (" << prevPts[i].x << ", " << prevPts[i].y << ") -> "
                      << "(" << nextPts[i].x << ", " << nextPts[i].y << ")" << std::endl;
        } else {
            std::cout << "Tracking failed for point: (" << prevPts[i].x << ", " << prevPts[i].y << ")" << std::endl;
        }
    }

    return 0;
}

🍉OpenCV代码

        我们首先导入所需的头文件和库,包括OpenCV、Eigen等。然后,我们定义了两幅图像的文件路径和一些辅助函数。

        接下来,我们实现了两个函数OpticalFlowSingleLevelOpticalFlowMultiLevel,用于单层和多层光流估计。这两个函数使用Lucas-Kanade算法来估计图像中关键点的光流向量。OpticalFlowSingleLevel函数用于单层光流估计,而OpticalFlowMultiLevel函数使用金字塔方法在多个尺度上进行光流估计。

        在主函数中,我们加载了两幅图像并使用Good Features to Track (GFTT)检测算法检测了第一幅图像中的关键点。然后,我们分别使用单层光流估计和多层光流估计方法跟踪这些关键点在第二幅图像中的位置。我们还使用OpenCV的calcOpticalFlowPyrLK函数进行验证。

        最后,我们将结果进行可视化,并显示了单层光流估计、多层光流估计和OpenCV光流估计的对比图像。通过这些图像,我们可以观察到不同方法的跟踪效果。

#include <opencv2/opencv.hpp>
#include <string>
#include <Eigen/Core>
#include <Eigen/Dense>

using namespace std;
using namespace cv;

// this program shows how to use optical flow

string file_1 = "./1.png";  // 第一幅图像
string file_2 = "./2.png";  // 第二幅图像

// TODO 实现这个函数
/**
 * 单层光流估计
 * @param [in] img1 第一幅图像
 * @param [in] img2 第二幅图像
 * @param [in] kp1 第一幅图像中的关键点
 * @param [in|out] kp2 第二幅图像中的关键点,如果为空,则使用kp1的初始值
 * @param [out] success 关键点是否成功跟踪的标志
 * @param [in] inverse 是否使用逆向公式?
 */
void OpticalFlowSingleLevel(
        const Mat &img1,
        const Mat &img2,
        const vector<KeyPoint> &kp1,
        vector<KeyPoint> &kp2,
        vector<bool> &success,
        bool inverse = false
);

// TODO 实现这个函数
/**
 * 多层光流估计,默认金字塔的尺度为2
 * 图像金字塔将在函数内部创建
 * @param [in] img1 第一幅图像
 * @param [in] img2 第二幅图像
 * @param [in] kp1 第一幅图像中的关键点
 * @param [out] kp2 第二幅图像中的关键点
 * @param [out] success 关键点是否成功跟踪的标志
 * @param [in] inverse 设置为true以启用逆向公式
 */
void OpticalFlowMultiLevel(
        const Mat &img1,
        const Mat &img2,
        const vector<KeyPoint> &kp1,
        vector<KeyPoint> &kp2,
        vector<bool> &success,
        bool inverse = false
);

/**
 * 根据参考图像获取灰度值(双线性插值)
 * @param img 图像
 * @param x x坐标
 * @param y y坐标
 * @return 像素的灰度值
 */
inline float GetPixelValue(const cv::Mat &img, float x, float y) {
    uchar *data = &img.data[int(y) * img.step + int(x)];
    float xx = x - floor(x);
    float yy = y - floor(y);
    return float(
            (1 - xx) * (1 - yy) * data[0] +
            xx * (1 - yy) * data[1] +
            (1 - xx) * yy * data[img.step] +
            xx * yy * data[img.step + 1]
    );
}


int main(int argc, char **argv) {

    // 图像,注意这里使用的是CV_8UC1而不是CV_8UC3
    Mat img1 = imread(file_1, 0);
    Mat img2 = imread(file_2, 0);

    // 关键点,这里使用GFTT(Good Features to Track)算法
    vector<KeyPoint> kp1;
    Ptr<GFTTDetector> detector =GFTTDetector::create(500, 0.01, 20); // 最多检测500个关键点
    detector->detect(img1, kp1);

    // 现在让我们在第二幅图像中跟踪这些关键点
    // 首先在验证图片中使用单层LK
    vector<KeyPoint> kp2_single;
    vector<bool> success_single;
    OpticalFlowSingleLevel(img1, img2, kp1, kp2_single, success_single);

    // 然后测试多层LK
    vector<KeyPoint> kp2_multi;
    vector<bool> success_multi;
    OpticalFlowMultiLevel(img1, img2, kp1, kp2_multi, success_multi);

    // 使用OpenCV的光流进行验证
    vector<Point2f> pt1, pt2;
    for (auto &kp: kp1) pt1.push_back(kp.pt);
    vector<uchar> status;
    vector<float> error;
    cv::calcOpticalFlowPyrLK(img1, img2, pt1, pt2, status, error, cv::Size(8, 8));

    // 绘制这些函数的差异
    Mat img2_single;
    cv::cvtColor(img2, img2_single, CV_GRAY2BGR);
    for (int i = 0; i < kp2_single.size(); i++) {
        if (success_single[i]) {
            cv::circle(img2_single, kp2_single[i].pt, 2, cv::Scalar(0, 250, 0), 2);
            cv::line(img2_single, kp1[i].pt, kp2_single[i].pt, cv::Scalar(0, 250, 0));
        }
    }

    Mat img2_multi;
    cv::cvtColor(img2, img2_multi, CV_GRAY2BGR);
    for (int i = 0; i < kp2_multi.size(); i++) {
        if (success_multi[i]) {
            cv::circle(img2_multi, kp2_multi[i].pt, 2, cv::Scalar(0, 250, 0), 2);
            cv::line(img2_multi, kp1[i].pt, kp2_multi[i].pt, cv::Scalar(0, 250, 0));
        }
    }

    Mat img2_CV;
    cv::cvtColor(img2, img2_CV, CV_GRAY2BGR);
    for (int i = 0; i < pt2.size(); i++) {
        if (status[i]) {
            cv::circle(img2_CV, pt2[i], 2, cv::Scalar(0, 250, 0), 2);
            cv::line(img2_CV, pt1[i], pt2[i], cv::Scalar(0, 250, 0));
        }
    }

    cv::imshow("tracked single level", img2_single);
    cv::imshow("tracked multi level", img2_multi);
    cv::imshow("tracked by opencv", img2_CV);
    cv::waitKey(0);

    return 0;
}

void OpticalFlowSingleLevel(
        const Mat &img1,
        const Mat &img2,
        const vector<KeyPoint> &kp1,
        vector<KeyPoint> &kp2,
        vector<bool> &success,
        bool inverse
) {

    // 参数
    int half_patch_size = 4;
    int iterations = 10;
    bool have_initial = !kp2.empty();

   ```cpp
    for (size_t i = 0; i < kp1.size(); i++) {
        auto kp = kp1[i];
        double dx = 0, dy = 0; // dx,dy 需要估计
        if (have_initial) {
            dx = kp2[i].pt.x - kp.pt.x;
            dy = kp2[i].pt.y - kp.pt.y;
        }

        double cost = 0, lastCost = 0;
        bool succ = true; // 标志关键点是否成功跟踪

        // Gauss-Newton 迭代
        for (int iter = 0; iter < iterations; iter++) {
            Eigen::Matrix2d H = Eigen::Matrix2d::Zero();
            Eigen::Vector2d b = Eigen::Vector2d::Zero();
            cost = 0;

            if (kp.pt.x + dx <= half_patch_size || kp.pt.x + dx >= img1.cols - half_patch_size ||
                kp.pt.y + dy <= half_patch_size || kp.pt.y + dy >= img1.rows - half_patch_size) {   // 超出图像边界
                succ = false;
                break;
            }

            // 计算误差和雅可比矩阵
            for (int x = -half_patch_size; x < half_patch_size; x++)
                for (int y = -half_patch_size; y < half_patch_size; y++) {

                    // TODO 从这里开始编写代码 (~8 行)
                    double error = 0;
                    Eigen::Vector2d J;  // 雅可比矩阵
                    if (inverse == false) {
                        // 正向雅可比矩阵
                    } else {
                        // 逆向雅可比矩阵
                        // 注意,当 dx、dy 更新时,J 不会改变,所以我们可以存储它并且只计算误差
                    }

                    // 计算 H、b 和设置 cost;
                    H;
                    b;
                    cost;
                    // TODO 编写代码结束
                }

            // 计算更新量
            // TODO 从这里开始编写代码 (~1 行)
            Eigen::Vector2d update;
            // TODO 编写代码结束

            if (isnan(update[0])) {
                // 有时会出现当我们拥有黑色或白色的补丁时,H 是不可逆的情况
                cout << "update is nan" << endl;
                succ = false;
                break;
            }
            if (iter > 0 && cost > lastCost) {
                cout << "cost increased: " << cost << ", " << lastCost << endl;
                break;
            }

            // 更新 dx、dy
            dx += update[0];
            dy += update[1];
            lastCost = cost;
            succ = true;
        }

        success.push_back(succ);

        // 设置 kp2
        if (have_initial) {
            kp2[i].pt = kp.pt + Point2f(dx, dy);
        } else {
            KeyPoint tracked = kp;
            tracked.pt += cv::Point2f(dx, dy);
            kp2.push_back(tracked);
        }
    }
}

void OpticalFlowMultiLevel(
        const Mat &img1,
        const Mat &img2,
        const vector<KeyPoint> &kp1,
        vector<KeyPoint> &kp2,
```cpp
        vector<bool> &success,
        bool inverse
) {
    // 参数
    int half_patch_size = 4;
    int iterations = 10;
    bool have_initial = !kp2.empty();

    // 创建图像金字塔
    vector<Mat> pyramid1, pyramid2;
    buildOpticalFlowPyramid(img1, pyramid1, cv::Size(2, 2));
    buildOpticalFlowPyramid(img2, pyramid2, cv::Size(2, 2));

    for (size_t i = 0; i < kp1.size(); i++) {
        auto kp = kp1[i];
        double dx = 0, dy = 0; // dx,dy 需要估计
        if (have_initial) {
            dx = kp2[i].pt.x - kp.pt.x;
            dy = kp2[i].pt.y - kp.pt.y;
        }

        double cost = 0, lastCost = 0;
        bool succ = true; // 标志关键点是否成功跟踪

        // Gauss-Newton 迭代
        for (int level = pyramid1.size() - 1; level >= 0; level--) {
            Mat img1_level = pyramid1[level];
            Mat img2_level = pyramid2[level];

            // 根据金字塔的尺度调整 dx、dy
            dx *= 2;
            dy *= 2;

            cost = 0;
            lastCost = 0;

            for (int iter = 0; iter < iterations; iter++) {
                Eigen::Matrix2d H = Eigen::Matrix2d::Zero();
                Eigen::Vector2d b = Eigen::Vector2d::Zero();
                cost = 0;

                if (kp.pt.x + dx <= half_patch_size || kp.pt.x + dx >= img1_level.cols - half_patch_size ||
                    kp.pt.y + dy <= half_patch_size || kp.pt.y + dy >= img1_level.rows - half_patch_size) {   // 超出图像边界
                    succ = false;
                    break;
                }

                // 计算误差和雅可比矩阵
                for (int x = -half_patch_size; x < half_patch_size; x++)
                    for (int y = -half_patch_size; y < half_patch_size; y++) {

                        // TODO 从这里开始编写代码 (~8 行)
                        double error = 0;
                        Eigen::Vector2d J;  // 雅可比矩阵
                        if (inverse == false) {
                            // 正向雅可比矩阵
                        } else {
                            // 逆向雅可比矩阵
                            // 注意,当 dx、dy 更新时,J 不会改变,所以我们可以存储它并且只计算误差
                        }

                        // 计算 H、b 和设置 cost;
                        H;
                        b;
                        cost;
                        // TODO 编写代码结束
                    }

                // 计算更新量
                // TODO 从这里开始编写代码 (~1 行)
                Eigen::Vector2d update;
                // TODO 编写代码结束

                if (isnan(update[0])) {
                    // 有时会出现当我们拥有黑色或白色的补丁时,H 是不可逆的情况
                    cout << "update is nan" << endl```cpp
                    succ = false;
                    break;
                }
                if (iter > 0 && cost > lastCost) {
                    cout << "cost increased: " << cost << ", " << lastCost << endl;
                    break;
                }

                // 更新 dx、dy
                dx += update[0];
                dy += update[1];
                lastCost = cost;
                succ = true;
            }

            if (level > 0) {
                dx /= 2;
                dy /= 2;
            }
        }

        success.push_back(succ);

        // 设置 kp2
        if (have_initial) {
            kp2[i].pt = kp.pt + Point2f(dx, dy);
        } else {
            KeyPoint tracked = kp;
            tracked.pt += cv::Point2f(dx, dy);
            kp2.push_back(tracked);
        }
    }
}

🌟总结

🍎源码

        光流法是计算机视觉领域中用于估计图像中像素点运动的一种技术。它通过分析图像序列中像素点的灰度值变化,推断出像素点的运动方向和速度。光流法基于以下两个假设:

  1. 亮度恒定假设(Brightness Constancy Assumption):在一个像素点的邻域内,随着时间的推移,像素点的灰度值保持不变。
  2. 空间连续性假设(Spatial Coherence Assumption):邻近的像素点在图像中的运动方向和速度相似。

        光流法的主要思想是通过比较图像序列中的相邻帧来计算像素点的位移向量。常见的光流算法包括Lucas-Kanade算法和Horn-Schunck算法。这些算法基于局部像素点间的亮度差异,建立了一个优化问题,并使用最小二乘法或其他优化方法求解。

        光流法在计算机视觉中有广泛的应用,包括物体跟踪、运动分析、视频稳定、三维重建、虚拟现实和增强现实等领域。它可以用于目标检测和跟踪,分析人体动作和姿态,实现视频稳定化和运动捕捉,以及构建三维场景模型等。

        然而,光流法也存在一些限制。首先,它假设像素点在邻域内亮度恒定,这在存在光照变化或纹理缺失的情况下可能不成立。其次,光流法对于大的位移或快速运动的物体可能无法准确估计。此外,光流法对图像噪声和运动模糊也比较敏感。

        为了克服这些问题,研究人员提出了许多改进的光流算法和技术,如金字塔光流、密集光流、稠密光流和基于学习的光流等方法。这些方法利用多尺度信息、稠密采样和机器学习等技术来提高光流估计的准确性和鲁棒性。

总结起来,光流法是一种用于估计图像中像素点运动的重要技术。它在计算机视觉领域有广泛的应用,并为物体跟踪、运动分析、视频稳定、三维重建和虚拟现实等任务提供了基础。然而,光流法也存在一些局限性,需要根据具体的应用场景和需求选择合适的算法和技术。同时,随着计算机视觉领域的不断发展,新的光流算法和改进技术不断涌现,为光流法的性能和应用提供了更多的可能性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/723083.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Bubbliiiing视频记录】Pytorch 搭建自己的Unet语义分割平台

来源 b站 地址 什么是语义分割 语义分割&#xff1a;对图像每个像素点进行分类 常见神经网络处理过程&#xff1a;Encoder提取特征&#xff0c;接着Docoder恢复成原图大小的图片 UNet整体结构 分为三个部分 主干特征提取部分&#xff1a; 卷积和最大池化的堆叠获得五个初…

Snail-Camunda

gitee地址&#xff1a;snail-camunda 当前版本&#xff1a;V1.0 &#x1f496;&#x1f496;&#x1f496;希望大家动动发财的小手帮忙star一下&#xff0c;感谢&#xff01; ⭐项目介绍 本项目是对Camunda接口…

data和filter协议文件包含

实验目的 通过本实验&#xff0c;了解php封装伪协议&#xff0c;掌握filter协议和data协议的用法 实验环境 操作机&#xff1a;kali 靶机&#xff1a;Windows Server 2003 实验地址&#xff1a;http://靶机ip/exp/include2/filter/ http://靶机ip/exp/include/include3.php…

【C++ OJ练习】1.求和

1.题目链接 求123...n_牛客题霸_牛客网 2.解题思路 利用C构造函数的特性 既然不能使用循环 那么我们可以定义n个对象 就会调用n次构造函数 去代替循环 每次调用的时候进行加法的计算 3.代码 class Sum { public:Sum(){_sum _i;_i;}static void Init(){_i 1;_sum 0;}st…

ruoyi若依 组织架构设计--[ 菜单管理 ]

ruoyi若依 组织架构设计--[ 菜单管理 ] 1. 关于ruoyi组织架构各个表的设计2. 从产品的角度考虑 [ 菜单管理 ]2.1菜单树List页面2.2 菜单树新增2.3 菜单树修改2.4 删除菜单 1. 关于ruoyi组织架构各个表的设计 1.部门表 ancestors字段。 添加这个字段&#xff0c;方便列举所有子…

用maven安装Netty

Netty简单介绍 官网&#xff1a;https://netty.io/index.html Netty是一个异步的、事件驱动的网络应用框架&#xff0c;既可用于开发高性能的服务端&#xff0c;也可用于开发客户端。 Netty是一个NIO的客户端和服务端框架&#xff0c;用于快速、容易地开发网络应用&#xff0c…

Linux·图解Linux网络包接收过程

因为要对百万、千万、甚至是过亿的用户提供各种网络服务&#xff0c;所以在一线互联网企业里面试和晋升后端开发同学的其中一个重点要求就是要能支撑高并发&#xff0c;要理解性能开销&#xff0c;会进行性能优化。而很多时候&#xff0c;如果你对Linux底层的理解不深的话&…

Paddle OCR 安装使用教程

简介 PaddleOCR是飞浆开源文字识别模型&#xff0c;最新开源的超轻量PP-OCRv3模型大小仅为16.2M。同时支持中英文识别&#xff1b;支持倾斜、竖排等多种方向文字识别&#xff1b;支持GPU、CPU预测&#xff0c;并且支持使用paddle开源组件训练自己的超轻量模型&#xff0c;对于…

微信小程序路由跳转,API调用,页面传值

路由跳转 wx.switchTab 跳转到 tabBar 页面&#xff0c;并关闭其他所有非 tabBar 页面。 wx.switchTab({url: /pages/tarbar/index })wx.reLaunch 关闭所有页面&#xff0c;打开到应用内的某个页面。 wx.reLaunch({url: /pages/index/index })wx.redirectTo 关闭当前页面…

13服务端实战:初始化项目

本章开始将进行服务端的实战开发&#xff0c;包含了 Devops、服务端以及其他涉及到的中间件的模块。 由于 NestJS 的文档非常完善以及之前有写过比较完整的 NestJS 的小册&#xff0c;如果对 NestJS 非常熟悉的同学可以快速跳过本章&#xff0c;另外整个实战篇的代码都会按照流…

shell命令以及运行原理、Linux权限、粘滞位

今天我带来Linux重点知识&#xff1a;shell命令以及运行原理、Linux权限、粘滞位的知识点。 目录 shell命令以及运行原理Linux权限的概念Linux下的用户su命令 Linux权限管理文件访问者的分类文件的类型和访问权限文件权限值的表达方法 Linux关于权限的知识点补充1.Linux创建用户…

Unity3d C#使用XCharts数据显示格式说明(如:数据类型、数据显示为百分比%等)

前言 XCharts是开源且比较强大的插件&#xff0c;在Unity3d中搭建UI时常常使用的数据图表的制作插件&#xff0c; 特别是当下的数字沙盘、数字孪生等项目中应用较广。笔者公司也一直在使用该插件&#xff0c;本文主要是在开发过程中的一个小需求引发的整理分享。在项目中需要将…

vue2 若依项目,使用plotly.js-dist图表库,将数据图表一键导出为图片

此代码适用的场景是一个页面有多个数据图表。 首先需要拿到你生成数据图表的数据&#xff0c; 然后赋值给一个数组&#xff0c;数组需要在data定义&#xff0c;还需要去重。 // 检查是否有相同的parameter值const hasDuplicate this.toImageArr.some(iiem > iiem.paramete…

由于找不到xinput1_3.dll,无法继续执行代码的解决方法都有哪些?

关于由于找不到xinput1_3.dll,无法继续执行代码这个问题&#xff0c;其实在网上经常看别人提及&#xff0c;要解决这个问题&#xff0c;还是不难的&#xff0c;今天小编就来给大家详细的说说关于这个问题的解决方法吧&#xff0c;本文会介绍多种的修复方法&#xff0c;下面一起…

商品数据封装接口(PHP/Java/Python)

封装 在面向对象程式设计方法中&#xff0c;封装是指—种将抽象性函式接口的实现细节部分包装、隐藏起来的方法 封装可以被认为是一个保护屏障&#xff0c;防止该类的代码和数据被外部类定义的代码随机访问 要访问该类的代码和数据&#xff0c;必须通过严格的接口控制 封装最…

提升生产效率的关键:如何选择适合您企业的设备管理系统?

在现代工业生产中&#xff0c;设备管理对于提升生产效率和降低成本至关重要。一个高效的设备管理系统可以帮助企业实现设备的有效监控、维护和优化&#xff0c;从而提高设备的可靠性、降低停机时间&#xff0c;并最终提升生产效率。选择适合企业的设备管理系统可能是一个复杂的…

Vision Pro:多人XR协作这样玩才对味

早在iOS 15系统更新中&#xff0c;苹果就推出了SharePlay功能&#xff0c;该功能主要针对多人协作和社交&#xff0c;允许用户在FaceTime会话中一起看剧、听歌、共享屏幕。Vision Pro头显也支持该功能&#xff0c;允许用户共享AR图像和空间上下文信息&#xff0c;实现远程可视化…

LeetCode 每日一题——2178. 拆分成最多数目的正偶数之和

1.题目描述 2178. 拆分成最多数目的正偶数之和 给你一个整数 finalSum 。请你将它拆分成若干个 互不相同 的正偶数之和&#xff0c;且拆分出来的正偶数数目 最多 。 比方说&#xff0c;给你 finalSum 12 &#xff0c;那么这些拆分是 符合要求 的&#xff08;互不相同的正偶…

Spring Boot 中的服务发现

Spring Boot 中的服务发现 Spring Boot 是一个非常流行的 Java Web 开发框架&#xff0c;它提供了很多工具和组件来简化 Web 应用程序的开发。其中&#xff0c;服务发现是 Spring Boot 中的一个非常重要的组件&#xff0c;它可以帮助我们自动地发现和管理应用程序中的服务。 什…

Prometheus 时序数据

一 时序索引 Prometheus 存储的是时序数据&#xff0c;时间戳&#xff08;timestamp&#xff09;来源于服务端本地的系 统时间。Prometheus 使用 Unix 时间戳&#xff08;即自 1970 年 1 月 1 日 00:00:00 UTC 起经过的秒数&#xff09;表示时间。 数 据 格 式 &#xff1a; …