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插件 方便简洁
相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax