我们都知道为什么 Rust 如此出色。然而,与 C/C++ 等老巨头相比,它有点过于新颖和闪亮,我们经常需要在没有适当文档的情况下使用 C++ 绑定。
背景
现在,让我们首先回答这个问题,我们为什么要关心在 Rust 中运行 OpenCV?为什么不直接使用 C++、Java 或 Python?
C++ 是相当古老的冠军,与 Rust 或 Go 相比,编译 C++ 代码并不有趣。对于在 Python 中长大的年轻一代来说,用 C++ 安装包似乎很中世纪。
谁愿意花时间安装软件包?尤其是今天有这么多优秀和强大的人。Rust 的包管理器 Cargo 非常棒。
在 Python 中使用 OpenCV 很容易。易于安装,易于在庞大的社区中使用。如果你真的想把事情做好,Python 就是你要走的路。尽管 Python 语言速度非常慢,但实际上,很少的代码行是 Python 代码的一大特点。
如果你只是想做一些需要 for 循环的额外功能呢? 或者如果你想并行运行这些东西呢? Python可以做,只是不太好用。
Rust 的 OpenCV
对于 Mac 用户,你可以按照下面的超短教程进行操作。
让我们从安装 OpenCV 开始。不幸的是,OpenCV 不是另一个 Rust 包。它需要在你的计算机上安装 OpenCV (C++)。然而,在 Rust 中,不需要痛苦的链接和编写 CMake 文件。Rust 中的 OpenCV 实际上比使用 C++ 更容易,并且当你想要引入许多依赖项(大量 CMake 文件的 gulp)时不会让你头疼。
在 macOS 上安装它非常容易。假设你有 brew,那么只需要运行:
brew install opencv
然后在你的 cargo.toml 添加
[dependencies]
opencv = "0.63.0" # or whatever version is the latest
你可以按照 opencv-rust 存储库获取完整的安装帮助:
当安装它时,在编译时遇到了问题,但可以按照故障排除部分轻松修复。因此,如果你遇到问题,请确保在抓头发之前检查该部分。
这个 OpenCV Rust 绑定到 C++ API(这很好,因为 C 已经被废弃了)。
由于 Rust 可以直接与 C 接口,C++ 被包装在一个额外的 C 层中,然后暴露给 Rust。
简单代码
第一个示例将基于 Makeitnow 的视频教程:
对于有经验的 OpenCV 用户来说,这非常简单。
使用 anyhow 来处理结果:
所以将使用它而不是 opencv::Result。
让我们写代码吧!
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.
fn main() -> 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)
let mut cam = videoio::VideoCapture::new(0, videoio::CAP_ANY)?;
let mut 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)?;
let key = highgui::wait_key(1)?;
if key == 113 { // quit with q
break;
}
}
Ok(())
}
太棒了!我们可以打开一个网络摄像头并将生成的帧放入 frame 变量中。
代码应该是不言自明的。否则看视频!
PS:这将是等效的 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::{self as cv, prelude::*};
fn main() -> Result<()> {
// Read image
let img = cv::imgcodecs::imread("./assets/demo_img.png", cv::imgcodecs::IMREAD_COLOR)?;
// Use Orb
let mut orb = <dyn cv::features2d::ORB>::create(
500,
1.2,
8,
31,
0,
2,
cv::features2d::ORB_ScoreType::HARRIS_SCORE,
31,
20,
)?;
let mut orb_keypoints = cv::core::Vector::default();
let mut orb_desc = cv::core::Mat::default();
let mut dst_img = cv::core::Mat::default();
let mask = 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
let mut sift = cv::features2d::SIFT::create(0, 3, 0.04, 10., 1.6)?;
let mut sift_keypoints = cv::core::Vector::default();
let mut 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
let a = dst_img.try_as_array()?;
// Convert :: ndarray::ArrayView3 -> RgbImage
// Note, this require copy as RgbImage will own the data
let test_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(())
}
trait AsArray {
fn try_as_array(&self) -> Result<ArrayView3<u8>>;
}
impl AsArray for cv::core::Mat {
fn try_as_array(&self) -> Result<ArrayView3<u8>> {
if !self.is_continuous() {
return Err(anyhow!("Mat is not continuous"));
}
let bytes = self.data_bytes()?;
let size = self.size()?;
let a = ArrayView3::from_shape((size.height as usize, size.width as usize, 3), bytes)?;
Ok(a)
}
}
// From Stack Overflow: https://stackoverflow.com/questions/56762026/how-to-save-ndarray-in-rust-as-image
fn array_to_image(arr: ArrayView3<u8>) -> RgbImage {
assert!(arr.is_standard_layout());
let (height, width, _) = arr.dim();
let raw = arr.to_slice().expect("Failed to extract slice from array");
RgbImage::from_raw(width as u32, height as u32, 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 部分进行评论。所以首先创建检测器Rustlet 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 的文档中找到。只需将鼠标悬停在“创建”功能上,你就会看到文档 [VSCode]。
C++ 默认参数可以在 VSCode 中看到。如果使用另一个 IDE,你可能只需转到函数定义并阅读文档字符串。
计算关键点
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 中的哪些类型有点棘手。使用类型推断和一点直觉就可以找到它!
Detective Work - 绘制矩形(一个更有指导性的步骤)
由于 OpenCV Rust 绑定几乎没有文档,因此它是一种侦探游戏。我决定展示 C++ 代码是有原因的。
我们可以看到,大部分 Rust 代码都可以从 C++ 中推断出来(在某种程度上)。利用今天令人难以置信的 IDE,我们有机会。
此外,Rust 有一个很好的文档系统。策略是依靠opencv-rust 文档
和 C++ 的命名。所以让我们使用这个策略来弄清楚如何绘制矩形。
首先,我们想好要做什么,即:
· 绘制矩形(在 C++ 中是cv::rectangle)然后我们转到opencv-rust docs
现在,我们所要做的就是找出类型(说起来容易做起来难)。在这里,我们将使用 IDE(在我的例子中是 VSCode)。使用 LSP,Nvim 或 Emacs 也应该没问题。
第一个参数应该很明显,图像。那将是 cv::core::Mat。我们可以通过直觉来弄清楚。Mat是存储图像数据的默认类型,所以应该是Mat。Mat 实现了 ToInputOutputArray 特性,一切都很好。
接下来,什么是 Rect 类型?
似乎我们可以在 core 中找到它。那很好。但是,我该如何构建一个 Rect?
使用 LSP,我们可以找到合适构造函数的自动完成。在这里,再次,有一点直觉和运气可以快速找到它(尽管与 Rust 相比,C++ 代码完成远非那么容易上手)。如果你被困在 C++ 中,通常会在网上寻找解决方案,这也不是那么令人愉快。
好的,让我们在这里看看 from_points 构造函数要说什么
我们需要two points!……但Point是什么?????
在 core 看起来像这样!让LSP为我们做更多的提升!
所以,“new”似乎很有希望,但“from_vec2”也是如此!我们可能可以使用其中任何一个。但是让我们选择“new”。
让我们给它输入两个整数,看看会发生什么。所以现在我们已经弄清楚了第二个参数
cv::core::Rect::from_points(
cv::core::Point::new(0, 0),
cv::core::Point::new(50, 50
)
(现在其余的方法相同……)。这有点乏味,但是一旦你开始了解类型,工作就会变得更轻松。在那个阶段,感觉很自然,你不会介意用 Rust 而不是 C++ 工作。
ndarray!
ndarray 似乎是 Rust 上最适合的矩阵库(对于正在为 Python 的 NumPy 包编写绑定的小伙子或小伙子来说,ndarray 是要走的路)。后面还有一篇关于用 Python 和 NumPy 绑定 Rust 的文章!
这有点棘手。现在我们需要将 C++ 类型转换为 Rust 类型。我们知道我们正在处理矩阵类型。它们通常是行优先的:https://en.wikipedia.org/wiki/Row-_and_column-major_order
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) 的无符号整数,我们可以直接读取数据而无需对其进行类型转换。
et 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<u8>
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
好的!我们得到了一种将 Mat 转换为 ArrayView3<u8> 并具有性能的方法。
请注意,这仅适用于连续数组。
作为一个特征,看起来像
trait AsArray {
fn try_as_array(&self) -> Result<ArrayView3<u8>>;
}
impl AsArray for cv::core::Mat {
fn try_as_array(&self) -> Result<ArrayView3<u8>> {
if !self.is_continuous() {
return Err(anyhow!("Mat is not continuous"));
}
let bytes = self.data_bytes()?;
let size = self.size()?;
let a = ArrayView3::from_shape((size.height as usize, size.width as usize, 3), bytes)?;
Ok(a)
}
}
为了快速将 Mat 转换为 Array,我们现在可以调用:
let array: ArrayView<u8> = mat.try_as_array()?;
结论
Rust 中的 OpenCV 肯定是可能的。它需要更深入的知识才能在不同类型之间进行转换,并且需要意志力来弄清楚绑定。但它有效!
由于 Rust 包管理器 Cargo 非常好,它鼓励使用其他人的包(例如用于在许多流行的 crate 之间转换图像类型的cv-convert(https://crates.io/crates/cv-convert)使生活更轻松。
希望我们将来会看到更多很酷的包。一些 Rust GPU 包开始出现,谁知道呢,也许将来有可能将 Rust 直接编译为 SPIR-V 以实现真正的快速计算!那将是多么美好的未来!
参与评论
登录后参与讨论 0/1000