热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

OpenCV.js实现乔丹动图素描效果图文教程【Js基础】

这篇文章主要为大家介绍了OpenCV.js实现乔丹动图素描效果的图文教程示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多

背景

大家都知道,最近几年大热的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

可以直接如下引入,也可以下载到本地,再引入:

查看 OpenCV.js 引入状态

代码如下:

// html

OpenCV.js is loading...

// js
let Module = {
  onRuntimeInitialized() {
    document.getElementById("status").innerHTML = "OpenCV.js is ready.";
  }
};
Module.onRuntimeInitialized();

效果,当页面的 loading 变成 read ,说明已完成OpenCV.js加载。

2. 读取图片并显示

html 代码如下:

No Image
imageSrc
canvasOutput

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 代码:

imageSrc
canvasOutput

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

OpenCV.js is loading...

No Image
imageSrc
canvasOutput

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乔丹动图素描效果的资料请关注编程笔记其它相关文章!


推荐阅读
  • Redux入门指南
    本文介绍Redux的基本概念和工作原理,帮助初学者理解如何使用Redux管理应用程序的状态。Redux是一个用于JavaScript应用的状态管理库,特别适用于React项目。 ... [详细]
  • Coursera ML 机器学习
    2019独角兽企业重金招聘Python工程师标准线性回归算法计算过程CostFunction梯度下降算法多变量回归![选择特征](https:static.oschina.n ... [详细]
  • 2018-2019学年第六周《Java数据结构与算法》学习总结
    本文总结了2018-2019学年第六周在《Java数据结构与算法》课程中的学习内容,重点介绍了非线性数据结构——树的相关知识及其应用。 ... [详细]
  • 本题来自WC2014,题目编号为BZOJ3435、洛谷P3920和UOJ55。该问题描述了一棵不断生长的带权树及其节点上小精灵之间的友谊关系,要求实时计算每次新增节点后树上所有可能的朋友对数。 ... [详细]
  • 本文介绍了如何使用JavaScript的Fetch API与Express服务器进行交互,涵盖了GET、POST、PUT和DELETE请求的实现,并展示了如何处理JSON响应。 ... [详细]
  • 本文探讨了如何在 F# Interactive (FSI) 中通过 AddPrinter 和 AddPrintTransformer 方法自定义类型(尤其是集合类型)的输出格式,提供了详细的指南和示例代码。 ... [详细]
  • 本文介绍了如何在 Node.js 中使用 `setDefaultEncoding` 方法为可写流设置默认编码,并提供了详细的语法说明和示例代码。 ... [详细]
  • 历经三十年的开发,Mathematica 已成为技术计算领域的标杆,为全球的技术创新者、教育工作者、学生及其他用户提供了一个领先的计算平台。最新版本 Mathematica 12.3.1 增加了多项核心语言、数学计算、可视化和图形处理的新功能。 ... [详细]
  • 深入解析Serverless架构模式
    本文将详细介绍Serverless架构模式的核心概念、工作原理及其优势。通过对比传统架构,探讨Serverless如何简化应用开发与运维流程,并介绍当前主流的Serverless平台。 ... [详细]
  • 深入解析Java虚拟机(JVM)架构与原理
    本文旨在为读者提供对Java虚拟机(JVM)的全面理解,涵盖其主要组成部分、工作原理及其在不同平台上的实现。通过详细探讨JVM的结构和内部机制,帮助开发者更好地掌握Java编程的核心技术。 ... [详细]
  • 基于Node.js、Express、MongoDB和Socket.io的实时聊天应用开发
    本文详细介绍了使用Node.js、Express、MongoDB和Socket.io构建的实时聊天应用程序。涵盖项目结构、技术栈选择及关键依赖项的配置。 ... [详细]
  • 深入解析Java枚举及其高级特性
    本文详细介绍了Java枚举的概念、语法、使用规则和应用场景,并探讨了其在实际编程中的高级应用。所有相关内容已收录于GitHub仓库[JavaLearningmanual](https://github.com/Ziphtracks/JavaLearningmanual),欢迎Star并持续关注。 ... [详细]
  • 嵌入式开发环境搭建与文件传输指南
    本文详细介绍了如何为嵌入式应用开发搭建必要的软硬件环境,并提供了通过串口和网线两种方式将文件传输到开发板的具体步骤。适合Linux开发初学者参考。 ... [详细]
  • Nature Microbiology: 人类肠道古菌基因组目录
    本研究揭示了人类肠道微生物群落中古细菌的多样性,分析了来自24个国家、农村和城市人群的1,167个非冗余古细菌基因组。研究鉴定了多个新分类群,并探讨了古菌对宿主的适应性及其与社会人口特征的关系。 ... [详细]
  • 在高并发需求的C++项目中,我们最初选择了JsonCpp进行JSON解析和序列化。然而,在处理大数据量时,JsonCpp频繁抛出异常,尤其是在多线程环境下问题更为突出。通过分析发现,旧版本的JsonCpp存在多线程安全性和性能瓶颈。经过评估,我们最终选择了RapidJSON作为替代方案,并实现了显著的性能提升。 ... [详细]
author-avatar
横戈跃马2012
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有