Skip to content

使用opencv.js边缘计算

新建html文件,引入opencv.js

html
<script src="https://docs.opencv.org/4.x/opencv.js"></script>

添加canvas容器

html
<canvas id="myCanvas"></canvas>

添加样式使canvas占满全屏

css
canvas {
    position: fixed;
    top: 0;
    left: 0;
    height: 100vh;
    width: 100vw;
}

使用opencv.js提供的对图标进行边缘检测算法拿到图像数据

javascript
function getCannyEdges(imagePath) {
    return new Promise((resolve, reject) => {
        const img = new Image();
        img.src = imagePath;
        img.onload = function () {
            const imageInput = cv.imread(img);

            // 将图像转换为灰度图像
            const gray = new cv.Mat();
            cv.cvtColor(imageInput, gray, cv.COLOR_RGBA2GRAY);

            // 使用Canny边缘检测算法
            const edges = new cv.Mat();
            cv.Canny(gray, edges, 50, 150);

            // 放大一倍(或按需要放大)
            const outputWidth = edges.cols * 1;
            const outputHeight = edges.rows * 1;
            const enlargedOutput = new cv.Mat();
            cv.resize(edges, enlargedOutput, new cv.Size(outputWidth, outputHeight), 0, 0, cv.INTER_LINEAR);

            // 返回 OpenCV 的 Mat 边缘检测结果
            resolve(enlargedOutput);

            // 释放内存
            imageInput.delete();
            gray.delete();
            edges.delete();
        };

        img.onerror = reject;
    });
}

canvas实现图像粒子动画

设置canvas容器的宽高为图片的宽高,分析边缘计算的数据拿到黑色像素的位置信息,为了优化效率,只取偶数位的像素信息。

javascript
function drawEdgesWithAnimation(mat, canvas) {
  const ctx = canvas.getContext('2d');
  const width = mat.cols;
  const height = mat.rows;

  // 设置 canvas 尺寸
  canvas.width = width;
  canvas.height = height;
  console.log(width,height)
  // 获取黑色像素点
  let blackPixels = [];
  
  for (let i = 0; i < height; i++) {
      let last=0
      for (let j = 0; j < width; j++) {
          const pixel = mat.ucharPtr(i, j);
          if (pixel[0] === 255&&(i+j)%2==0) {  // 黑色边缘像素
              blackPixels.push({ x: j, y: i });
      
              last=j
          }
      }
     
  }
}

创建动画函数,使粒子动画在20帧完成。函数传入所有像素粒子的初始位置信息,根据最终位置信息计算每一帧的粒子位置,进行渲染,以此来达到动画的效果。、

添加动画触发方法,这里我是直接给canvas加了点击事件

因为粒子的大小是像素级别的,所以粒子数会非常多,所以在获取黑色像素信息那一步只取偶数位的粒子。

javascript
// 点击事件响应
canvas.addEventListener('click', (event) => {
    const rect = canvas.getBoundingClientRect();
    const clickX = ((event.clientX - rect.left) / rect.width) * canvas.width;
    const clickY = ((event.clientY - rect.top) / rect.height) * canvas.height;

    // 找到点击范围内的黑色像素
    const affectedPixels = blackPixels.filter(pixel => {
        const dx = pixel.x - clickX;
        const dy = pixel.y - clickY;
        return Math.sqrt(dx * dx + dy * dy) < 5000; // 半径为50的范围
    });
    // 随机移动这些像素
    affectedPixels.forEach(pixel => {
        pixel.targetX = pixel.x + (Math.random() - 0.5) * 200;
        pixel.targetY = pixel.y + (Math.random() - 0.5) * 200;
    });

    // 开始动画
    animatePixels(affectedPixels);
});

初始化调用方法

javascript
// 使用示例
window.onload = function () {
    getCannyEdges('./1.jpg').then((mat) => {
        const canvas = document.getElementById('myCanvas');
        drawEdgesWithAnimation(mat, canvas);
        canvas.click()
    });
}

加载效率优化

因为opencv.js非常大,差不多有10M,这对于浏览器来说会使网页加载很慢。所以我们可以使用预处理的方式来优化掉opencv.js。

我们的目标是实现粒子动画,所以我们完全可以直接保持图标边缘计算后的黑色像素坐标信息,然后直接使用这些坐标信息来进行动画的实现。


可以选择将数据保存为json格式,类似这样{x:100,y:120}。这是一个好注意,但是依然牢记这是在浏览器环境,我们可以尽可能的对数据进行压缩来保证文件的传输速度。以下是优化思路:

  1. 因为数据都只有x,y属性,所以我们可以直接村数据,然后使用英文逗号分割,类似这样100,120。(经测试,json是1.6M,使用后变成800Kb)
  2. 观察数据,粒子总是从左到右,丛上到下的,也就是说对于同一行的粒子他们的y是相同的,而下一行的粒子的y是上一行+1;所以我们只存x的数据,遇到下一行就添加一个@,所有数据依然使用英文逗号分隔。(经测试,数据从800Kb变为400Kb)
  3. 400Kb已经基本达到预期,但还可以优化一下。我们观察数据,对于一行的粒子,他们的x总是从0到结尾,中间是持续增长的。所以我们可以不用存储x的值,而是x(i)-x(i-1)的值,也就是后一项与前一项的差值。(经测试,数据从400Kb变为200+Kb)

实现方式:修改drawEdgesWithAnimation方法

javascript
function drawEdgesWithAnimation(mat, canvas) {
  const ctx = canvas.getContext('2d');
  const width = mat.cols;
  const height = mat.rows;

  // 设置 canvas 尺寸
  canvas.width = width;
  canvas.height = height;
  console.log(width,height)
  // 获取黑色像素点
  let blackPixels = [];
  let str=""
  for (let i = 0; i < height; i++) {
      let last=0
      for (let j = 0; j < width; j++) {
          const pixel = mat.ucharPtr(i, j);
          if (pixel[0] === 255&&(i+j)%3==0) {  // 黑色边缘像素
              blackPixels.push({ x: j, y: i });
              str+=`${j-last},`
              last=j
          }
      }
      str+='@,'
  }
  //保存为json文件

  str=str.slice(0,-3)
  const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(str));
  const downloadAnchorNode = document.createElement('a');
  downloadAnchorNode.setAttribute("href", dataStr);
  downloadAnchorNode.setAttribute("download", "data.txt");
  document.body.appendChild(downloadAnchorNode); // required for firefox
  downloadAnchorNode.click();
  downloadAnchorNode.remove();
  console.log(JSON.stringify(str))
}

像素位置还原方法:

typescript
function(pixelData:string){
  let blackPixels: Pixel[] = [];
  const pixelArr=pixelData.split(',')
  let j=0;
  let last =0;
  let xPixel=0;
  for( let i=0;i<pixelArr.length;i++){
    if(pixelArr[i]=='@'){
      i++;
      j++;
      last=0
    }
    xPixel=last+Number(pixelArr[i])
    blackPixels.push({ x: xPixel, y: j, originalX: xPixel, originalY: j, targetX: xPixel, targetY: j });
    last=xPixel
  }
}

Last updated: