谁才是Rust视频处理的最佳选择?ffmpeg-next vs opencv vs video-rs

699 阅读8分钟

图片

创作不易,方便的话点点关注,谢谢

本文是经过严格查阅相关权威文献和资料,形成的专业的可靠的内容。全文数据都有据可依,可回溯。特别申明:数据和资料已获得授权。本文内容,不涉及任何偏颇观点,用中立态度客观事实描述事情本身。

文章结尾有最新热度的文章,感兴趣的可以去看看。

文章有点长(3799字阅读时长:13分),期望您能坚持看完,并有所收获


图片

我想针对以下几个指标,对三个流行的Rust视频处理库(即ffmpeg-nextopencvvideo-rs)进行测试:易用性、易修改性以及速度。请注意,我在Rust方面还是个新手,每个实现过程中可能都存在一些“小问题”。

图片

本次任务是提取并保存视频前20秒的视频帧。需要注意的是,这算不上一个很好的速度测试,因为大部分处理过程都是输入输出(IO)操作。不过,它可以让我们对这些库有一定的了解。

计时测试是通过构建发布版本(执行cargo build -release命令),

然后运行time./target/release/binary-name来对优化后的代码进行准确评估的。

这些测试是在VSCode中进行的,其设置说明和Dockerfile可以在这里找到。为了便于阅读,我将所有的Cargo包都添加到了一个Cargo.toml文件中。

[package]
name ="opencv-testing"
version ="0.1.0"
edition ="2021"

[dependencies]
ffmpeg-next="7.0.2"
image ="0.25.1"
ndarray ="0.15.6"
opencv ={ version ="0.92.0","features"=["clang-runtime"]}
video-rs ={ version ="0.8", features =["ndarray"] }

OpenCV

在对Rust版的OpenCV进行测试后,我发现它比Python版的要慢,这挺让人意外的。Python的执行时间是13 - 16秒,而Rust的执行时间是32秒!

起初,我以为可能是编译优化方面的问题,所以我从源代码编译了OpenCV。然而,这并没有很明显的节省时间,我也不确定问题出在哪里。为了内容完整,我把修改过程添加如下。

RUN cd /opt && \
    git clone https://github.com/opencv/opencv.git && \
cd opencv && \
    git checkout ${OPENCV_VERSION}&& \
mkdir build && \
cd build && \
    cmake -D CMAKE_BUILD_TYPE=RELEASE \
-D CMAKE_INSTALL_PREFIX=/usr/local \
-D WITH_TBB=ON \
-D WITH_OPENMP=ON \
-D ENABLE_FAST_MATH=ON \
-D BUILD_EXAMPLES=OFF \
-D WITH_IPP=ON \
-D WITH_CUDA=OFF \
-D BUILD_opencv_python2=OFF \
-D BUILD_opencv_python3=ON \
-D WITH_FFMPEG=ON \
-D WITH_GSTREAMER=ON \
-D WITH_V4L=ON \
-D WITH_QT=OFF \
-D WITH_OPENGL=ON \
-D OPENCV_GENERATE_PKGCONFIG=ON \
-D OPENCV_ENABLE_NONFREE=ON \
..&& \
    make -j$(nproc)&& \
    make install && \
    ldconfig

不过,如果我使用Tokio并行化输入输出文件保存操作,执行时间就会比Python实现的更快。欢迎大家对此发表见解!

以下是使用OpenCV库提取视频帧并保存的代码:

use std::fs::create_dir_all;
use std::path::Path;
use std::time::Instant;
use opencv::{imgcodecs, prelude::*, videoio,Result};
use opencv::prelude::Mat;
use tokio::task;

#[tokio::main]
asyncfnmain()->Result<()>{
letwindow="video capture";

letvideo_url="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
letmut cam= videoio::VideoCapture::from_file(video_url, videoio::CAP_ANY)?;// 0 is the default camera
letopened= videoio::VideoCapture::is_opened(&cam)?;
if!opened {
panic!("Unable to open default camera!");
}

letoutput_folder="frames_opencv";
create_dir_all(output_folder).expect("failed to create output directory");

letframe_rate= cam.get(videoio::CAP_PROP_FPS)?;// Get the frame rate of the video
letmax_duration=20.0;// Max duration in seconds
letmax_frames=(frame_rate * max_duration).ceil()asusize;

letmut frame_count=0;
letmut tasks=vec![];

while frame_count < max_frames {
letmut frame=Mat::default();
        cam.read(&mut frame)?;
if frame.size()?.width >0{
letframe_path=format!("{}/frame_{:05}.png", output_folder, frame_count);
letframe_clone= frame.clone();// Clone the frame to move into the async block

// Spawn a blocking task to save the frame
lettask= task::spawn_blocking(move||{
                imgcodecs::imwrite(&frame_path,&frame_clone,&opencv::types::VectorOfi32::new()).expect("failed to save frame");
});
            tasks.push(task);

            frame_count +=1;
}

}

// Await all tasks to finish
fortaskin tasks {
        task.await.expect("task failed");
}

println!("Saved {} frames in the '{}' directory", frame_count, output_folder);
Ok(())
}

