我们都知道为什么 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

入门 - 安装 (MacOS)

Linux 用户通常足够聪明,可以弄清楚如何在他们的机器上安装 OpenCV,否则请按照此处的指南进行操作:https://github.com/twistedfall/opencv-rust
Windows 用户可以按照本指南进行操作:https://github.com/twistedfall/opencv-rust
对于 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 存储库获取完整的安装帮助:https://github.com/twistedfall/opencv-rust
当安装它时,在编译时遇到了问题,但可以按照故障排除部分轻松修复。因此,如果你遇到问题,请确保在抓头发之前检查该部分。
这个 OpenCV Rust 绑定到 C++ API(这很好,因为 C 已经被废弃了)。
由于 Rust 可以直接与 C 接口,C++ 被包装在一个额外的 C 层中,然后暴露给 Rust。

简单代码

第一个示例将基于 Makeitnow 的视频教程:
https://www.youtube.com/watch?v=zcfixnuJFXg
对于有经验的 OpenCV 用户来说,这非常简单。
使用 anyhow 来处理结果:https://docs.rs/anyhow/latest/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.
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)?;

let
 key = highgui::wait_key(
1
)?;

if
 key == 
113
 { 
// quit with q
break
;

        }

    }

Ok
(())

}

太棒了!我们可以打开一个网络摄像头并将生成的帧放入 frame 变量中。
代码应该是不言自明的。否则看视频!
PS:这将是等效的 Python 代码
import
 cv2

vid = cv2.VideoCapture(
0
)

whileTrue
:

    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
let
 img = 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();

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
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
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
(())

}

traitAsArray
 {

fntry_as_array
(&
self
) -> 
Result
<ArrayView3<
u8
>>;

}

impl
 AsArray 
for
 cv::core::Mat {

fntry_as_array
(&
self
) -> 
Result
<ArrayView3<
u8
>> {

if
 !
self
.is_continuous() {

returnErr
(anyhow!(
"Mat is not continuous"
));

        }

let
 bytes = 
self
.data_bytes()?;

let
 size = 
self
.size()?;

let
 a = 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();

let
 raw = 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
letmut
 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
letmut
 orb_keypoints = cv::core::Vector::default();

letmut
 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
letmut
 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 文档(https://docs.rs/opencv/latest/opencv/index.html)和 C++ 的命名。
所以让我们使用这个策略来弄清楚如何绘制矩形。
首先,我们想好要做什么,即:
  • 绘制矩形(在 C++ 中是cv::rectangle
然后我们转到opencv-rust docs:https://docs.rs/opencv/latest/opencv/index.html
现在,我们所要做的就是找出类型(说起来容易做起来难)。在这里,我们将使用 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() {

returnErr
(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 
asusize
, w 
asusize
3
), data_bytes)?; 
// The 3 is because we have bgr. For gray image this will be 1
好的!我们得到了一种将 Mat 转换为 ArrayView3<u8> 并具有性能的方法。
请注意,这仅适用于连续数组。
作为一个特征,看起来像
traitAsArray
 {

fntry_as_array
(&
self
) -> 
Result
<ArrayView3<
u8
>>;

}

impl
 AsArray 
for
 cv::core::Mat {

fntry_as_array
(&
self
) -> 
Result
<ArrayView3<
u8
>> {

if
 !
self
.is_continuous() {

returnErr
(anyhow!(
"Mat is not continuous"
));

        }

let
 bytes = 
self
.data_bytes()?;

let
 size = 
self
.size()?;

let
 a = 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 非常好,它鼓励使用其他人的包(例如用于在许多流行的 crate 之间转换图像类型的cv-convert(https://crates.io/crates/cv-convert)使生活更轻松。
希望我们将来会看到更多很酷的包。一些 Rust GPU 包开始出现,谁知道呢,也许将来有可能将 Rust 直接编译为 SPIR-V 以实现真正的快速计算!那将是多么美好的未来!
☆ END ☆
如果看到这里,说明你喜欢这篇文章,请转发、点赞。微信搜索「uncle_pn」,欢迎添加小编微信「 woshicver」,每日朋友圈更新一篇高质量博文
扫描二维码添加小编↓
继续阅读
阅读原文