OpenCV.js實(shí)現(xiàn)喬丹動(dòng)圖素描效果圖文教程
背景
大家都知道,最近幾年大熱的AI(人工智能)
,并且使用AI
做人臉識(shí)別和物品的分類,其實(shí)AI
不光可以做這些基本操作,還可以用其來(lái)畫素描,因?yàn)楸救耸菃痰さ幕@球粉絲,于是想用AI
的技術(shù)來(lái)實(shí)現(xiàn)喬老爺子素描。
技術(shù)
因?yàn)楸救耸乔岸顺绦蛟?愛好 AI
,所以我會(huì)用前端和AI
的方式來(lái)實(shí)現(xiàn)喬老爺子素描。正好OpenCV.js
可以滿足我們的需求。
OpenCV.js 優(yōu)點(diǎn)
OpenCV.js
的出現(xiàn)使得 JavaScript
開發(fā)者可以高效便捷的使用 OpenCV
提供的圖形處理算法,也就是說開發(fā)者僅憑借瀏覽器就能快速開發(fā)諸如圖片風(fēng)格美化、圖像識(shí)別、OCR等功能的應(yīng)用。
OpenCV.js 地址
文檔:docs.opencv.org/4.x/index.h…
github:github.com/opencv/open…
閑話不多說,今天就讓我們跟著喬老爺子一起用OpenCV
實(shí)現(xiàn)素描效果吧!
項(xiàng)目搭建
準(zhǔ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();
效果,當(dāng)頁(yè)面的 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(); };
效果如下圖:
然后點(diǎn)擊上傳圖片,上傳圖片后如下顯示:
稍微解釋一下上面的代碼,首先我們可以本地上傳一個(gè)圖片,通過fileInput
獲取圖片文件,并把圖片傳給imageSrc
渲染,
然后我們利用cv.imread('demo.jpg')
讀取了這張圖片,保存到img_origin
這個(gè)變量里面。
接下來(lái)用cv.imshow('origin', img_origin)
將這張照片通過一個(gè)canvas
顯示出來(lái),并且這個(gè)窗口的名稱叫做canvasOutput
。
3. 彩色圖片轉(zhuǎn)成灰度圖
接下來(lái)我們要把彩色圖片轉(zhuǎn)換成灰度圖:
function cvtColor(img_origin) { let img_gray = new cv.Mat(); cv.cvtColor(img_origin, img_gray, cv.COLOR_RGBA2GRAY, 0); return img_gray; }
沒錯(cuò),將彩色RGB
圖片轉(zhuǎn)換成灰度圖用cv.cvtColor(img_origin, img_gray, cv.COLOR_RGBA2GRAY, 0);
就可以啦。
但是要注意這里我們用的是cv.cvtColor
方法,它的cv.COLOR_RGBA2GRAY
傳參。
上面這段代碼執(zhí)行后,效果如下:
4. 對(duì)灰度圖進(jìn)行高斯模糊
接下來(lái)讓我們對(duì)這張灰度圖進(jìn)行高斯模糊:
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ù)就表示高斯核的尺寸,這個(gè)核尺寸越大圖像越模糊。但是記住尺寸得是奇數(shù)!這是為了保證中心位置是一個(gè)像素而不是四個(gè)像素。
什么高斯模糊?
模糊就是一種特殊的濾波,經(jīng)過這種濾波后圖像變得不清晰。我們知道濾波 = 原始圖像和掩膜的卷積,當(dāng)掩膜(窗口)服從高斯分布時(shí),此時(shí)我們稱這種濾波為高斯濾波,也稱為高斯模糊。
這樣我們就得到一個(gè)模糊的喬老爺子:
5. 圖像二值化
接下來(lái)到關(guān)鍵的一步啦!讓我們對(duì)這張模糊過的圖片進(jìn)行二值化:
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í)很簡(jiǎn)單,就是對(duì)一張圖片上的點(diǎn),像素值大于等于某個(gè)值的都直接設(shè)為最大值,小于這個(gè)值的都直接設(shè)為最小值,這樣這張圖片上每個(gè)點(diǎn)都只可能是最大值或最小值其中之一了,其中我們比較的這個(gè)數(shù)值就是閾值。
運(yùn)行后就可以得到一個(gè)二值化的喬老爺子:
6.再次對(duì)二值化圖像進(jìn)行模糊
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()
完成了高斯模糊,這樣我們就可以得到一個(gè)模糊的描邊喬老爺子,如下顯示:
7.再次進(jìn)行二值化
接下來(lái)我們對(duì)這張圖片再次進(jìn)行二值化:
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.圖像開運(yùn)算
下面讓我們?nèi)サ魣D片中一些細(xì)小的噪點(diǎn),這種效果可以通過圖像的開運(yùn)算來(lái)實(shí)現(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; }
要理解圖像的開運(yùn)算就要知道圖像的腐蝕和膨脹,所謂的圖像腐蝕就是如下的操作,類似于把一個(gè)胖子縮小一圈變瘦的感覺:
圖像膨脹就是腐蝕的反向操作,把圖像中的區(qū)塊變大一圈,把瘦子變成胖子。
因此當(dāng)我們對(duì)一個(gè)圖像先腐蝕再膨脹的時(shí)候,一些小的區(qū)塊就會(huì)由于腐蝕而消失,再膨脹回來(lái)的時(shí)候大塊區(qū)域的邊線的寬度沒有發(fā)生變化,這樣就起到了消除小的噪點(diǎn)的效果。圖像先腐蝕再膨脹的操作就叫做開運(yùn)算。
這樣下來(lái)我們就可以實(shí)現(xiàn)對(duì)一張彩色圖片轉(zhuǎn)換成素描的效果啦!
看到這里恭喜大家你已經(jīng)完成了70%了,下面我們要玩高級(jí)一點(diǎn)做動(dòng)圖。
10.讀取并處理視頻中的圖像
搞定了單張圖片,對(duì)視頻進(jìn)行處理就非常簡(jiǎn)單了,只需要將視頻里每一幀都做同樣的處理再輸出即可。
首先在開頭位置加上讀取視頻的語(yǔ)句:
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)建一個(gè)setTimeout
定時(shí)任務(wù),將圖像處理的語(yǔ)句都放進(jìn)去通過上面的方法處理成圖片,并通過canvasOutput
渲染出來(lái)。
最后完整代碼如下:
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();
結(jié)語(yǔ)
其實(shí)很簡(jiǎn)單,大家可以自己實(shí)操,最后說幾個(gè)我遇見的問題:
OpenCV.js
文件比較大,解決方法:本地、cdn。canvas
渲染視頻需要服務(wù)環(huán)境,解決方法:node.js。
以上就是OpenCV.js實(shí)現(xiàn)喬丹動(dòng)圖素描效果圖文教程的詳細(xì)內(nèi)容,更多關(guān)于OpenCV.js喬丹動(dòng)圖素描效果的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序module.exports模塊化操作實(shí)例淺析
這篇文章主要介紹了微信小程序module.exports模塊化操作,結(jié)合實(shí)例形式簡(jiǎn)單分析了module.exports模塊化的定義與引用相關(guān)操作技巧與注意事項(xiàng),需要的朋友可以參考下2018-12-12Rxjs?TakeUntil?操作符內(nèi)容梳理總結(jié)
這篇文章主要介紹了Rxjs?TakeUntil操作符內(nèi)容梳理總結(jié),文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-06-06JS實(shí)現(xiàn)顯示帶倒影的圖片橫排居中放大展示特效實(shí)例【測(cè)試可用】
這篇文章主要介紹了JS實(shí)現(xiàn)顯示帶倒影的圖片橫排居中放大展示功能,可實(shí)現(xiàn)點(diǎn)擊圖片及點(diǎn)擊左右按鈕滑動(dòng)切換的效果,涉及javascript針對(duì)鼠標(biāo)事件的響應(yīng)及頁(yè)面元素動(dòng)態(tài)操作相關(guān)技巧,需要的朋友可以參考下2016-08-08js實(shí)現(xiàn)股票實(shí)時(shí)刷新數(shù)據(jù)案例
下面小編就為大家?guī)?lái)一篇js實(shí)現(xiàn)股票實(shí)時(shí)刷新數(shù)據(jù)案例。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2017-05-05CKEditor擴(kuò)展插件:自動(dòng)排版功能autoformat插件實(shí)現(xiàn)方法詳解
這篇文章主要介紹了CKEditor擴(kuò)展插件:自動(dòng)排版功能autoformat插件實(shí)現(xiàn)方法,結(jié)合實(shí)例形式詳細(xì)分析了CKEditor擴(kuò)展插件實(shí)現(xiàn)自動(dòng)排版功能的autoformat插件具體定義、配置與使用技巧,需要的朋友可以參考下2020-02-02vue-router路由懶加載的實(shí)現(xiàn)(解決vue項(xiàng)目首次加載慢)
這篇文章主要介紹了vue-router路由懶加載的實(shí)現(xiàn)(解決vue項(xiàng)目首次加載慢),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2018-08-08點(diǎn)擊單元格后可編輯單元格內(nèi)文本如何制作
點(diǎn)擊單元格后可編輯單元格內(nèi)文本如何制作...2006-10-10