OpenCV.js實現(xiàn)喬丹動圖素描效果圖文教程

背景
大家都知道,最近幾年大熱的AI(人工智能),并且使用AI做人臉識別和物品的分類,其實AI不光可以做這些基本操作,還可以用其來畫素描,因為本人是喬丹的籃球粉絲,于是想用AI的技術來實現(xiàn)喬老爺子素描。


技術
因為本人是前端程序猿 愛好 AI,所以我會用前端和AI的方式來實現(xiàn)喬老爺子素描。正好OpenCV.js可以滿足我們的需求。
OpenCV.js 優(yōu)點
OpenCV.js 的出現(xiàn)使得 JavaScript 開發(fā)者可以高效便捷的使用 OpenCV 提供的圖形處理算法,也就是說開發(fā)者僅憑借瀏覽器就能快速開發(fā)諸如圖片風格美化、圖像識別、OCR等功能的應用。
OpenCV.js 地址
文檔:docs.opencv.org/4.x/index.h…
github:github.com/opencv/open…
閑話不多說,今天就讓我們跟著喬老爺子一起用OpenCV實現(xiàn)素描效果吧!
項目搭建
準備圖片



1. 引入 OpenCV.js
可以直接如下引入,也可以下載到本地,再引入:
<script src="https://docs.opencv.org/4.x/opencv.js"></script>
查看 OpenCV.js 引入狀態(tài)
代碼如下:
// 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傳參。
上面這段代碼執(zhí)行后,效果如下:

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)參數(shù)就表示高斯核的尺寸,這個核尺寸越大圖像越模糊。但是記住尺寸得是奇數(shù)!這是為了保證中心位置是一個像素而不是四個像素。
什么高斯模糊?
模糊就是一種特殊的濾波,經(jīng)過這種濾波后圖像變得不清晰。我們知道濾波 = 原始圖像和掩膜的卷積,當掩膜(窗口)服從高斯分布時,此時我們稱這種濾波為高斯濾波,也稱為高斯模糊。
這樣我們就得到一個模糊的喬老爺子:

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;
}
二值化的概念其實很簡單,就是對一張圖片上的點,像素值大于等于某個值的都直接設為最大值,小于這個值的都直接設為最小值,這樣這張圖片上每個點都只可能是最大值或最小值其中之一了,其中我們比較的這個數(shù)值就是閾值。
運行后就可以得到一個二值化的喬老爺子:

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.圖像開運算
下面讓我們去掉圖片中一些細小的噪點,這種效果可以通過圖像的開運算來實現(xiàn):
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;
}
要理解圖像的開運算就要知道圖像的腐蝕和膨脹,所謂的圖像腐蝕就是如下的操作,類似于把一個胖子縮小一圈變瘦的感覺:

圖像膨脹就是腐蝕的反向操作,把圖像中的區(qū)塊變大一圈,把瘦子變成胖子。
因此當我們對一個圖像先腐蝕再膨脹的時候,一些小的區(qū)塊就會由于腐蝕而消失,再膨脹回來的時候大塊區(qū)域的邊線的寬度沒有發(fā)生變化,這樣就起到了消除小的噪點的效果。圖像先腐蝕再膨脹的操作就叫做開運算。
這樣下來我們就可以實現(xiàn)對一張彩色圖片轉換成素描的效果啦!


看到這里恭喜大家你已經(jīng)完成了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);
然后創(chuàng)建一個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');
代碼監(jiān)聽和控制
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渲染視頻需要服務環(huán)境,解決方法:node.js。
以上就是OpenCV.js實現(xiàn)喬丹動圖素描效果圖文教程的詳細內容,更多關于OpenCV.js喬丹動圖素描效果的資料請關注腳本之家其它相關文章!
相關文章
js中int和string數(shù)據(jù)類型互相轉化實例
在本篇文章里小編給大家分享了關于js中int和string數(shù)據(jù)類型互相轉化實例和代碼,需要的朋友們學習下。2019-01-01
微信小程序開發(fā)實現(xiàn)的IP地址查詢功能示例
這篇文章主要介紹了微信小程序開發(fā)實現(xiàn)的IP地址查詢功能,可實現(xiàn)基于第三方接口的IP地址查詢功能,需要的朋友可以參考下2019-03-03
firefox TBODY 用js顯示和隱藏時出現(xiàn)錯位的解決方法
今天幫別人寫一個網(wǎng)頁,發(fā)現(xiàn):當用javascript動態(tài)設置tr.style.display = "block"顯示某行時,使用IE瀏覽沒有問題,但使用firefox瀏覽時該行被移到了其它行的后面,很是詫異。2008-12-12

