vue3+canvas实现摄像头ROI区域标记

  1. 通过canvas配合JSMpeg来渲染视频
  2. 给canvas绑定按下,移动、抬起事件实现路径绘画
  3. 获取坐标后roi如何计算,如果过程中屏幕发生改变、如何重新计算roi坐标

路径绘画代码

js 复制代码
<canvas
      //contextmenu.prevent 这里我是点击鼠标右击结束绘画,当数量大于8后禁用绘画
      id="mycanvas"
      ref="mycanvas"
      @mousedown="canvasDown($event)"
      @mousemove="canvasMove($event)"
      @mouseup="canvasUp($event)"
      @contextmenu.prevent="all_coordinatesArr.length < 8 ? doubleclick() : ''"
      >浏览器不持之canvas
</canvas>
<script setup>
const isdraw = ref(false); //是否在画图形
const ctx = ref(null); //canvas对象
const coordinates = ref([]); //一个多边形的坐标信息
const endtip = ref(false); //是否结束一个多边形的绘制
const all_coordinates = ref([]); //所有多边形的信息
const all_coordinatesArr = ref([]); //初始坐标点
const isSwitch = ref(false); //控制是否可以画图
 //鼠标按下事件
  const canvasDown = (e) => {
     //是否开启绘画
    if (isSwitch.value) {
    // 判断是否点击的为鼠标左键
        if (e.button === 0) {
          //判断是否结束本地绘画
          if (endtip.value) {
          //当前点击坐标小于10后可以结束本次绘画
            if (all_coordinates.value.length < 10) {
              endtip.value = false; //清空,重新画
            } else {
              //   $message.warning("最多可绘制十个闭合的凸多边形区域");
          return;
        }
      }
      //获取鼠标按下的坐标,放入本次坐标数组中
      var x = e.offsetX;
      var y = e.offsetY;
      var insertFlag = true;
      //判断这次绘画的线条是否有坐标相交
      coordinates.value.forEach((item, index) => {
        if (item.cor_x == x && item.cor_y == y) {
          insertFlag = false;
        }
      });
      if (insertFlag) {
        coordinates.value.push({ cor_x: x, cor_y: y });
        // 判断相交
        var lineList = [];
        coordinates.value.forEach((item, index) => {
          if (index < coordinates.value.length - 1) {
            lineList.push([
              item.cor_x,
              item.cor_y,
              coordinates.value[index + 1].cor_x,
              coordinates.value[index + 1].cor_y,
            ]);
          }
        });
        lineList.forEach((item, index) => {
          if (index > 0 && index < lineList.length - 1) {
            var flag = judgeIntersect(
              lineList[lineList.length - 1][0],
              lineList[lineList.length - 1][1],
              lineList[lineList.length - 1][2],
              lineList[lineList.length - 1][3],
              lineList[index - 1][0],
              lineList[index - 1][1],
              lineList[index - 1][2],
              lineList[index - 1][3]
            );
            if (flag) {
              coordinates.value.pop();
            }
          }
        });
      }
      if (coordinates.value.length > 19) {
        // $message.warning("每个区域最多可绘制20个点坐标");
        doubleclick();
        drawcircles();
        return;
      }
      drawcircle();
      isdraw.value = true; //正在画多边形
    }
  }
};
const canvasMove = (e) => {
  //没开始画或者结束画之后不进行操作
  ctx.value.strokeStyle = "rgba(24, 144, 255, 1)";
  if (coordinates.value.length == 0 || !isdraw.value || endtip.value) {
    return;
  }
  var x = e.offsetX;
  var y = e.offsetY;
  //获取上一个点
  var last_x = coordinates.value[coordinates.value.length - 1].cor_x;
  var last_y = coordinates.value[coordinates.value.length - 1].cor_y;
  ctx.value.clearRect(0, 0, canWidth.value, canheight.value); //清空画布
  drawline(); //把之前的点连线
  drawcircle(); //画之前的点
  if (all_coordinates.value.length != 0) {
    //不止一个多边形,把多边形们画出来
    drawlines();
    drawcircles();
    fillarea();
  }

  //获取鼠标移动时的点,画线,实现线段跟踪效果。
  ctx.value.beginPath();
  ctx.value.moveTo(last_x, last_y);
  ctx.value.lineTo(x, y); //追踪鼠标
  ctx.value.stroke(); //绘制已定义的路径 追踪鼠标
  ctx.value.closePath();
};

const canvasUp = (e) => {
  ctx.value.clearRect(0, 0, canWidth.value, canheight.value); //清空画布
  drawline(); //把之前的点连线
  drawcircle(); //画之前的点
  ctx.value.strokeStyle = "rgba(24, 144, 255, 1)"; //图形边框颜色
  if (all_coordinates.value.length != 0) {
    //不止一个多边形,把多边形们画出来
    drawlines();
    drawcircles();
    fillarea();
  }
};

