本文经授权,根据Intel软件工程师Dmitry Kurtaev的文章翻译整理。
原文地址
https://habr.com/ru/company/intel/blog/437600/
英译文地址
https://opencv.org/opencv4arts-draw-my-city-vincent/
OpenCV是一个有20年历史的开源计算机视觉库,而且一直在持续不断开发。20岁,正是探索世界奥妙的年龄。有没有什么基于OpenCV的项目可以让人们的生活更美好、让大家开心呢?在思考和探索OpenCV新模块时,我想到了可以用OpenCV创建一些具有强烈视觉效果的应用。看到这些应用,人们的反应首先是惊叹,然后才意识到“哦,这就是计算机视觉”。本文首先来做一个风格迁移的实验,然后介绍此处理过程的核心以及相对OpenCV来说还很新的OpenCV.js,即JavaScript的OpenCV。风格迁移
首先要抱歉的告诉并不看好深度学习的读者,深度神经网络将是本文的一个重点,因为它的确很有效。OpenCV并不提供训练神经网络的功能,但是可以通过OpenCV应用已经训练好的网络模型。这里以CycleGan为例。感谢作者提供的免费模型,让我们能将苹果的图片转换为橙子,马的图片转换为斑马,卫星图像转换为普通地图,冬天的照片转换为夏天。虽然训练是将冬天转换为夏天,但训练会产生两个方向的生成器,所以利用模型也可以把夏天的照片转换为冬天。所以这里要用CycleGan作为例子。这里我使用可以把照片转换为画家的作品的神经网络模型,也就是转换为梵高、莫奈、塞尚或者浮世绘风格的图画。所以需要四个网络模型。在训练模型的时候,并不是使用画家的某一幅作品,而是其大量的作品。这样训练得到的模型会按照画家的绘画风格进行风格迁移,而不是按照某幅作品形式进行迁移。 OpenCV.jsOpenCV是一个用C++实现的库,但其大部分函数都可以自动生成wrappers。目前OpenCV主线版本支持Python和Java的wrapper。此外,也有自定义的Go和PHP实现。如果你在实际应用使用其它语言wrapper,也欢迎告诉我。OpenCV.js是2017年谷歌编程之夏(Google Summer of Code)的一个项目。跟其它语言不同,OpenCV.js并不是对原始方法函数的封装,它是用Emscripten使用LLVM和Clang完全编译生成的。它可以将C或C++的程序转换成一个能在浏览器运行的.js文件。
例如
#include
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
然后在浏览器中打开
html>
<html>
<head>
<script src="main.js" type="text/javascript">script>
head>
html>
OpenCV.js可以用下面的方式与你的工程联系起来(nightly build)
<script src="https://docs.opencv.org/master/opencv.js" type="text/javascript">script>
另外还需要一个用JavaScript手动实现的读取图像的库
<script src="https://docs.opencv.org/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默认的读入图像文件后产生一幅3通道的BGR图像是不同的。这点在导入其它语言的算法时要特别注意。
然后,只需调用imshow函数即可以显示图像。imshow的参数是指定的canvas(RGB或GRBA)。
cv.imshow("canvasOutput", img);
算法
整个处理过程其实就是神经网络的前向计算。我们不需要知道网络内部发生了什么,只需提供正确的输入,然后正确的解析预测(网络输出)即可。这个例子的输入是一个四维浮点矩阵,元素值范围为[-1,1]。这四个维度分别是:图片的数量、图像的通道、图像的高、图像的宽,用NCHW来表示。这个四维矩阵叫做blob,是一个二值量。预处理首先要将其转换成OpenCV格式的图像,即图像通道是交错的,元素值范围为[0,255]。后处理是预处理的反向过程,即将网络输出元素值范围为[-1,1]的NCHW blob转换到[0, 255]区间的无符号整型。人眼看到的Nizhny Novgorod Kremlin的图片块
OpenCV中的交错表示
按颜色平面的表示
所以,OpenCV.js读取和输出图片的整个过程为:imread -> RGBA -> BGR [0, 255] -> NCHW [-1, 1] -> [network]
[network] -> 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页面。当然也可以使用原始链接,只是下载的时间会长一点。
从canvas读入图像,然后将其从RGBA转换到BGR
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模块需要用下面的list来定义有效的函数:
dnn = {'dnn_Net': ['setInput', 'forward'],
'': ['readNetFromCaffe', 'readNetFromTensorflow',
'readNetFromTorch', 'readNetFromDarknet',
'readNetFromONNX', 'readNet', 'blobFromImage']}
上面说到的对于输出的转换,即将blob分成3个通道然后再合并为一幅图像,其实可以用imagesFromBlob函数来实现。但是目前它还没有添加进上面的list,也许你可以来完成这个功能,成为你对OpenCV的第一个贡献?:)
结论
可以到我的GitHub( https://dkurtaev.github.io/opencv4arts)查看上面风格迁移的例子,你也可以测试代码。每次使用新的图像时,建议刷新一下页面,否则会影响后面的处理。另外,处理过程不是很快,最好改变一下图像的尺寸。在写这篇文章并选择一幅合适的示例图片时,我偶然发现了一张我朋友照的Nizhny Novgorod Kremlin的照片。我建议你在实验时也使用你喜欢的景点的照片,也许你还可以在评论里告诉我一些关于她的趣事。 OpenCV中国团队官方推荐和认证OpenCV&AI在线课程长按右侧QR码进入课程
OpenCV中国团队于2019年9月由深圳市人工智能与机器人研究院支持成立,非营利目的,致力于OpenCV的开发、维护和推广工作。
获取OpenCV最新动态,长按关注我们