开发OpenCV代码(C++),视频边缘检测再做差,记录每帧处理时间,板卡性能选型,有关OpenCV库函数解析(完结)

Prompt与进度推进

Step 1 构思代码的自然语言描述,生成原始代码

Prompt 1(描述问题,提供思路)

项目背景:我现在手头有一块RK3588的开发板,装有ubuntu20.04系统,需要在上面跑OpenCV库的C++代码,对视频实时进行处理。问题:现在我需要在项目前期通过一些测试代码来验证这块板卡的性能,看其是否满足需求,给我一点思路。

Useful Answer Recording :

编写测试代码:
1.编写简单的C++程序来读取视频文件或摄像头流。
2.应用一些基本的OpenCV图像处理功能,如色彩空间转换、滤波、边缘检测等。
3.测量并记录处理每一帧所需的时间,以评估性能。

Prompt 2(提出具体要求,生成原始代码)

帮我编写一个C++代码,可以使用OpenCV库,要求如下:
1.输入一个视频文件,分辨率为4K(2160*3840),帧率为30帧,时长在2分钟左右
2.连续地截取视频中的图像进行处理,要求视频以正常速度播放,中间来不及处理的图像全部跳过
3.对每张图像的处理:1.灰度化 2.拉普拉斯边缘检测
4.对于处理后的图像:在处理完一张图像后,将其与上一张处理后的图像作差值,生成的差值图像保存在一个文件夹中
5.计算每张图片的处理时间,将这一系列数字(单位为ms)保存在一个文本文件中
6.将这一系列数字取倒数,作为处理的帧率,同样保存在一个文本文件中

Step 2 读代码,学习OpenCV库函数

int main(int argc, char argv)**

  • argc(argument count) 是一个整数,表示命令行参数的数量,包括程序本身的名称(因此终端输入参数个数为argc-1)
  • argv(argument variable) 是一个指向字符数组即字符串的指针数组实际存的是首字符的地址。argv存储输入的命令行参数(从[1]开始,[0]指向程序本身)

小番外NO.1 **argv与*argv[]的区分。

**argv常见于c++,*argv[]常见于c。

注意,由于MD输入法自动匹配*来进行字体强调,为防止误触,下面常在.*前加句点,句点无意义可忽略。并且全部省略了前面的char

二者在作为函数参数时其实质是一样的,都是二级指针。也就是“指向字符指针表示字符串,前面说过的首地址的指针”。下面做一些解释:

i.**.argv本身就是二级指针,没什么。

ii.*argv[]本身其实是一个指针数组,也就是放了一堆指针的数组。放什么指针呢?自然还是字符指针。也就是说.*argv[]表示了多个字符串全体。但是在作为函数参数时,为了不在堆栈中拷贝数组的数据,数组退化为首地址。这样可以保持数据的一致性,不会出现多份拷贝不相同的冲突,同时节约内存资源。

而.**argv 是一个指向指针的指针,它指向的是 argv 数组的第一个元素(也是一个指针)这样就找到了数组。如果要找到第二个,第三个字符串首地址,则只需要像访问数组元素一样使用 argv[1] 或 argv[2] 即可,也就找到了每一个字符串,也就是我们存的数据。

iii.需要注意的是。字符串的长度,是事先不知道的,需要遍历数组,在访问到’/0’的停止字符后字符串截止。因此我们只需要知道字符串的首地址就可以操作字符串,每个字符串后续的地址是无关紧要的。

iv.标准库函数 strlen(头文件string.h),它接受一个字符指针作为参数,并返回该字符串的长度(不包括结尾的空字符)。strlen 函数通过遍历字符串,计算遇到的字符数,直到遇到空字符为止。如:

  size_t length = strlen(argv[1]);  

v.另一个需要注意的是,在数组不作为函数参数传递时,是不会退化为指针的。例如:

void foo(int arr[]) {  //arr[]作为函数参数,退化为一个指向 int 的指针 
    sizeof(arr);// 将返回指针的大小,而不是整个数组的大小  
}  
int main(){  
    int arr[10];  
    // 在这里,arr是一个具有 10 个 int 元素的数组  
    sizeof(arr) //将返回整个数组的大小(通常是 10 * sizeof(int))  
    foo(arr); // 当数组arr作为函数参数传递时,它退化为指向其第一个元素的指针  
    return 0;  
}

