OpenCV.js实现乔丹动图素描效果图文教程
背景
大家都知道,最近几年大热的AI(人工智能)
,并且使用AI
做人脸识别和物品的分类,其实AI
不光可以做这些基本操作,还可以用其来画素描,因为本人是乔丹的篮球粉丝,于是想用AI
的技术来实现乔老爷子素描。
技术
因为本人是前端程序猿 爱好 AI
,所以我会用前端和AI
的方式来实现乔老爷子素描。正好OpenCV.js
可以满足我们的需求。
OpenCV.js 优点
OpenCV.js
的出现使得 JavaScript
开发者可以高效便捷的使用 OpenCV
提供的图形处理算法,也就是说开发者仅凭借浏览器就能快速开发诸如图片风格美化、图像识别、OCR等功能的应用。
OpenCV.js 地址
文档:docs.opencv.org/4.x/index.h…
github:github.com/opencv/open…
闲话不多说,今天就让我们跟着乔老爷子一起用OpenCV
实现素描效果吧!
项目搭建
准备图片
1. 引入 OpenCV.js
可以直接如下引入,也可以下载到本地,再引入:
<script class="lazy" data-src="https://docs.opencv.org/4.x/opencv.js"></script>
查看 OpenCV.js 引入状态
代码如下:
// html
<p id="status">OpenCV.js is loading...</p>
// js
let Module = {
onRuntimeInitialized() {
document.getElementById('status').innerHTML = 'OpenCV.js is ready.';
}
};
Module.onRuntimeInitialized();
效果,当页面的 loading
变成 read
,说明已完成OpenCV.js
加载。
2. 读取图片并显示
html 代码如下:
<div>
<div class="inputoutput">
<img id="imageclass="lazy" data-src" alt="No Image" width="100%" />
<div class="caption">imageclass="lazy" data-src <input type="file" id="fileInput" name="file" /></div>
</div>
<div class="inputoutput">
<canvas id="canvasOutput" ></canvas>
<div class="caption">canvasOutput</div>
</div>
</div>
js 代码如下:
let imgElement = document.getElementById('imageclass="lazy" data-src');
let inputElement = document.getElementById('fileInput');
inputElement.addEventListener('change', (e) => {
imgElement.class="lazy" data-src = URL.createObjectURL(e.target.files[0]);
}, false);
imgElement.onload = function() {
let img_origin = cv.imread(imgElement);
cv.imshow('canvasOutput', img_origin);
img_origin.delete();
};
效果如下图:
然后点击上传图片,上传图片后如下显示:
稍微解释一下上面的代码,首先我们可以本地上传一个图片,通过fileInput
获取图片文件,并把图片传给imageclass="lazy" data-src
渲染,
然后我们利用cv.imread('demo.jpg')
读取了这张图片,保存到img_origin
这个变量里面。
接下来用cv.imshow('origin', img_origin)
将这张照片通过一个canvas
显示出来,并且这个窗口的名称叫做canvasOutput
。
3. 彩色图片转成灰度图
接下来我们要把彩色图片转换成灰度图:
function cvtColor(img_origin) {
let img_gray = new cv.Mat();
cv.cvtColor(img_origin, img_gray, cv.COLOR_RGBA2GRAY, 0);
return img_gray;
}
没错,将彩色RGB
图片转换成灰度图用cv.cvtColor(img_origin, img_gray, cv.COLOR_RGBA2GRAY, 0);
就可以啦。
但是要注意这里我们用的是cv.cvtColor
方法,它的cv.COLOR_RGBA2GRAY
传参。
上面这段代码执行后,效果如下:
4. 对灰度图进行高斯模糊
接下来让我们对这张灰度图进行高斯模糊:
function GaussianBlur(img_origin) {
let img_blurred = new cv.Mat();
let ksize = new cv.Size(5, 5);
cv.GaussianBlur(img_origin, img_blurred, ksize, 0);
return img_blurred;
}
在这里,我们用cv.GaussianBlur(img_origin, img_blurred, ksize, 0)
完成了图像的高斯模糊。
在这里我们使用的(5,5)
参数就表示高斯核的尺寸,这个核尺寸越大图像越模糊。但是记住尺寸得是奇数!这是为了保证中心位置是一个像素而不是四个像素。
什么高斯模糊?
模糊就是一种特殊的滤波,经过这种滤波后图像变得不清晰。我们知道滤波 = 原始图像和掩膜的卷积,当掩膜(窗口)服从高斯分布时,此时我们称这种滤波为高斯滤波,也称为高斯模糊。
这样我们就得到一个模糊的乔老爷子:
5. 图像二值化
接下来到关键的一步啦!让我们对这张模糊过的图片进行二值化:
function adaptiveThreshold(img_origin) {
let img_threshold = new cv.Mat();
cv.adaptiveThreshold(img_origin, img_threshold, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 5, 2);
return img_threshold;
}
二值化的概念其实很简单,就是对一张图片上的点,像素值大于等于某个值的都直接设为最大值,小于这个值的都直接设为最小值,这样这张图片上每个点都只可能是最大值或最小值其中之一了,其中我们比较的这个数值就是阈值。
运行后就可以得到一个二值化的乔老爷子:
6.再次对二值化图像进行模糊
function img(img_origin, img_target) {
let img_gray = cvtColor(img_origin);
let ksize1 = new cv.Size(5, 5);
let img_blurred1 = GaussianBlur(img_gray, ksize1);
let img_threshold1 = adaptiveThreshold(img_blurred1);
let img_blurred2 = GaussianBlur(img_threshold1, ksize1);
img_target = img_blurred2;
cv.imshow('canvasOutput', img_target);
}
和上面写的一样我们用cv.GaussianBlur()
完成了高斯模糊,这样我们就可以得到一个模糊的描边乔老爷子,如下显示:
7.再次进行二值化
接下来我们对这张图片再次进行二值化:
function img(img_origin, img_target) {
let img_gray = cvtColor(img_origin);
let ksize1 = new cv.Size(5, 5);
let img_blurred1 = GaussianBlur(img_gray, ksize1);
let img_threshold1 = adaptiveThreshold(img_blurred1);
let img_blurred2 = GaussianBlur(img_threshold1, ksize1);
let img_threshold2 = threshold(img_blurred2);
img_target = img_threshold2;
cv.imshow('canvasOutput', img_target);
}
8.图像开运算
下面让我们去掉图片中一些细小的噪点,这种效果可以通过图像的开运算来实现:
function bitwise_not(img_origin) {
let img_opening = new cv.Mat();
let M = new cv.Mat();
let ksize = new cv.Size(3, 3);
M = cv.getStructuringElement(cv.MORPH_CROSS, ksize);
cv.morphologyEx(img_origin, img_opening, cv.MORPH_GRADIENT, M);
return img_opening;
}
要理解图像的开运算就要知道图像的腐蚀和膨胀,所谓的图像腐蚀就是如下的操作,类似于把一个胖子缩小一圈变瘦的感觉:
图像膨胀就是腐蚀的反向操作,把图像中的区块变大一圈,把瘦子变成胖子。
因此当我们对一个图像先腐蚀再膨胀的时候,一些小的区块就会由于腐蚀而消失,再膨胀回来的时候大块区域的边线的宽度没有发生变化,这样就起到了消除小的噪点的效果。图像先腐蚀再膨胀的操作就叫做开运算。
这样下来我们就可以实现对一张彩色图片转换成素描的效果啦!
看到这里恭喜大家你已经完成了70%了,下面我们要玩高级一点做动图。
10.读取并处理视频中的图像
搞定了单张图片,对视频进行处理就非常简单了,只需要将视频里每一帧都做同样的处理再输出即可。
首先在开头位置加上读取视频的语句:
let video = document.getElementById('videoInput');
let cap = new cv.VideoCapture(video);
let frame = new cv.Mat(video.height, video.width, cv.CV_8UC4);
let fgmask = new cv.Mat(video.height, video.width, cv.CV_8UC1);
然后创建一个setTimeout
定时任务,将图像处理的语句都放进去通过上面的方法处理成图片,并通过canvasOutput
渲染出来。
最后完整代码如下:
html 代码:
<div>
<div class="control"><button id="startAndStop" disabled>Start</button></div>
<div class="inputoutput">
<video id="videoInput" width="320" height="240" class="lazy" data-src="./mp4/7.mp4"></video>
<div class="caption">imageclass="lazy" data-src <input type="file" id="fileInput" name="file" /></div>
</div>
<div class="inputoutput">
<canvas id="canvasOutput" ></canvas>
<div class="caption">canvasOutput</div>
</div>
</div>
js 代码: 首先要变量声明
let streaming = false;
let videoInput = document.getElementById('videoInput');
let startAndStop = document.getElementById('startAndStop');
let canvasOutput = document.getElementById('canvasOutput');
let canvasContext = canvasOutput.getContext('2d');
代码监听和控制
startAndStop.addEventListener('click', () => {
if (!streaming) {
videoInput.play().then(() => {
onVideoStarted();
});
} else {
videoInput.pause();
videoInput.currentTime = 0;
onVideoStopped();
}
});
function onVideoStarted() {
streaming = true;
startAndStop.innerText = 'Stop';
videoInput.height = videoInput.width * (videoInput.videoHeight / videoInput.videoWidth);
video()
}
function onVideoStopped() {
streaming = false;
canvasContext.clearRect(0, 0, canvasOutput.width, canvasOutput.height);
startAndStop.innerText = 'Start';
}
videoInput.addEventListener('canplay', () => {
startAndStop.removeAttribute('disabled');
});
主要渲染代码:
function video() {
let video = document.getElementById('videoInput');
let cap = new cv.VideoCapture(video);
let frame = new cv.Mat(video.height, video.width, cv.CV_8UC4);
let fgmask = new cv.Mat(video.height, video.width, cv.CV_8UC1);
const FPS = 30;
function processVideo() {
try {
if (!streaming) {
// clean and stop.
frame.delete(); fgmask.delete();
return;
}
let begin = Date.now();
// start processing.
cap.read(frame);
img(frame, fgmask);
// cv.imshow('canvasOutput', fgmask);
// schedule the next one.
let delay = 1000/FPS - (Date.now() - begin);
setTimeout(processVideo, delay);
} catch (err) {
console.log(err);
}
};
// schedule the first one.
setTimeout(processVideo, 0);
}
原图:
效果如下:
Markup
<p id="status">OpenCV.js is loading...</p>
<div>
<div class="control"><button id="startAndStop" disabled>Start</button></div>
<div class="inputoutput">
<video id="videoInput" width="300" class="lazy" data-src="./mp4/7.mp4"></video>
<img id="imageclass="lazy" data-src" alt="No Image" width="100%"/>
<div class="caption">imageclass="lazy" data-src <input type="file" id="fileInput" name="file" /></div>
</div>
<div class="inputoutput">
<canvas id="canvasOutput" ></canvas>
<div class="caption">canvasOutput</div>
</div>
</div>
script
let streaming = false;
let videoInput = document.getElementById('videoInput');
let startAndStop = document.getElementById('startAndStop');
let canvasOutput = document.getElementById('canvasOutput');
let canvasContext = canvasOutput.getContext('2d');
let imgElement = document.getElementById('imageclass="lazy" data-src');
let inputElement = document.getElementById('fileInput');
inputElement.addEventListener('change', (e) => {
imgElement.class="lazy" data-src = URL.createObjectURL(e.target.files[0]);
}, false);
imgElement.onload = function() {
let img_origin = cv.imread(imgElement);
let img_target = new cv.Mat();
img(img_origin, img_target);
// cv.imshow('canvasOutput', img_origin);
img_origin.delete();
img_target.delete();
};
function img(img_origin, img_target) {
let img_gray = cvtColor(img_origin);
let ksize1 = new cv.Size(5, 5);
let img_blurred1 = GaussianBlur(img_gray, ksize1);
let img_threshold1 = adaptiveThreshold(img_blurred1);
let img_blurred2 = GaussianBlur(img_threshold1, ksize1);
let img_threshold2 = threshold(img_blurred2);
let img_opening = bitwise_not(img_threshold2);
let ksize2 = new cv.Size(3, 3);
let img_opening_blurred = GaussianBlur(img_opening, ksize2);
img_target = img_opening_blurred;
cv.imshow('canvasOutput', img_target);
// img_origin.delete();
}
function cvtColor(img_origin) {
let img_gray = new cv.Mat();
cv.cvtColor(img_origin, img_gray, cv.COLOR_RGBA2GRAY, 0);
return img_gray;
}
function GaussianBlur(img_origin, ksize) {
let img_blurred = new cv.Mat();
// let ksize = new cv.Size(5, 5);
cv.GaussianBlur(img_origin, img_blurred, ksize, 0);
return img_blurred;
}
function adaptiveThreshold(img_origin) {
let img_threshold = new cv.Mat();
cv.adaptiveThreshold(img_origin, img_threshold, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 5, 2);
return img_threshold;
}
function threshold(img_origin) {
let img_threshold = new cv.Mat();
cv.threshold(img_origin, img_threshold, 200, 255, cv.THRESH_BINARY);
return img_threshold;
}
function bitwise_not(img_origin) {
let img_opening = new cv.Mat();
let M = new cv.Mat();
let ksize = new cv.Size(3, 3);
M = cv.getStructuringElement(cv.MORPH_CROSS, ksize);
cv.morphologyEx(img_origin, img_opening, cv.MORPH_GRADIENT, M);
return img_opening;
}
function video() {
let video = document.getElementById('videoInput');
let cap = new cv.VideoCapture(video);
let frame = new cv.Mat(video.height, video.width, cv.CV_8UC4);
let fgmask = new cv.Mat(video.height, video.width, cv.CV_8UC1);
const FPS = 30;
function processVideo() {
try {
if (!streaming) {
// clean and stop.
frame.delete(); fgmask.delete();
return;
}
let begin = Date.now();
// start processing.
cap.read(frame);
img(frame, fgmask);
// cv.imshow('canvasOutput', fgmask);
// schedule the next one.
let delay = 1000/FPS - (Date.now() - begin);
setTimeout(processVideo, delay);
} catch (err) {
console.log(err);
}
};
// schedule the first one.
setTimeout(processVideo, 0);
}
startAndStop.addEventListener('click', () => {
if (!streaming) {
videoInput.play().then(() => {
onVideoStarted();
});
} else {
videoInput.pause();
videoInput.currentTime = 0;
onVideoStopped();
}
});
function onVideoStarted() {
streaming = true;
startAndStop.innerText = 'Stop';
videoInput.height = videoInput.width * (videoInput.videoHeight / videoInput.videoWidth);
video()
}
function onVideoStopped() {
streaming = false;
canvasContext.clearRect(0, 0, canvasOutput.width, canvasOutput.height);
startAndStop.innerText = 'Start';
}
videoInput.addEventListener('canplay', () => {
startAndStop.removeAttribute('disabled');
});
let Module = {
// https://emscripten.org/docs/api_reference/module.html#Module.onRuntimeInitialized
onRuntimeInitialized() {
document.getElementById('status').innerHTML = 'OpenCV.js is ready.';
}
};
Module.onRuntimeInitialized();
结语
其实很简单,大家可以自己实操,最后说几个我遇见的问题:
OpenCV.js
文件比较大,解决方法:本地、cdn。canvas
渲染视频需要服务环境,解决方法:node.js。
以上就是OpenCV.js实现乔丹动图素描效果图文教程的详细内容,更多关于OpenCV.js乔丹动图素描效果的资料请关注编程网其它相关文章!
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341