<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
大家都知道,最近幾年大熱的AI(人工智慧)
,並且使用AI
做臉部辨識和物品的分類,其實AI
不光可以做這些基本操作,還可以用其來畫素描,因為本人是喬丹的籃球粉絲,於是想用AI
的技術來實現喬老爺子素描。
因為本人是前端程式猿 愛好 AI
,所以我會用前端和AI
的方式來實現喬老爺子素描。正好OpenCV.js
可以滿足我們的需求。
OpenCV.js
的出現使得 JavaScript
開發者可以高效便捷的使用 OpenCV
提供的圖形處理演演算法,也就是說開發者僅憑藉瀏覽器就能快速開發諸如圖片風格美化、影象識別、OCR等功能的應用。
檔案:docs.opencv.org/4.x/index.h…
github:github.com/opencv/open…
閒話不多說,今天就讓我們跟著喬老爺子一起用OpenCV
實現素描效果吧!
可以直接如下引入,也可以下載到本地,再引入:
<script src="https://docs.opencv.org/4.x/opencv.js"></script>
程式碼如下:
// 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
載入。
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
。
接下來我們要把彩色圖片轉換成灰度圖:
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
傳參。
上面這段程式碼執行後,效果如下:
接下來讓我們對這張灰度圖進行高斯模糊:
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)
引數就表示高斯核的尺寸,這個核尺寸越大影象越模糊。但是記住尺寸得是奇數!這是為了保證中心位置是一個畫素而不是四個畫素。
什麼高斯模糊?
模糊就是一種特殊的濾波,經過這種濾波後影象變得不清晰。我們知道濾波 = 原始影象和掩膜的折積,當掩膜(視窗)服從高斯分佈時,此時我們稱這種濾波為高斯濾波,也稱為高斯模糊。
這樣我們就得到一個模糊的喬老爺子:
接下來到關鍵的一步啦!讓我們對這張模糊過的圖片進行二值化:
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 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()
完成了高斯模糊,這樣我們就可以得到一個模糊的描邊喬老爺子,如下顯示:
接下來我們對這張圖片再次進行二值化:
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); }
下面讓我們去掉圖片中一些細小的噪點,這種效果可以通過影象的開運算來實現:
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%了,下面我們要玩高階一點做動圖。
搞定了單張圖片,對視訊進行處理就非常簡單了,只需要將視訊裡每一幀都做同樣的處理再輸出即可。
首先在開頭位置加上讀取視訊的語句:
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喬丹動圖素描效果的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45