cv::VideoCapture

  • cv::VideoCapture cap(video_file)
    读取并打开视频,准备读取,但不会马上放进内存中读取。如要读取:
  • cap.read(frame)
if (!cap.isOpened()) {
    std::cerr << "Error opening video file: " << video_file << std::endl;
    return -1;
}
  • cap.isOpened()
    如果成功打开视频,返回1;不成功,返回0
  • std::cerr
    C++标准库中的一个输出流对象,属于 头文件的一部分,与std::cout不同的是,std::cerr 是非缓冲的,这意味着写入 std::cerr 的数据通常会立即被发送到其目标设备,而不会等待缓冲区填满或直到程序结束。

注:如果要在VS中模拟命令行输入,则点击属性管理器>右击代码 >属性>配置属性-调试>命令参数>右边小三角>编辑>写入参数后>应用

  • cap >> frame;//从视频对象中获取下一帧,Mat类型,>>是一个stream operater , 流处理操作符

cv::MAT

用于表示图像或任意维度稠密数组的类。cv::Mat 类包含两个主要部分:矩阵头(包含矩阵的尺寸、存储类型、存储位置等信息)和一个指向存储数据的矩阵的指针。这种设计允许高效地复制矩阵头而不是复制整个矩阵数据,从而实现轻量级的对象传递。

  1. 数据类型:CV_8U(8位无符号整数,常用于灰度图像)、CV_32F(32位浮点数,常用于图像处理中的浮点运算)
  2. ROI支持
  3. 连续性:如果矩阵在内存中是连续的,cv::Mat 会存储一个标志来表示这一点,从而可以更快地遍历数据。
  4. 通道:对于彩色图像,cv::Mat 通常包含多个通道(如 RGB 或 BGR),可以通过拆分或合并通道来操作单个颜色通道。
  • cv::imread
image = cv::imread(argv[1], cv::IMREAD_COLOR); // 读取彩色图像
  1. 传入地址
  2. IMREAD_COLOR=1
  • image.empty()
    空则为1 ,非空为0

resize改变图像尺寸

显示尺寸较大的高清图片时,需要压缩图像尺寸,否则无法显示全貌

  • `cv::Size desiredSize(800, 600);` 
    

是一个类,用法:desiredSize(宽,高)

  • vs中如果需要前进、后退(如使用函数定义跳转时),可在左上角点击小箭头,或者快捷键Ctrl-后退Ctrlshift-前进
  • static_cast()
    强制类型转换,如下例:
double d = 3.14;  
int i = static_cast<int>(d);  // i 的值现在是 3

注意,static_cast<>()不进行类型检查,可能会产生内存错误

  • 将现有图片image等比缩小
cv::Size desiredSize(static_cast<int>(image.cols * scaleFactor), static_cast<int>(image.rows * scaleFactor));
//相乘后取整,得到目标宽高像素值,后续使用线性压缩变换
  • image.cols列像素数
  • image.rows行像素数
cv::resize(image, resizedImage, desiredSize, 0, 0, cv::INTER_LINEAR);
  1. image 原图Mat
  2. resizedImage 目标Mat
  3. 目标宽高像素
    1. fx , fy 比例因子与3. 不共用,若有3. 则自动忽略
  4. cv::INTER_LINEAR 是一种双线性插值方法,它考虑了源图像中 2x2 像素邻域的值来计算输出图像中的每个像素值。
  • 命令行给主函数传递其他类型参数
    思路:传递的还是char*(string)字符串,但是可以用转换函数解释为其他类型,如:
    std::atof()
  1. std::atof 函数将字符串转换为浮点数。
  2. 请注意,std::atof 在遇到无法解析为数字的字符时会停止解析,并返回已经解析的部分。
  3. 如果字符串无法转换为有效的浮点数,std::atof 将返回0.0。
    附上resize部分代码,输入图像地址和图像尺寸的乘积系数即可展示图像:
#include <opencv2/opencv.hpp>  
#include <iostream>  

