首頁 > 軟體

OpenCV.js實現喬丹動圖素描效果圖文教學

2022-08-04 14:00:36

背景

大家都知道,最近幾年大熱的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喬丹動圖素描效果的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com