一、环境配置:VS2022、cuda12.4、basler库、opencv3.4.16库、ffmpeg7.0.1库
经过多次测试,实现了相机采集实时压缩视频,代码应该是比较精简的,可用于工程中。
其中cuda12.4、opencv3.4.16、ffmpeg7.0.1大家可以自己找一下教程配置一下。
相机的话我用的是basler相机测试的,大家在相机采集这一块,可以根据自己的相机型号进行修改,相机配好后,上面那些库也配置一下,就可以了。真的想用的话就自己好好配一下相机库跟相机采集代码的一些东西,只要相机配好了,其他应该是没问题的。
二、这个代码我已经多次测试,测试情况如下:
该demo实现了从内存图片数据到视频的压缩方法,采用了ffmpeg的接口。
1、加装cuda前:测试30fps,3000张,压缩的视频大小为42.7M,视频时长99秒,程序运行时长131秒。
2、加装cuda后:
测试内容:相机采集图像,将图像实时压缩并合成视频。
测试相机:basler,500万像素。测试30fps,采集3000张图片,压缩的视频大小为34.8M,视频时长99秒,程序运行时长99秒,已达到实时压缩视频。
3、运行期间CPU,GPU情况:
cpu:i7-14700KF,占用率7%~10%,平均8%左右。
gpu:RTX4070,占用率11%~16%,平均13%左右。
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <libavutil/opt.h>
#include <libswscale/swscale.h>
}
#include "CPylonBasler.h"
#include <string>
#include <time.h>
#include <functional>
#include <opencv2/opencv.hpp>
#include <iomanip>
#include <cstdlib>
#include <iostream>
#include <vector>
#include <nvjpeg.h>
#include <chrono>
#include <mutex>
#include <thread>
#include <condition_variable>
#include <atomic>
#include <queue>
#include <cuda_runtime.h>
#include <pylon/PylonIncludes.h>
#include <device_launch_parameters.h>
//本程序经过测试,已达到实时将图像采集并合成压缩视频的效果,运用了cuda12.4,ffmpeg进行加速。
std::mutex frame_mutex;
std::condition_variable frame_cond;
std::atomic<bool> capturing(true);
std::queue<cv::Mat> frame_queue;
void initialize_ffmpeg() {
avformat_network_init(); // 初始化网络库,如果不需要网络流,可以省略此行
}
AVFormatContext* create_format_context(const char* filename) {
AVFormatContext* format_context = nullptr;
avformat_alloc_output_context2(&format_context, nullptr, nullptr, filename);
if (!format_context) {
std::cerr << "无法分配格式上下文" << std::endl;
return nullptr;
}
return format_context;
}
AVStream* create_video_stream(AVFormatContext* format_context, AVCodecID codec_id, int width, int height, int fps, AVCodecContext** codec_context) {
const AVCodec* codec = avcodec_find_encoder(codec_id);
if (!codec) {
std::cerr << "找不到编码器" << std::endl;
return nullptr;
}
AVStream* stream = avformat_new_stream(format_context, codec);
if (!stream) {
std::cerr << "无法创建视频流" << std::endl;
return nullptr;
}
*codec_context = avcodec_alloc_context3(codec);
if (!*codec_context) {
std::cerr << "无法分配编码器上下文" << std::endl;
return nullptr;
}
(*codec_context)->codec_id = codec_id;
(*codec_context)->codec_type = AVMEDIA_TYPE_VIDEO;
(*codec_context)->width = width;
(*codec_context)->height = height;
(*codec_context)->time_base = { 1, fps };
(*codec_context)->pix_fmt = AV_PIX_FMT_YUV420P;
(*codec_context)->gop_size = 10;
(*codec_context)->max_b_frames = 1;
if (format_context->oformat->flags & AVFMT_GLOBALHEADER)
(*codec_context)->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
if (avcodec_open2(*codec_context, codec, nullptr) < 0) {
std::cerr << "无法打开编码器" << std::endl;
return nullptr;
}
stream->codecpar->codec_id = codec_id;
stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
stream->codecpar->width = width;
stream->codecpar->height = height;
stream->codecpar->format = (*codec_context)->pix_fmt;
if (avcodec_parameters_from_context(stream->codecpar, *codec_context) < 0) {
std::cerr << "无法从编码器上下文拷贝参数到视频流" << std::endl;
return nullptr;
}
return stream;
}
void write_frame(AVFormatContext* format_context, AVStream* stream, AVCodecContext* codec_context, AVFrame* frame, int frame_index) {
frame->pts = frame_index;
AVPacket* packet = av_packet_alloc();
if (!packet) {
std::cerr << "无法分配AVPacket" << std::endl;
return;
}
int ret = avcodec_send_frame(codec_context, frame);
if (ret < 0) {
std::cerr << "发送帧时出错: " << ret << std::endl;
av_packet_free(&packet);
return;
}
while (ret >= 0) {
ret = avcodec_receive_packet(codec_context, packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
}
else if (ret < 0) {
std::cerr << "编码帧时出错: " << ret << std::endl;
break;
}
packet->stream_index = stream->index;
packet->pts = av_rescale_q(packet->pts, codec_context->time_base, stream->time_base);
packet->dts = av_rescale_q(packet->dts, codec_context->time_base, stream->time_base);
packet->duration = av_rescale_q(packet->duration, codec_context->time_base, stream->time_base);
ret = av_interleaved_write_frame(format_context, packet);
if (ret < 0) {
std::cerr << "写入帧时出错: " << ret << std::endl;
break;
}
av_packet_unref(packet);
}
av_packet_free(&packet);
}
//cuda的核函数,CUDA内核将灰度图像转换为YUV420P格式
__global__ void gray_to_yuv420p_kernel(const unsigned char* gray_img, unsigned char* y_plane, unsigned char* u_plane, unsigned char* v_plane, int width, int height) {
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
if (x < width && y < height) {
int gray_index = y * width + x;
int y_index = y * width + x;
y_plane[y_index] = gray_img[gray_index];
if (x % 2 == 0 && y % 2 == 0) {
int uv_index = (y / 2) * (width / 2) + (x / 2);
u_plane[uv_index] = 128;
v_plane[uv_index] = 128;
}
}
}
void capture_frames(CPylonBasler& camera, int width, int height, int fps, int num_frames) {
auto start_time = std::chrono::steady_clock::now();
for (int i = 0; i < num_frames && capturing; ++i) {
camera.SofwareTrigger();
camera.SaveCapturedImage(); // 将相机采集的图像数据保存到m_pImgBuffer中
cv::Mat gray_img(cv::Size(width, height), CV_8UC1, camera.m_pImgBuffer);
{
std::lock_guard<std::mutex> lock(frame_mutex);
frame_queue.push(gray_img.clone());
}
frame_cond.notify_one();
auto elapsed_time = std::chrono::steady_clock::now() - start_time;
auto sleep_time = std::chrono::milliseconds(1000 / fps) * (i + 1) - elapsed_time;
if (sleep_time > std::chrono::milliseconds(0)) {
std::this_thread::sleep_for(sleep_time);
}
}
capturing = false;
frame_cond.notify_one();
}
int main() {
// 记录程序开始时间
auto program_start_time = std::chrono::steady_clock::now();
const char* filename = "30.mp4";
const AVCodecID codec_id = AV_CODEC_ID_H264;
const int fps = 30;
const int num_frames = 1000;
const int width = 2448;// 根据实际相机输出分辨率调整
const int height = 2048;
// 初始化相机(根据实际相机库调整)
CPylonBasler camera;
camera.ConnectCamera();
camera.SetAllCamParams();
camera.SetCamTriggerMode(TRIGGER_SOFTWARE);
camera.baslerCamera.StartGrabbing();
// 初始化FFmpeg
initialize_ffmpeg();
// 创建输出格式上下文和视频流
AVCodecContext* codec_context = nullptr;
AVFormatContext* format_context = create_format_context(filename);
if (!format_context)
return -1;
AVStream* video_stream = create_video_stream(format_context, codec_id, width, height, fps, &codec_context);
if (!video_stream)
return -1;
// 如果需要,打开输出文件
if (!(format_context->oformat->flags & AVFMT_NOFILE)) {
if (avio_open(&format_context->pb, filename, AVIO_FLAG_WRITE) < 0) {
std::cerr << "无法打开输出文件" << std::endl;
return -1;
}
}
// 写入文件头部信息
avformat_write_header(format_context, nullptr);
// 分配YUV420P格式的AVFrame
AVFrame* frame = av_frame_alloc();
frame->format = AV_PIX_FMT_YUV420P; // 修改为YUV420P格式
frame->width = width;
frame->height = height;
av_image_alloc(frame->data, frame->linesize, width, height, AV_PIX_FMT_YUV420P, 16); // 最后一个参数,如果视频出现了条纹状可进行修改(1,2,4,8,16,32,64),目前16以下经过测试都可以。
// CUDA内存分配
unsigned char* d_gray_img, * d_y_plane, * d_u_plane, * d_v_plane;
size_t gray_img_size = width * height * sizeof(unsigned char);
size_t uv_plane_size = (width / 2) * (height / 2) * sizeof(unsigned char);
cudaMalloc(&d_gray_img, gray_img_size);
cudaMalloc(&d_y_plane, gray_img_size);
cudaMalloc(&d_u_plane, uv_plane_size);
cudaMalloc(&d_v_plane, uv_plane_size);
// 启动帧采集线程
std::thread capture_thread(capture_frames, std::ref(camera), width, height, fps, num_frames);
dim3 block_size(16, 16);
dim3 grid_size((width + block_size.x - 1) / block_size.x, (height + block_size.y - 1) / block_size.y);
// 主循环:从队列中获取帧并写入视频流
int frame_index = 0;
while (capturing || !frame_queue.empty()) {
std::unique_lock<std::mutex> lock(frame_mutex);
frame_cond.wait(lock, [] { return !frame_queue.empty() || !capturing; });
if (!frame_queue.empty()) {
cv::Mat gray_img = frame_queue.front();
frame_queue.pop();
lock.unlock();
// 将灰度图像数据复制到CUDA内存
cudaMemcpy(d_gray_img, gray_img.ptr<unsigned char>(), gray_img_size, cudaMemcpyHostToDevice);
// 调用CUDA kernel进行格式转换
gray_to_yuv420p_kernel <<<grid_size, block_size>>> (d_gray_img, d_y_plane, d_u_plane, d_v_plane, width, height);
cudaDeviceSynchronize();
// 将CUDA结果复制到AVFrame
cudaMemcpy(frame->data[0], d_y_plane, gray_img_size, cudaMemcpyDeviceToHost);
cudaMemcpy(frame->data[1], d_u_plane, uv_plane_size, cudaMemcpyDeviceToHost);
cudaMemcpy(frame->data[2], d_v_plane, uv_plane_size, cudaMemcpyDeviceToHost);
// 写入帧到视频流
write_frame(format_context, video_stream, codec_context, frame, frame_index++);
}
}
// 写入文件尾部信息
av_write_trailer(format_context);
if (!(format_context->oformat->flags & AVFMT_NOFILE)) {
avio_close(format_context->pb);
}
// 释放资源
avcodec_free_context(&codec_context);
avformat_free_context(format_context);
av_frame_free(&frame);
// 释放CUDA内存
cudaFree(d_gray_img);
cudaFree(d_y_plane);
cudaFree(d_u_plane);
cudaFree(d_v_plane);
capturing = false;
frame_cond.notify_one();
capture_thread.join();
// 记录程序结束时间并计算运行时长
auto program_end_time = std::chrono::steady_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::seconds>(program_end_time - program_start_time).count();
std::cout << "程序运行时长: " << duration << "秒" << std::endl;
return 0;
}