int main(int argc, char** argv) {
    if (argc < 2) {
        std::cerr << "Usage: " << argv[0] << " <image_file>" << std::endl;
        return -1;
    }

    cv::Mat image;
    float scaleFactor = std::atof(argv[2]);//输入浮点数,作为图像尺寸的乘积系数
    image = cv::imread(argv[1], cv::IMREAD_COLOR); // 读取彩色图像  

    if (image.empty()) { // 检查图像是否正确加载  
        std::cerr << "Could not open or find the image!" << std::endl;
        return -1;
    }

    // 缩放图像以适应屏幕  
    cv::Mat resizedImage;
    cv::Size desiredSize(static_cast<int>(image.cols * scaleFactor), static_cast<int>(image.rows * scaleFactor));
    //乘上系数得到目标尺寸
    cv::resize(image, resizedImage, desiredSize, 0, 0, 1);
    cv::namedWindow("Display window", cv::WINDOW_AUTOSIZE); // 创建一个窗口  
    cv::imshow("Display window", resizedImage); // 在窗口中显示缩放后的图像  
    cv::waitKey(0); // 等待按键事件  
    return 0;
}

std::ofstream

output file stream 输出文件流,包含在标准库中

std::ofstream time_log("processing_times.txt",std::ios::app);
  • 这行代码创建了一个名为time_logstd::ofstream对象,并尝试打开一个名为processing_times.txt的文件以进行写入。
  • 如果processing_times.txt文件不存在,它会被创建。
  • 若不加std::ios::app,则为输出模式,如果文件已存在,它的内容将被清空。在加上std::ios::app后则为append追加模式,新写入的数据不会覆盖之前的数据,而会添加到文件的末尾。
  • 可以使用time_log对象来向processing_times.txt文件写入数据。
  • time_log.is_open()
    文件打开成功为1 ,失败为0

小番外NO.2 选python还是c++?

  1. 可移植性(Win & Linux)
    python代码在每次运行时由解释器转换成字节码(.pyc)再转成机器语言,这个过程只要使用同一种解释器,就可以轻松地保证代码的通用性。不过c++也不差,只不过略逊一筹。
  2. 开发工具
    python的开发工具大多有跨平台特性,而C++的一些工具则在Linux上面临是否适配的问题。例如Linux上貌似没有VS,只有VScode(as far as I know)
  3. 代码开发难度
    python这种动态类型语言在难度上大多时候显著低于c++。并且可读性也更强。上手也更快。
  4. 性能
    python generally需要更长的运行时间,但是如果大量地采用OpenCV这种高级库函数开发,其实C++并没有优势,这里主要取决于你用的库函数的特性,如果对性能敏感,建议实际上板跑一段代码试一试,其他方法并不靠谱。
  5. Last But Definitely Not Least
    取决于你的Co-Workers习惯于什么代码,可能轮不到你选呢哈哈。所以说把用各种语言开发的经历都当作一次机会吧,不要总觉得没用到自己觉得最方便的语言就太过沮丧,总不能说换个代码一下子就寸步难行吧。

tips:注意平台的路径分隔符:Windows 上的 \和Linux上的/(话说csdn的MD好多bug啊,\这个符号就是不能加任何类型强调,/就可以,奇怪)

chrono库

  • auto start_time = std::chrono::high_resolution_clock::now();
  1. high_resolution_clock是一个类,表示高精度时钟
  2. now()是一个静态成员函数,获取当前时间点,一个time_point对象(从某个时刻开始 , it depends )
  3. auto: 这是一个C++11引入的关键字,此处被推断为time_point

cv::cvtColor

色彩变换函数

cv::cvtColor(frame, gray_frame, cv::COLOR_BGR2GRAY);
  1. 待处理Mat
  2. 目标Mat
  3. RGB/BGR to GRAY

cv::Laplacian

拉普拉斯变换是一种二阶导数滤波器,可以用来增强图像中的边缘和其他高频部分

