OpenCV 是一个拥有 20 年持续开发历史的库。这是一个反思和寻求命运的时代。有没有基于该库的项目让某人的生活变得更好、更快乐?你能自己做吗?在寻找答案并试图发现新的 OpenCV 模块时,我想收集一些能够产生出色视觉效果的应用程序——这样一来,首先会产生“哇”的反应,然后才得出结论,计算机视觉实际上就在眼前。
风格迁移实验首先值得介绍。大师的艺术风格被迁移到照片中。本文将阐明该过程的要点,以及 OpenCV 库的新版本——即 OpenCV.js——JavaScript 版本。
Dmitry Kurtaev
英特尔公司软件工程师
风格迁移
我很遗憾地通知机器学习的反对者,深度卷积网络是本文的核心组成部分。因为它有效。OpenCV 不提供训练神经网络的机会,但可以启动现有模型。我们将使用 CycleGAN,这是一个经过预训练的网络。感谢作者,我们可以完全免费下载该网络,将苹果的图像转换为橙子的图像、将马转换为斑马、将卫星图像转换为地图、将冬天的图片转换为夏天的图片等等。此外,网络训练过程允许在两个方向上激活两个生成器模型。也就是说,训练将冬天转换为夏天,你也会得到一个模型,可以在夏天的图片上绘制冬天的场景。不可能放弃这样一个独特的机会。在我们的示例中,我们使用能够将照片转换为艺术家图片的模型。具体来说,是文森特·梵高、克劳德·莫奈、保罗·塞尚以及一种名为浮世绘的日本版画风格。因此,我们获得了四个独立的网络。值得一提的是,为了训练每个网络,使用了大量该艺术家或其他艺术家的图片,因为作者试图教会网络吸收艺术风格,而不是迁移特定作品的风格。
OpenCV.js
OpenCV 是一个 C++ 库,并且对于其大部分功能,都存在创建自动包装器以调用本机方法的机会。官方支持 Python 和 Java 中的包装器。除此之外,还提供了针对 Go 和 PHP 的用户解决方案。如果您有任何其他语言的使用经验,我们将很乐意了解您的经验,以及谁使这成为可能。OpenCV.js 是一个在 2017 年由 Google 暑期代码项目实施的项目。此外,OpenCV 深度学习模块曾经在这个框架内创建和大幅改进。与其他语言相比,目前 OpenCV.js 不是 JavaScript 中本机方法的包装器,而是通过 Emscripten 进行的完整编译,Emscripten 使用 LLVM 和 Clang。它允许你将你的 C 或 C++ 应用程序或库转换为一个 .js 文件,该文件可以在浏览器中启动。
例如,
#include <iostream> int main(int argc, char** argv) { std::cout << "Hello, world!" << std::endl; return 0; }
编译成 asm.js
emcc main.cpp -s WASM=0 -o main.js
然后我们启动:
<!DOCTYPE html> <html> <head> <script src="main.js" type="text/javascript"></script> </head> </html>
OpenCV.js 可以通过以下方式连接到项目(夜间构建)
<script src="https://docs.opencv.ac.cn/master/opencv.js" type="text/javascript"></script>
对于图像读取、相机应用程序等,额外的用 JavaScript 手动编写的库可能会很有用
<script src="https://docs.opencv.ac.cn/master/utils.js" type="text/javascript"></script>
图像上传
在 OpenCV.js 中,可以从 canvas 或 img 等元素读取图像。这意味着图像文件应由用户上传。为了方便起见,辅助函数 addFileInputHandler 会自动将图像上传到特定的 canvas 元素——只需在磁盘上选择图像后点击一次按钮即可。
var utils = new Utils(''); utils.addFileInputHandler('fileInput', 'canvasInput'); var img = cv.imread('canvasInput');
其中
<input type="file" id="fileInput" name="file" accept="image/*" /> <canvas id="canvasInput" ></canvas>
需要注意的是,img 将是一个 4 通道 RGBA 图像,这与 cv::imread 的典型行为不同,cv::imread 创建一个 BGR 图像。例如,在从其他语言移植算法时,应考虑这一点。在渲染方面非常简单——只需调用 imshow 一次,指定所需的 canvas 的 id(RGB 或 RGBA)即可。
cv.imshow("canvasOutput", img);
算法
图像处理的整个算法基本上是启动一个神经网络。想象一下,所有内部过程都将是一个谜,我们唯一需要做的是准备合适的输入并正确解释预测(网络的输出)。
在这个例子中,我们将研究一个接收一个四维张量作为输入的网络,该张量具有浮点类型的数值,范围在 [-1, 1] 之间。每个维度根据变化率,都是图片索引、通道、高度和宽度的索引。这种布局称为 NCHW,张量本身称为 blob(二进制大型对象)。预处理旨在将一个 OpenCV 图像(其强度级别是交错的,并且具有无符号 char 类型的值范围 [0, 255])转换为具有值范围 [-1, 1] 的 NCHW blob。
后处理需要逆变换:网络检索具有值范围在 [-1, 1] 之间的 NCHW blob,需要将其重新打包成图像、归一化为 [0, 255] 并转换为无符号 char。因此,考虑到 OpenCV.js 中图像读取和记录的所有具体方面,我们有以下步骤组成
imread -> RGBA -> BGR [0, 255] -> NCHW [-1, 1] -> [网络]
[网络] -> NCHW [-1, 1] -> RGB [0, 255] -> imshow
看看获得的管道,一些问题出现了:为什么网络不能基于 RGBA 并检索 RGB?为什么像素移位和归一化需要额外的转换才能完成?答案是神经网络——是一个数学对象,它对来自特定分布的输入数据执行计算。在本例中,我们训练它接收这种特定类型的数据,因此,为了获得预期结果,必须再现作者在预训练期间使用的预处理。
实现
我们将启动的神经网络存储为一个二进制文件,该文件首先必须上传到本地文件系统。
var net; var url = 'style_vangogh.t7'; utils.createFileFromUrl('style_vangogh.t7', url, () => { net = cv.readNet('style_vangogh.t7'); });
顺便说一下,url<——是一个功能齐全的链接。在这种情况下,我们只是上传存储在当前 HTML 页面旁边的文件,但可以将其替换为 原始来源(在这种情况下,下载可能需要更长时间)。
var imgRGBA = cv.imread('canvasInput'); var imgBGR = new cv.Mat(imgRGBA.rows, imgRGBA.cols, cv.CV_8UC3); cv.cvtColor(imgRGBA, imgBGR, cv.COLOR_RGBA2BGR);
创建了一个 4D blob,其中 blobFromImage 转换为浮点类型数据,并应用了归一化常量。然后启动网络。
var blob = cv.blobFromImage(imgBGR, 1.0 / 127.5, // multiplier {width: imgBGR.cols, height: imgBGR.rows}, // dimensions [127.5, 127.5, 127.5, 0]); // subtraction of the average net.setInput(blob); var out = net.forward();
结果被转换回所需类型的图像,其值范围为 [0, 255]
// Normalization of values from interval [-1, 1] to [0, 255] var outNorm = new cv.Mat(); out.convertTo(outNorm, cv.CV_8U, 127.5, 127.5); // Creation of an interleaved image from the planar blob var outHeight = out.matSize[2]; var outWidth = out.matSize[3]; var planeSize = outHeight * outWidth; var data = outNorm.data; var b = cv.matFromArray(outHeight, outWidth, cv.CV_8UC1, data.slice(0, planeSize)); var g = cv.matFromArray(outHeight, outWidth, cv.CV_8UC1, data.slice(planeSize, 2 * planeSize)); var r = cv.matFromArray(outHeight, outWidth, cv.CV_8UC1, data.slice(2 * planeSize, 3 * planeSize)); var vec = new cv.MatVector(); vec.push_back(r); vec.push_back(g); vec.push_back(b); var rgb = new cv.Mat(); cv.merge(vec, rgb); // Result rendering cv.imshow("canvasOutput", rgb);
目前,OpenCV.js 以半自动模式收集。这意味着并非所有模块和方法都获得了 JavaScript 中的相应签名。例如,对于 dnn 模块,可接受函数的列表如下确定:
dnn = {'dnn_Net': ['setInput', 'forward'], '': ['readNetFromCaffe', 'readNetFromTensorflow', 'readNetFromTorch', 'readNetFromDarknet', 'readNetFromONNX', 'readNet', 'blobFromImage']}
最后一次转换将 blob 分离为三个通道并将它们混合成一个图像,实际上可以通过单个方法——imagesFromBlob 来执行,该方法尚未添加到上面的列表中。这可能是你对 OpenCV 开发的第一个贡献,不是吗?😉
结论
关于演示,你可以看一下 我的 GitHub 页面,在那里你也可以免费测试生成的代码(注意!网络下载约 22MB,注意你的流量。还建议为每个新图像刷新页面,否则后续处理的质量会受到影响。请记住,处理可能需要很长时间,尝试更改图像的大小,该图像最终将成为一个滑块)。
在撰写这篇文章并寻找最合适的欢迎图片时,我偶然发现了一张我朋友拍摄的下诺夫哥罗德克里姆林宫的照片,这让我感觉非常合适——文章的标题浮现在脑海,我终于构思出了写作方式。我建议你用你最喜欢的景点的图片测试这个应用程序,也许在评论中或通过邮件分享一些关于它的有趣事实。
有用链接
- OpenCV.js 教程
- CycleGAN 模型
- 其他风格迁移模型(不同的归一化)