经过这样的修改后,执行时间为6秒。

Video-RS

video-rs是一个用于Rust的通用视频库,它使用了来自FFMPEG的libav系列库。它旨在为许多常见的视频任务(如读取、写入、复用、编码和解码)提供一个稳定且符合Rust风格的接口。

为了让对比相对合理(尽管它们各有特点,就好比不同的水果一样),这里同样会使用Tokio来进行并行输入输出操作。

以下是使用video-rs库提取视频帧并保存的代码:

use std::fs::create_dir_all;
use std::path::Path;
use std::error::Error;
use video_rs::decode::Decoder;
use video_rs::Url;
use image::{ImageBuffer,Rgb};
use tokio::task;

#[tokio::main]
asyncfnmain()->Result<(),Box<dynError>>{
    video_rs::init().unwrap();

letsource=
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
.parse::<Url>()
.unwrap();
letmut decoder=Decoder::new(source).expect("failed to create decoder");

letoutput_folder="frames_video_rs";
create_dir_all(output_folder).expect("failed to create output directory");

let(width, height)= decoder.size();
letframe_rate= decoder.frame_rate();// Assuming 30 FPS if not available

letmax_duration=20.0;// Max duration in seconds
letmax_frames=(frame_rate * max_duration).ceil()asusize;

letmut frame_count=0;
letmut elapsed_time=0.0;
letmut tasks=vec![];

forframein decoder.decode_iter(){
ifletOk((_timestamp, frame))= frame {
if elapsed_time > max_duration {
break;
}

letrgb= frame.slice(ndarray::s![..,..,0..3]).to_slice().unwrap();

letimg:ImageBuffer<Rgb<u8>,Vec<u8>>=ImageBuffer::from_raw(width, height, rgb.to_vec())
.expect("failed to create image buffer");

letframe_path=format!("{}/frame_{:05}.png", output_folder, frame_count);

lettask= task::spawn_blocking(move||{
                img.save(&frame_path).expect("failed to save frame");
});

            tasks.push(task);

            frame_count +=1;
            elapsed_time +=1.0/ frame_rate;
}else{
break;
}
}

// Await all tasks to finish
fortaskin tasks {
        task.await.expect("task failed");
}

println!("Saved {} frames in the '{}' directory", frame_count, output_folder);
Ok(())
}

这段代码看起来相当直观,也就是从解码器获取帧,将帧切片为一个ndarray数组,然后使用图像库保存图像。执行时间为2 - 4秒!这确实是一种改进!

FFMPEG-Next

好吧,我甚至都不太确定该从哪里开始对这个库进行并行化处理。它的代码量明显比另外两个库多得多。我试着像前面两个示例那样使用spawn_blocking方法,但没能成功实现。

以下是使用ffmpeg-next库提取视频帧并保存的代码:

use ffmpeg::format::{input,Pixel};
use ffmpeg::media::Type;
use ffmpeg::software::scaling::{context::Context, flag::Flags};
use ffmpeg::util::frame::video::Video;
use std::fs::{self,File};
use std::path::Path;

