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;不成功,返回0std::cerr
C++标准库中的一个输出流对象,属于 头文件的一部分,与std::cout
不同的是,std::cerr
是非缓冲的,这意味着写入std::cerr
的数据通常会立即被发送到其目标设备,而不会等待缓冲区填满或直到程序结束。
注:如果要在VS中模拟命令行输入,则点击属性管理器>右击代码 >属性>配置属性-调试>命令参数>右边小三角>编辑>写入参数后>应用
cap >> frame;//从视频对象中获取下一帧,Mat类型
,>>是一个stream operater , 流处理操作符
cv::MAT
用于表示图像或任意维度稠密数组的类。cv::Mat 类包含两个主要部分:矩阵头(包含矩阵的尺寸、存储类型、存储位置等信息)和一个指向存储数据的矩阵的指针。这种设计允许高效地复制矩阵头而不是复制整个矩阵数据,从而实现轻量级的对象传递。
- 数据类型:CV_8U(8位无符号整数,常用于灰度图像)、CV_32F(32位浮点数,常用于图像处理中的浮点运算)
- ROI支持
- 连续性:如果矩阵在内存中是连续的,cv::Mat 会存储一个标志来表示这一点,从而可以更快地遍历数据。
- 通道:对于彩色图像,cv::Mat 通常包含多个通道(如 RGB 或 BGR),可以通过拆分或合并通道来操作单个颜色通道。
- cv::imread
image = cv::imread(argv[1], cv::IMREAD_COLOR); // 读取彩色图像
- 传入地址
- 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);
- image 原图Mat
- resizedImage 目标Mat
- 目标宽高像素
-
- fx , fy 比例因子与3. 不共用,若有3. 则自动忽略
cv::INTER_LINEAR
是一种双线性插值方法,它考虑了源图像中 2x2 像素邻域的值来计算输出图像中的每个像素值。
- 命令行给主函数传递其他类型参数
思路:传递的还是char*(string)字符串,但是可以用转换函数解释为其他类型,如:
std::atof()
- std::atof 函数将字符串转换为浮点数。
- 请注意,std::atof 在遇到无法解析为数字的字符时会停止解析,并返回已经解析的部分。
- 如果字符串无法转换为有效的浮点数,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_log
的std::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++?
- 可移植性(Win & Linux)
python代码在每次运行时由解释器转换成字节码(.pyc)再转成机器语言,这个过程只要使用同一种解释器,就可以轻松地保证代码的通用性。不过c++也不差,只不过略逊一筹。 - 开发工具
python的开发工具大多有跨平台特性,而C++的一些工具则在Linux上面临是否适配的问题。例如Linux上貌似没有VS,只有VScode(as far as I know) - 代码开发难度
python这种动态类型语言在难度上大多时候显著低于c++。并且可读性也更强。上手也更快。 - 性能
python generally需要更长的运行时间,但是如果大量地采用OpenCV这种高级库函数开发,其实C++并没有优势,这里主要取决于你用的库函数的特性,如果对性能敏感,建议实际上板跑一段代码试一试,其他方法并不靠谱。 - Last But Definitely Not Least
取决于你的Co-Workers习惯于什么代码,可能轮不到你选呢哈哈。所以说把用各种语言开发的经历都当作一次机会吧,不要总觉得没用到自己觉得最方便的语言就太过沮丧,总不能说换个代码一下子就寸步难行吧。
tips:注意平台的路径分隔符:Windows 上的 \和Linux上的/(话说csdn的MD好多bug啊,\这个符号就是不能加任何类型强调,/就可以,奇怪)
chrono库
auto start_time = std::chrono::high_resolution_clock::now();
- high_resolution_clock是一个类,表示高精度时钟
now()
是一个静态成员函数,获取当前时间点,一个time_point
对象(从某个时刻开始 , it depends )auto
: 这是一个C++11引入的关键字,此处被推断为time_point
cv::cvtColor
色彩变换函数
cv::cvtColor(frame, gray_frame, cv::COLOR_BGR2GRAY);
- 待处理Mat
- 目标Mat
- RGB/BGR to GRAY
cv::Laplacian
拉普拉斯变换是一种二阶导数滤波器,可以用来增强图像中的边缘和其他高频部分
cv::Laplacian(gray_frame, laplacian, CV_16S, 3, 1, 0, cv::BORDER_DEFAULT);
gray_frame
是输入图像(src
)。laplacian
是输出图像(dst
),它将包含拉普拉斯变换的结果。CV_16S
是输出图像的深度(ddepth
),表明结果应存储为16位有符号整数。因为拉普拉斯变换的结果可能有负数。下面还会对这个负数进行处理。- 3是孔径大小(
ksize
),必须为正奇数。 - 1是缩放因子(
scale
),可对结果进行缩放。 - 0是增量(
delta
),可以加到所有的结果上去,相当于一个偏置,一般不用。 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就不太行