cv::Laplacian(gray_frame, laplacian, CV_16S, 3, 1, 0, cv::BORDER_DEFAULT);
  1. gray_frame是输入图像(src)。
  2. laplacian是输出图像(dst),它将包含拉普拉斯变换的结果。
  3. CV_16S是输出图像的深度(ddepth),表明结果应存储为16位有符号整数。因为拉普拉斯变换的结果可能有负数。下面还会对这个负数进行处理。
  4. 3是孔径大小(ksize),必须为正奇数。
  5. 1是缩放因子(scale),可对结果进行缩放。
  6. 0是增量(delta),可以加到所有的结果上去,相当于一个偏置,一般不用。
  7. cv::BORDER_DEFAULT是边界类型(borderType)。
  • cv::convertScaleAbs(laplacian, edges);
    用于对输入数组进行线性变换以适应0到255的范围,再取绝对值
    laplacian是输入
    edges是输出,类型为CV_8U(8位无符号整数)

获取当前视频播放到的帧位置,并将这个位置转换成一个字符串

cap.get(cv::CAP_PROP_POS_FRAMES)

获取当前视频播放到的帧位置(成员函数get),并将这个位置(数字)转换成一个字符串,帧数从0开始,0表示第一帧

  • 这个get貌似还能做视频关键帧有关的操作,后面可以再研究下
  • std::to_string()转换为字符串

cv::imwrite(addr+image)

在指定地址保存图像文件,图像格式包含在地址中,地址可使用string加法生成

Mat副本 = cv::Mat.clone(Mat对象)

clone()是Mat类的一个成员函数,用来copy一个Mat对象,注意不能直接等号赋值,因为那样只是复制了矩阵头和指针,没有包含数据和其他信息。如:

prev_edges = edges.clone();  

std::chrono::duration_caststd::chrono::milliseconds(end_time-start_time)

std::chrono::duration_cast<>()用于将一个time_point的差值转换成其他形式,在这里转换为了毫秒

time_log << processing_time.count()

<<是插入运算符
.count()会返回时间数值

Step 3 调试代码

问题1(check)

记录的times和fps文本文档不会在打开时清空
解决:

 {// 清空文件
std::ofstream time_log("processing_times.txt", std::ios::trunc); 
std::ofstream fps_log("processing_fps.txt", std::ios::trunc);
} // 这里 time_log 会被自动关闭,因为它已经离开了作用域 
std::ofstream time_log("processing_times.txt", std::ios::app);
std::ofstream fps_log("processing_fps.txt", std::ios::app);

问题2(check)

  • 只保存了第一张图片,而且不会退出while循环,怀疑是始终停留在第一帧图片
  • 尝试:第一帧图片其实只要正常做完边缘检测存起来就好了,其他步骤都去掉,看看文本文档是否还能生成
  • 反馈:这个思路不成立,第一次处理后不管下一次读的是不是下一帧,程序还是会进后续的处理程序。不过“当前帧数”始终是0,应该就是这个问题。cap >> frame; 这个代码没法连着往后读取
  • 反馈:cap>>frame好像没问题
  • [后续]换了一台电脑就一点问题没有了。。。晕。不知道为啥

小番外NO.3 我发现其实自己的gmail账号就能注册chatGPT

  • 我咋记得之前是不行的。。。还是GPT好用啊,文心一言再见了。
  • [后续]GPT的稳定性还是有问题,今天打开电脑不管怎么调试网络都打不开了,又回归文心。。。
  • 后面又试了试,我目前的网络环境是不支持100%成功率访问的。基本上就是试一下US服务器和Singapore服务器,不行的话就放弃了。

问题3 (check)

  • 在代码中保存了差值图片,每次运行之后都会生成一大堆图片,想每次自动新建一个文件夹把图片保存进去。改动代码如下:
#include <filesystem>
...
// 为文件夹名创建一个时间戳
auto timestamp = std::chrono::system_clock::now();
std::string folder_name = "diff_images_" + std::to_string(std::chrono::duration_cast<std::chrono::seconds>(timestamp.time_since_epoch()).count());
std::filesystem::create_directory(folder_name);
...
// 保存差值图像  
std::string filename = folder_name + "/diff_frame" + std::to_string(cap.get(cv::CAP_PROP_POS_FRAMES)) + ".png";
//注意要把创建的folde_name加在路径的最前面
cv::imwrite(filename, diff);
  • 需要注意的是,filesystem库函数及其功能只有在C++17以上的标准下才可使用,要注意一下自己的环境是否支持。比如debian 9就不太行
  • 18
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值