fnmain()->Result<(),Box<dyn std::error::Error>>{
    ffmpeg::init().unwrap();

letsource_url="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
letframes_folder="frames_ffmpeg";

// Create the frames folder if it does not exist
if!Path::new(frames_folder).exists(){
        fs::create_dir(frames_folder)?;
}

// Open the input video file
letmut ictx=input(&source_url)?;

// Find the best video stream
letinput_stream= ictx
.streams()
.best(Type::Video)
.ok_or(ffmpeg::Error::StreamNotFound)?;

letframerate_q= input_stream.avg_frame_rate();
letframerate_f64= framerate_q.numerator()asf64/ framerate_q.denominator()asf64;
letmax_duration=20.0;// Max duration in seconds
letmax_frames=(framerate_f64 * max_duration).ceil()asusize;

// Get the index of the video stream
letinput_video_stream_index= input_stream.index();

// Set up the video decoder
letcontext_decoder= ffmpeg::codec::context::Context::from_parameters(input_stream.parameters())?;
letmut video_decoder= context_decoder.decoder().video()?;

// Set up the scaler to convert frames to RGB24
letmut scaler=Context::get(
        video_decoder.format(),
        video_decoder.width(),
        video_decoder.height(),
Pixel::RGB24,
        video_decoder.width(),
        video_decoder.height(),
Flags::BILINEAR,
)?;

letmut frame_index=0;

// Function to receive and process decoded frames
letmut receive_and_process_decoded_frames=|decoder:&mut ffmpeg::decoder::Video, frame_index:&mutusize|->Result<(), ffmpeg::Error>{
letmut decoded=Video::empty();
while decoder.receive_frame(&mut decoded).is_ok(){
letmut rgb_frame=Video::empty();
            scaler.run(&decoded,&mut rgb_frame)?;
save_file(&rgb_frame,*frame_index, frames_folder).unwrap();
*frame_index +=1;
}
Ok(())
};

// Process packets from the input video stream
for(stream, packet)in ictx.packets(){
if frame_index > max_frames {
break;
}
if stream.index()== input_video_stream_index {
            video_decoder.send_packet(&packet)?;
receive_and_process_decoded_frames(&mut video_decoder,&mut frame_index)?;
}
}

// Finalize decoding
    video_decoder.send_eof()?;
receive_and_process_decoded_frames(&mut video_decoder,&mut frame_index)?;

Ok(())
}

// Function to save a frame as a PNG file
fnsave_file(frame:&Video, index:usize, folder:&str)-> image::ImageResult<()>{
letfilename=format!("{}/frame{}.png", folder, index);
letpath=Path::new(&filename);
letmut _file=File::create(path)?;

let(width, height)=(frame.width(), frame.height());
letbuffer= frame.data(0);

// Create an ImageBuffer from the raw frame data
letimg_buffer= image::ImageBuffer::from_raw(width, height, buffer.to_vec()).unwrap();
letimg= image::DynamicImage::ImageRgb8(img_buffer);
    img.save(&filename)?;

Ok(())
}

那我试着尽力弄清楚这里面的情况。这里有需要循环处理的数据包,还有一个用于解码帧并保存它的闭包。我可能需要也可能不需要这个缩放器。在未进行并行化处理时,执行时间是10秒。这里可能还有很大的优化空间。

结论

总体而言,我认为对于视频处理来说,video-rs是我的首选。它最容易上手运行,语法看起来最简洁,而且在本次任务中速度也是最快的。遗憾的是,我觉得FFMPEG-Next不是一个好的选择,除非你想深入研究后端视频处理的细节内容。OpenCV虽然由于某些原因速度有点慢,但对于复杂任务来说可能是个更好的选项。由于上面的示例大多是输入输出操作,所以在某些任务(比如计算机视觉相关任务)中,Rust版的OpenCV仍有可能比Python版的OpenCV快得多。

以上就是我的分享。这些分析皆源自我的个人经验,希望上面分享的这些东西对大家有帮助,感谢大家!

图片

点个“在看”不失联

最新热门文章推荐:

惊!Go语言中的指针竟能让代码性能翻倍,你还不赶紧学?

告别繁琐!Phi-3-Vision-128K人工智能OCR轻松搞定PDF解析

Mojo比Python快35000倍?让我们来测试一下

告别C/C++ ?看看这15种替代语言如何征服开发者的心?

为什么说C和C++比其他语言更能培养优秀程序员?底层思维的重要性

必学!你的第一个C++ WebSocket客户端程序

必看:CTC训练神经网络中的波束搜索解码

C语言程序:从头开始编写一个实时嵌入式操作系统的内核

使用 TensorFlow 构建手写文本识别系统

深度学习中的池化层:减少计算量的背后是牺牲了分类准确性吗?

何恺明团队挑战CNN霸权,ViT能否颠覆目标检测传统?

用纯C++实现神经网络:不依赖Python和PyTorch,260行代码训练手写数字分类器准确率高达99%,你敢信?

我从VSCode转向Cursor的原因

监控你的Linux系统只需一个脚本!

干净简洁的实现Go项目结构 | GitHub 4.8k 星

为什么开发人员讨厌PHP?(世界上最好的语言)

中国人眼中的Yoshua Bengio:将人工智能安全理念带入现实应用并影响全球政策制定?

为何开发者:正在抛弃PostgreSQL、MySQL 和 MongoDB

马斯克等大佬质疑:OpenAI引领的人工智能发展道路,究竟是进步还是灾难的前奏?

国外程序员分享:C++在底层性能和实时线程处理方面碾压Rust

参考文献:《图片来源公共网络》

本文使用 文章同步助手 同步