//画线-把当前绘制的多边形之前的坐标线段绘制出来
const drawline = () => {
  for (var i = 0; i < coordinates.value.length - 1; i++) {
    ctx.value.beginPath();
    var x0 = coordinates.value[i].cor_x;
    var y0 = coordinates.value[i].cor_y;
    var x1 = coordinates.value[i + 1].cor_x;
    var y1 = coordinates.value[i + 1].cor_y;
    ctx.value.moveTo(x0, y0);
    ctx.value.lineTo(x1, y1);
    ctx.value.stroke();
    ctx.value.closePath();
  }
};
//画点-把当前绘制的多边形之前的端点画圆
const drawcircle = () => {
  ctx.value.fillStyle = "rgba(0, 133, 221, 1)";
  for (var i = 0; i < coordinates.value.length; i++) {
    var x = coordinates.value[i].cor_x;
    var y = coordinates.value[i].cor_y;
    ctx.value.beginPath(); //起始一条路径,或重置当前路径
    ctx.value.moveTo(x, y); //把路径移动到画布中的指定点(x,y)开始坐标
    ctx.value.arc(x, y, 5, 0, Math.PI * 2); //点
    ctx.value.fill(); //填充点
    ctx.value.closePath(); //创建从当前点回到起始点的路径
  }
};
// 线条

const drawlines = () => {

  //把所有多边形画出来

  for (var i = 0; i < all_coordinates.value.length; i++) {

    var cors = all_coordinates.value[i];

    //前后坐标连线

    for (var j = 0; j < cors.length - 1; j++) {
      ctx.value.beginPath();
      var x0 = cors[j].cor_x;
      var y0 = cors[j].cor_y;
      var y1 = cors[j + 1].cor_y;
      ctx.value.moveTo(x0, y0);
      ctx.value.lineTo(x1, y1);
      ctx.value.stroke();
      ctx.value.closePath();
    }

    //最后一个与第一个连线
    var begin_x = cors[0].cor_x;
    var begin_y = cors[0].cor_y;
    var end_x = cors[cors.length - 1].cor_x;
    var end_y = cors[cors.length - 1].cor_y;
    ctx.value.beginPath();
    ctx.value.moveTo(begin_x, begin_y);
    ctx.value.lineTo(end_x, end_y);
    ctx.value.stroke();
    ctx.value.closePath();
  }
};

const drawcircles = () => {
  //为所有的多边形端点画圆
  ctx.value.fillStyle = "rgba(24, 144, 255, 1)";
  for (var i = 0; i < all_coordinates.value.length; i++) {
    var cors = all_coordinates.value[i];
    for (var j = 0; j < cors.length; j++) {
      var x = cors[j].cor_x;
      var y = cors[j].cor_y;
      ctx.value.beginPath();
      ctx.value.moveTo(x, y);
      ctx.value.arc(x, y, 5, 0, Math.PI * 2);
      ctx.value.fill();
      ctx.value.closePath();
    }
  }
};
// 颜色填充

const fillarea = () => {
  ctx.value.fillStyle = "rgba(24, 144, 255, 0.30)";
  for (var i = 0; i < all_coordinates.value.length; i++) {
    var cors = all_coordinates.value[i];
    var x0 = cors[0].cor_x;
    var y0 = cors[0].cor_y;
    ctx.value.beginPath();
    ctx.value.moveTo(x0, y0);
    for (var j = 1; j < cors.length; j++) {
      var x = cors[j].cor_x;
      var y = cors[j].cor_y;
      ctx.value.lineTo(x, y);
    }
    ctx.value.fill();
    ctx.value.closePath();
  }
};
// 右击按钮时---------------------------------------
const doubleclick = () => {
  //双击画布,在最后一个点的时候双击,自动连线第一个点
  if (coordinates.value.length != 0 && coordinates.value.length >= 3) { //必须大于3个点位
    //连接起始点
    var x0 = coordinates.value[0].cor_x;
    var y0 = coordinates.value[0].cor_y;
    var x1 = coordinates.value[coordinates.value.length - 1].cor_x;
    var y1 = coordinates.value[coordinates.value.length - 1].cor_y;
    ctx.value.beginPath();
    ctx.value.moveTo(x0, y0);
    ctx.value.lineTo(x1, y1);
    ctx.value.stroke();
    ctx.value.closePath();
    isdraw.value = false;
    endtip.value = true;
    //每次的点添加到数组中
    all_coordinates.value.push(coordinates.value);
    
    // 设置坐标内部背景色透明
    ctx.value.fillStyle = "rgba(24, 144, 255, 0.30)";
    var bx = coordinates.value[0].cor_x;
    var by = coordinates.value[0].cor_y;
    ctx.value.moveTo(bx, by);
    for (var k = 1; k < coordinates.value.length; k++) {
      var x = coordinates.value[k].cor_x;
      var y = coordinates.value[k].cor_y;
      ctx.value.lineTo(x, y);
    }
    ctx.value.fill();
    ctx.value.closePath();
    all_coordinatesArr.value = calculatePolygonCoords(all_coordinates.value);
    const result = calculatePolygonCoords(all_coordinates.value).map((innerArray) =>
      innerArray.map((subArray) => subArray.map((value) => parseFloat(value)))
    );
    coordinates.value = [];
    isSwitch.value = false;
  }
};

