创作不易,方便的话点点关注,谢谢
本文是经过严格查阅相关权威文献和资料,形成的专业的可靠的内容。全文数据都有据可依,可回溯。特别申明:数据和资料已获得授权。本文内容,不涉及任何偏颇观点,用中立态度客观事实描述事情本身。
文章结尾有最新热度的文章,感兴趣的可以去看看。
文章有点长(5494字阅读时长:12分),期望您能坚持看完,并有所收获
现在,我们先来回答这个问题:为什么我们要关心在Rust中运行OpenCV这件事呢?
为什么不直接使用C++、Java或者Python呢?
C++可是老牌“冠军”了但与Rust或Go相比,编译C++代码可没那么轻松愉快。对于伴随Python长大的年轻一代来说,在C++中安装包感觉就像身处在上个世纪一样(很麻烦),对吧?谁愿意花时间去安装包呀?尤其是如今有那么多好用又强大的编程语言可选的时候。而且Rust的包管理器Cargo非常出色。
在Python中使用OpenCV很容易,安装简单,使用方便,还有庞大的社区支持。如果你真想把事情搞定,Python是个不错的选择。尽管Python语言出了名的运行速度慢,但实际上真正的Python代码量很少(我们大多数人都知道这一点)。要是你只是想添加一点简单的功能呢?Python也能做到,只是做得不太好——我说的就是这种情况。
Rust中的OpenCV
入门——安装(MacOS系统)
Linux用户通常,能搞清楚如何在自己的机器上安装OpenCV,不然的话可以参考这里的指南,Windows用户可以参考这份指南。对于Mac用户,只需遵循下面这个超简短的教程就行。
我们先从安装OpenCV开始。遗憾的是,OpenCV可不是普通的Rust包,它需要你先在电脑上安装OpenCV(C++版本)。不过在Rust中,不需要进行繁琐的链接以及编写CMake文件。
在我看来,在Rust中使用OpenCV实际上比在C++中使用更容易,而且当你要引入很多依赖项时,也不会让你头疼(一想到那些庞大复杂的CMake文件就头疼)。在macOS系统中安装它非常便利。若已成功安装Homebrew,仅需运行下述命令即可:brew install opencv
在你的“cargo.toml”文件里接着添加上述内容:
[dependencies]
opencv = "0.63.0" # or whatever version is the latest
你可以参考opencv-rust仓库来获取完整的安装帮助。我安装的时候,在编译方面遇到了一个问题,但按照“故障排除”部分的内容就能轻松解决。所以如果你遇到问题,在抓狂之前一定要先查看一下那个部分的内容。
这个OpenCV的Rust绑定是绑定到C++API上的(这挺好的,因为C语言的API基本上已经被弃用了)。由于Rust可以直接与C语言交互,所以C++部分会被包裹在一个额外的C层中,接下来再暴露给Rust使用。
简单代码示例
我的第一个示例。对于有经验的OpenCV用户来说,代码相当直观易懂。
use anyhow::Result;// Automatically handle the error types
use opencv::{
prelude::*,
videoio,
highgui
};// Note, the namespace of OpenCV is changed (to better or worse). It is no longer one enormous.
fnmain()->Result<()>{// Note, this is anyhow::Result
// Open a GUI window
highgui::named_window("window", highgui::WINDOW_FULLSCREEN)?;
// Open the web-camera (assuming you have one)
letmut cam= videoio::VideoCapture::new(0, videoio::CAP_ANY)?;
letmut frame=Mat::default();// This array will store the web-cam data
// Read the camera
// and display in the window
loop{
cam.read(&mut frame)?;
highgui::imshow("window",&frame)?;
letkey= highgui::wait_key(1)?;
if key ==113{// quit with q
break;
}
}
Ok(())
}
我们可以打开一个网络摄像头,并将获取到的帧数据存储到frame变量中。这段代码应该很容易理解。
附言:以下是与之等价的Python代码:
import cv2
vid = cv2.VideoCapture(0)
while True:
ret, frame = vid.read()
cv2.imshow('window', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
vid.release()
cv2.destroyAllWindows()
深入了解OpenCV-Rust绑定
我们来进行以下操作:
-从文件中读取图像-使用SIFT(尺度不变特征变换)和ORB(方向梯度直方图特征检测算法)检测关键点
-用不同颜色绘制关键点
-绘制一个矩形-将图像转换为ndarray(快速转换)
-将ndarray转换为image::RgbImage(用于检验我们上面的步骤是否按预期工作)
-保存图像
use anyhow::anyhow;
use anyhow::Result;
use image::RgbImage;
use ndarray::{Array1,ArrayView1,ArrayView3};
use opencv::{selfas cv, prelude::*};
fnmain()->Result<()>{
// Read image
letimg= cv::imgcodecs::imread("./assets/demo_img.png", cv::imgcodecs::IMREAD_COLOR)?;
// Use Orb
letmut orb=<dyn cv::features2d::ORB>::create(
500,
1.2,
8,
31,
0,
2,
cv::features2d::ORB_ScoreType::HARRIS_SCORE,
31,
20,
)?;
letmut orb_keypoints= cv::core::Vector::default();
letmut orb_desc= cv::core::Mat::default();
letmut dst_img= cv::core::Mat::default();
letmask= cv::core::Mat::default();
orb.detect_and_compute(&img,&mask,&mut orb_keypoints,&mut orb_desc,false)?;
cv::features2d::draw_keypoints(
&img,
&orb_keypoints,
&mut dst_img,
cv::core::VecN([0.,255.,0.,255.]),
cv::features2d::DrawMatchesFlags::DEFAULT,
)?;
cv::imgproc::rectangle(
&mut dst_img,
cv::core::Rect::from_points(cv::core::Point::new(0,0), cv::core::Point::new(50,50)),
cv::core::VecN([255.,0.,0.,0.]),
-1,
cv::imgproc::LINE_8,
0,
)?;
// Use SIFT
letmut sift= cv::features2d::SIFT::create(0,3,0.04,10.,1.6)?;
letmut sift_keypoints= cv::core::Vector::default();
letmut sift_desc= cv::core::Mat::default();
sift.detect_and_compute(&img,&mask,&mut sift_keypoints,&mut sift_desc,false)?;
cv::features2d::draw_keypoints(
&dst_img.clone(),
&sift_keypoints,
&mut dst_img,
cv::core::VecN([0.,0.,255.,255.]),
cv::features2d::DrawMatchesFlags::DEFAULT,
)?;
// Write image using OpenCV
cv::imgcodecs::imwrite("./tmp.png",&dst_img,&cv::core::Vector::default())?;
// Convert :: cv::core::Mat -> ndarray::ArrayView3
leta= dst_img.try_as_array()?;
// Convert :: ndarray::ArrayView3 -> RgbImage
// Note, this require copy as RgbImage will own the data
lettest_image=array_to_image(a);
// Note, the colors will be swapped (BGR <-> RGB)
// Will need to swap the channels before
// converting to RGBImage
// But since this is only a demo that
// it indeed works to convert cv::core::Mat -> ndarray::ArrayView3
// I'll let it be
test_image.save("out.png")?;
Ok(())
}
traitAsArray{
fntry_as_array(&self)->Result<ArrayView3<u8>>;
}
implAsArrayforcv::core::Mat{
fntry_as_array(&self)->Result<ArrayView3<u8>>{
if!self.is_continuous(){
returnErr(anyhow!("Mat is not continuous"));
}
letbytes=self.data_bytes()?;
letsize=self.size()?;
leta=ArrayView3::from_shape((size.height asusize, size.width asusize,3), bytes)?;
Ok(a)
}
}
// From Stack Overflow: https://stackoverflow.com/questions/56762026/how-to-save-ndarray-in-rust-as-image
fnarray_to_image(arr:ArrayView3<u8>)->RgbImage{
assert!(arr.is_standard_layout());
let(height, width, _)= arr.dim();
letraw= arr.to_slice().expect("Failed to extract slice from array");
RgbImage::from_raw(width asu32, height asu32, raw.to_vec())
.expect("container should have the right size for the image dimensions")
}
下面我先把代码整体呈现出来,接下来再逐步进行讲解。
读取图像:
读取图像的操作相当直接明了。你可能需要添加一些检查,确保图像能成功加载。OpenCV在找不到图像时并不会抛出错误。而且别被Rust中的Result类型误导了,它并不会检查图像是否正确加载了。
Rust代码:
// Read image
let img = opencv::imgcodecs::imread("./assets/demo_img.png", cv::imgcodecs::IMREAD_COLOR)?;
C++代码
cv::Mat I = cv::imread("./assets/demo_img.png", 0);
Python代码:
img: np.ndarray = cv2.imread("./assets/demo_img.png)
关键点检测与绘制:
ORB的代码与SIFT的代码极为相像,故而我仅针对ORB这一部分展开注释说明。首先创建探测器:
Rust代码:
let mut orb=<dyn cv::features2d::ORB>::create(
500,
1.2,
8,
31,
0,
2,
cv::features2d::ORB_ScoreType::HARRIS_SCORE,
31,
20,
)?;
C++代码
cv::Ptr<cv::ORB> orbPtr = cv::ORB::create();
这与C++代码极为相似,只是命名空间存在些许差异,并且需要提供参数。需注意所有的默认变量均可在Rust的文档里寻得,仅需将鼠标悬浮停留在“create”函数之上,便能在VSCode中查看到相关文档。C++的默认参数在VSCode中也能查看。如果使用的是其他集成开发环境(IDE),大概可以通过查看函数定义并阅读文档字符串来获取。
计算关键点:
再次对比Rust和C++代码
Rust代码:
let mut orb_keypoints = cv::core::Vector::default();
let mut orb_desc = cv::core::Mat::default();
orb.detect_and_compute(&img, &mask, &mut orb_keypoints, &mut orb_desc, false)?;
C++代码
std::vector<cv::KeyPoint> keypoints;
orbPtr->detect(image, keypoints);
cv::Mat desc;
orbPtr->compute( image, keypoints, desc );
同样这里的差异也不是特别大。刚开始可能不太容易知道如何初始化关键点和描述符,但一旦明白了,就会觉得很简单。从上面的代码来看,不能说Rust代码就比C++代码更复杂。
绘制关键点:
Rust代码:
let mut dst_img= cv::core::Mat::default();
cv::features2d::draw_keypoints(
&img,
&orb_keypoints,
&mut dst_img,
cv::core::VecN([0.,255.,0.,255.]),
cv::features2d::DrawMatchesFlags::DEFAULT,
)?;
C++代码
cv::Mat dst_img;
cv::drawKeypoints(image, keypoints, dst_img);
在Rust中弄清楚类型有点棘手,利用类型推断并凭借一点还是有可能搞清楚的。
探索工作——绘制矩形(更具引导性的步骤):
由于OpenCV的Rust绑定几乎没有文档,这就有点像摸着石头过河一样。我之所以展示C++代码是有原因的,我们可以看到,大多数Rust代码在一定程度上可以从C++代码推导出来(借助如今强大的集成开发环境,我们还是有机会做到的。除此之外,Rust有一个不错的文档系统。我的策略是依靠opencv-rust的文档以及参考C++中的命名方式。下面我们就用这个策略来弄清楚如何绘制矩形。
首先我们明确要做的事,也就是绘制矩形,(在C++里是cv::rectangle函数)。接下来我们去查看opencv-rust的文档。
现在我们要做的就是弄清楚类型(说起来容易做起来难)。在这里我们要利用集成开发环境(我用的是VSCode),使用Neovim或者Emacs应该也没问题。第一个参数应该很明显,就是图像,它的类型应该是cv::core::Mat很明显对吧?不过,我们可以凭借直觉推断出来,Mat是存储图像数据的默认类型,所以应该就是Mat类型,而且Mat实现了ToInputOutputArray这个特征,所以没问题接下来Rect类型是什么呢?
看起来我们可以在core模块中找到它,这挺好的。但然后我该如何构建一个Rect呢?
好的那我们来看看from_points构造函数是怎么回事。
我们需要两个点!……但Point又是什么呢好像在core模块里!
所以new”看起来挺靠谱的,“fromvec2”也不错!
嗯……我们大概用哪个都行,不过我们就选“new”吧(通常找“new”或者“from”相关的就行)。
好的我们给它传入两个整数看看会发生什么(编译器应该不会报错)!这样我们就弄清楚了第二个参数!(其余的部分也用同样的方法来处理这有点繁琐,但一旦你开始熟悉这些类型了,事情就会变得容易些到那个时候。
cv::core::Rect::from_points(
cv::core::Point::new(0, 0),
cv::core::Point::new(50, 50
)
转换为ndarray:
在Rust中,ndarray似乎是最适配的矩阵库(对于那些想要为Python的NumPy包编写绑定的人来说,ndarray是个不错的选择)。以后我会再写一篇关于将Rust与Python和NumPy绑定的文章!这一步着实有些棘手,如今我们需要将一个C++类型转换为Rust类型。
我们清楚,需要处理的是矩阵这类形式,而且它们一般是按照行优先的顺序来存储的。OpenCV的Mat以及ndarray的Array(View)皆为行优先存储,并且数据通常会依序存储于底层缓冲区之中。为了确保在我们这个案例当中亦是如此,我们需要检查Mat是否是连续的。
if !mat.is_continuous() {
return Err(anyhow!("Mat is not continuous :("));
}
我们可以利用这个知识,快速(无复制零成本)地将cv::Mat转换为ndarray::Array。不过需要注意的是,Array将会指向存储在Mat中的数据,所以当Mat被释放时,Array也必须被释放,否则我们(很可能)会指向已经释放的内存,这可不好!但看起来Rust会帮我们处理好这个问题。首先我们要提取Mat的数据字节,由于图像是以8位无符号整数(u8)存储的所以我们可以直接读取数据,无需进行类型转换。
let data_bytes: &[u8] = mat.data_bytes()?; // <-- This is the image data in sequence! Note it is pointing to the data in mat
接下来,我们需要计算出该数据的大小。当我们获取数据字节时,我们并没有得到我们想要的形状,而是得到了一个长序列。
let size = mat.size()?;
let h:i32 = size.height;
let w:i32 = size.width;
现在我们可以根据这些信息构造一个ArrayView3。好的!我们得到了一种将 Mat 转换为具有性能的 ArrayView3 的方法。请注意,这仅适用于连续数组。
let a = ArrayView3::from_shape((h as usize, w as usize, 3), data_bytes)?; // The 3 is because we have bgr. For gray image this will be 1
作为一个特征,大概可以写成这样:
trait AsArray{
fntry_as_array(&self)->Result<ArrayView3<u8>>;
}
implAsArrayforcv::core::Mat{
fntry_as_array(&self)->Result<ArrayView3<u8>>{
if!self.is_continuous(){
returnErr(anyhow!("Mat is not continuous"));
}
letbytes=self.data_bytes()?;
letsize=self.size()?;
leta=ArrayView3::from_shape((size.height asusize, size.width asusize,3), bytes)?;
Ok(a)
}
}
为了能够迅速地把Mat转化为Array,我们当下可以如此进行调用:
let array: ArrayView<u8> = mat.try_as_array()?;
结论
在Rust中使用OpenCV是完全可行的,只是需要更深入的知识来实现不同类型之间的转换,还需要有足够的耐心去弄清楚这些绑定关系。但它确实是可行的!由于Rust的包管理器Cargo非常出色,它鼓励使用其他人开发的包(比如cv-convert`,它可以在很多流行的包之间进行图像类型转换),这让开发工作变得更轻松了。我希望未来能看到更多很棒的包出现。一些用于GPU的Rust包已经开始涌现了,谁知道呢,也许在未来,我们可以直接将Rust编译为SPIR-V格式以实现超快速的计算呢!那将会是多么美好的未来啊!如果你一直读到这儿,那谢谢你花时间读。希望你在阅读时能有收获。
以上就是我的分享。这些分析皆源自我的个人经验,希望上面分享的这些东西对大家有帮助,感谢大家!
点个“在看”不失联
最新热门文章推荐:
告别繁琐!Phi-3-Vision-128K人工智能OCR轻松搞定PDF解析
为什么说C和C++比其他语言更能培养优秀程序员?底层思维的重要性
用纯C++实现神经网络:不依赖Python和PyTorch,260行代码训练手写数字分类器准确率高达99%,你敢信?
中国人眼中的Yoshua Bengio:将人工智能安全理念带入现实应用并影响全球政策制定?
为何开发者:正在抛弃PostgreSQL、MySQL 和 MongoDB
马斯克等大佬质疑:OpenAI引领的人工智能发展道路,究竟是进步还是灾难的前奏?
国外程序员分享:C++在底层性能和实时线程处理方面碾压Rust
参考文献:《图片来源公共网络》
本文使用 文章同步助手 同步