Rust和OpenCV

磐创AI 2022-05-24

rustopencvtext-indent

2816 字丨阅读本文需 13 分钟

我们都知道为什么 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赞 好资讯,需要你的鼓励
来自:磐创AI
0

参与评论

登录后参与讨论 0/1000

为你推荐

加载中...