OpenCV.js实现乔丹动图素描效果图文教程

Canace ·
更新时间:2024-09-20
· 1286 次阅读

目录

背景

技术

OpenCV.js 优点

OpenCV.js 地址

项目搭建

准备图片

1. 引入 OpenCV.js

查看 OpenCV.js 引入状态

2. 读取图片并显示

3. 彩色图片转成灰度图

4. 对灰度图进行高斯模糊

5. 图像二值化

6.再次对二值化图像进行模糊

7.再次进行二值化

8.图像开运算

10.读取并处理视频中的图像

结语

背景

大家都知道,最近几年大热的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 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="imageSrc" alt="No Image" width="100%" /> <div class="caption">imageSrc <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('imageSrc'); let inputElement = document.getElementById('fileInput'); inputElement.addEventListener('change', (e) => { imgElement.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获取图片文件,并把图片传给imageSrc渲染,

然后我们利用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" src="./mp4/7.mp4"></video> <div class="caption">imageSrc <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" src="./mp4/7.mp4"></video> <img id="imageSrc" alt="No Image" width="100%"/> <div class="caption">imageSrc <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('imageSrc'); let inputElement = document.getElementById('fileInput'); inputElement.addEventListener('change', (e) => { imgElement.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乔丹动图素描效果的资料请关注软件开发网其它相关文章!



js实现 opencv js 教程

需要 登录 后方可回复, 如果你还没有账号请 注册新账号