// 计算多边形坐标  转我 归一化坐标  方便保存、传输
function calculatePolygonCoords(coords) {
  const result = coords.map((arr) =>
    arr.map((obj) => [
    //Decimal.js 插件  解决精度丢失问题  pnpm i decimal.js 
      new Decimal(obj.cor_x / canWidth.value).toFixed(5),
      new Decimal(obj.cor_y / canheight.value).toFixed(5),
    ])
  );
  return result;
 //格式
// "POLYGON((0.11336 0.19512, 0.15376 0.64165, 0.54433 0.18574))",
}
// 判断直线是否相交
const judgeIntersect = (x1, y1, x2, y2, x3, y3, x4, y4) => {
  if (
    !(
      Math.min(x1, x2) <= Math.max(x3, x4) &&
      Math.min(y3, y4) <= Math.max(y1, y2) &&
      Math.min(x3, x4) <= Math.max(x1, x2) &&
      Math.min(y1, y2) <= Math.max(y3, y4)
    )
  )
    return false;
  var u, v, w, z;
  u = (x3 - x1) * (y2 - y1) - (x2 - x1) * (y3 - y1);
  v = (x4 - x1) * (y2 - y1) - (x2 - x1) * (y4 - y1);
  w = (x1 - x3) * (y4 - y3) - (x4 - x3) * (y1 - y3);
  z = (x2 - x3) * (y4 - y3) - (x4 - x3) * (y2 - y3);
  return u * v <= 0.00000001 && w * z <= 0.00000001;
};
</script>
    

进行自适应、坐标计算

js 复制代码
const canWidth = ref(null);
const canheight = ref(null);
const screenWidth = ref("");

// 监听屏幕变化
const watchScreen = () => {
  screenWidth.value = document.body.clientWidth;
  window.onresize = () => {
    //屏幕尺寸变化就重新赋值
    return (() => {
      screenWidth.value = document.body.clientWidth;
    })();
  };
};
watch(
  () => screenWidth.value,
  (newVal, old) => {
    canvasGetWid();
  }
);

// 自适应方法
const canvasGetWid = () => {

  //初始化画布对象
  const canvas = document.querySelector("#mycanvas");   // canvas本身
  const fileDIV = document.querySelector("#fileDIV"); // 这是canvas父级盒子
  ctx.value = canvas.getContext("2d"); //getContext() 方法返回一个用于在画布上绘图的环境,2d二维绘图
  // 获取元素的内容宽高  不包括 外边距margin
  canvas.width = fileDIV.offsetWidth; 
  canvas.height = fileDIV.offsetHeight;
  // 记录画板的宽高
  canWidth.value = canvas.width;
  canheight.value = canvas.height;
  
  
  //  根据屏幕变化重新计算坐标
  if (all_coordinatesArr.value) {
    all_coordinates.value = switchChange(all_coordinatesArr.value);
  }
  canvasUp();

};
//计算坐标
const switchChange = (arr) => {
  const result = arr.map((subArr) => {
    return subArr.map((item) => {
        // 这里使用float转为小数类型
        // 再使用Int转为整数 因为计算后的坐标如果出现小数点会出现一些偏差 保证整数最好
      const x = parseInt(parseFloat(item[0]) * canWidth.value);
      const y = parseInt(parseFloat(item[1]) * canheight.value);
      return { cor_x: x, cor_y: y };
    });
  });

  return result;

};

视频播放

js 复制代码
//播放视频
const startVideo = (rtsp) => {
  let url = window.global.BASE_URL_PROD_URL;
  const canvas = document.querySelector("#mycanvas");
  loading.value = true;
   new JSMpeg.Player(
    "ws://localhost:9999/rtsp?url=" +
       btoa("rtsp://admin:1234qwer@192.168.3.000:554/Streaming/Channels/201"),
       {
         canvas: document.getElementById("canvas"),
      }
     );

    videoShow.value = false;
    loading.value = false;
  }, 1000);

};

这里使用的是rtsp视频流

前端如果要转码的花有两种方式

  1. 通过ffmpeg进行转码,再通过node转换为ws接口 前端页面再进行连接 一个实时流就需要一个窗口
  2. 使用rtsp2Web转码,是一个node插件 方便简洁
相关推荐
前端小趴菜0536 分钟前
React-React.memo-props比较机制
前端·javascript·react.js
摸鱼仙人~2 小时前
styled-components:现代React样式解决方案
前端·react.js·前端框架
sasaraku.2 小时前
serviceWorker缓存资源
前端
RadiumAg3 小时前
记一道有趣的面试题
前端·javascript
yangzhi_emo3 小时前
ES6笔记2
开发语言·前端·javascript
yanlele4 小时前
我用爬虫抓取了 25 年 5 月掘金热门面试文章
前端·javascript·面试
中微子5 小时前
React状态管理最佳实践
前端
烛阴5 小时前
void 0 的奥秘:解锁 JavaScript 中 undefined 的正确打开方式
前端·javascript
中微子5 小时前
JavaScript 事件与 React 合成事件完全指南:从入门到精通
前端
Hexene...5 小时前
【前端Vue】如何实现echarts图表根据父元素宽度自适应大小
前端·vue.js·echarts