数字孪生Web 后台dt( digital twin)2.0版本 统一命名格式
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

380 lines
11 KiB

<!--
标注组件1.0
目录位置AI智能化区域监控 -> 视频智能监控范围 -> 标注区域
功能概述引入轻量级图片2d标注组件canvas-select,通过添加透明图层,与rtsp视频流结合,实现视频监控区域标注
-->
<template>
<a-modal
:keyboard="false"
:maskClosable="false"
:forceRender="true"
:visible="visible"
:title="title"
width="1280px"
@cancel="handleCancel"
destroyOnClose
style="top: 40px !important"
>
<a-row class="video_area">
<canvas ref="canvas" class="container"></canvas>
<div class="mpegPlayer" ref="mpegPlayer"></div>
</a-row>
<a-row class="operate_area">
<div>
<a-space :size="12">
<a-button shape="round" size="middle" @click="change(0)"> <ApiOutlined /> 退出创建</a-button>
<a-button type="primary" shape="circle" size="large" @click="change(1)" title="创建矩形">
<img class="icon_style" src="./assets/rect_icon.png" />
</a-button>
<a-button type="primary" shape="circle" size="large" @click="change(2)" title="创建多边形">
<img class="icon_style" src="./assets/polo_icon.png" />
</a-button>
<a-button type="primary" shape="circle" size="large" @click="change(3)" title="创建标记点">
<img class="icon_style" src="./assets/point_icon.png" />
</a-button>
<a-button type="primary" shape="circle" size="large" @click="change(4)" title="创建线">
<img class="icon_style" src="./assets/line_icon.png" />
</a-button>
<a-button type="primary" shape="circle" size="large" @click="change(5)" title="创建圆">
<img class="icon_style" src="./assets/circle_icon.png" />
</a-button>
<a-button type="primary" shape="round" size="middle" @click="onFocus()"><AimOutlined />专注模式</a-button>
<a-button type="primary" shape="round" size="middle" @click="lock()"><LinkOutlined />锁定/解锁</a-button>
<a-button type="primary" shape="round" danger size="middle" @click="clear()"> <FormatPainterOutlined />清除标注 </a-button>
</a-space>
</div>
<div>
<a-card size="small" :bordered="false" style="width: 200px; height: 80px">
<p>1.ESC键回退节点</p>
<p>2.BackSpace键删除图形</p>
</a-card>
</div>
</a-row>
<template #footer="">
<a-popconfirm title="是否保存标注?" ok-text="确认" cancel-text="取消" @confirm="handleOk()" @cancel="handleCancel">
<a-button type="primary">保存</a-button>
</a-popconfirm>
</template>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, getCurrentInstance, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { ApiOutlined, FormatPainterOutlined, AimOutlined, LinkOutlined } from '@ant-design/icons-vue';
const { proxy }: any = getCurrentInstance();
const props = defineProps({
title: {
type: String,
default: '区域标注',
},
visible: {
type: Boolean,
default: true,
},
// rtsp视频流地址
videoUrl: {
type: String,
default: '',
},
// 绘制数据——回显
labelData: {
type: String,
default: '',
},
// 点位数据——传递
locationData: {
type: String,
default: '',
},
});
const emit = defineEmits(['update:label-data', 'update:location-data']);
/* 播放rtsp视频相关 */
import MpegPlayer from 'jsmpeg-player';
const { ipcRenderer } = require('electron');
// DOM承载实例
const mpegPlayer = ref();
// 参数配置对象
const playOption = ref({
decodeFirstFrame: true,
});
// 消息
const msg = ref('');
// 播放容器
let player: any = ref(null);
// 开启播放
const open = () => {
const res = ipcRenderer.sendSync('openRtsp', props.videoUrl);
if (res.code === 200) {
player.value = new MpegPlayer.VideoElement(mpegPlayer.value, res.ws, playOption);
}
msg.value = res.msg;
};
// 关闭播放
const close = () => {
const res = ipcRenderer.sendSync('closeRtsp', props.videoUrl);
msg.value = res.msg;
};
/* 标注区域相关 */
// 导入2d绘图插件canvas-Select
import CanvasSelect from 'canvas-select';
// canvas实例
const canvas = ref(null);
// 导入透明静态图片
const hideUrl = new URL('./assets/hide.png', import.meta.url).href;
// 标注实例
let instance: any = ref(null);
// 图形标注列表
const option: any = ref([]);
// 选择绘制工具
function change(num: any) {
instance.createType = num;
}
// 清除标注数据
function clear() {
instance.setData([]);
}
// 专注模式
function onFocus() {
instance.setFocusMode(!instance.focusMode);
}
// 锁定/解锁画布
function lock() {
instance.lock = !instance.lock;
}
// 保存数据
function save() {
/* 保存绘制数据,用于回显 */
// 清除所有激活状态
instance.dataset.forEach((item) => {
item.active = false;
});
// 更新标注数据到表单
emit('update:label-data', JSON.stringify(instance.dataset));
/* 保存区域界限节点数据,用于后端分析 */
// 二次处理
const newData = instance.dataset.map((item) => {
let typeString;
switch (item.type) {
case 1:
typeString = 'rect';
break;
case 2:
typeString = 'polygon';
break;
case 3:
typeString = 'dot';
break;
case 4:
typeString = 'line';
break;
case 5:
typeString = 'circle';
break;
default:
typeString = '';
}
// 新的图形数据格式 - 拼接标注数据,传递后端
const newShape: any = {
type: typeString,
coor: item.coor,
};
if (item.type === 5) {
// 如果type为5(circle),则添加radius属性
newShape.radius = item.radius;
}
return newShape;
});
// 更新节点数据到表单
emit('update:location-data', JSON.stringify(newData));
}
// 删除选中图形 backspace / 回退绘制节点 esc
function changeSelect(event) {
if (event.keyCode == 8) {
// 删除选中图形
instance.dataset = instance.dataset.filter((item) => !item.active);
// 更新视图
instance.update();
} else if (event.keyCode == 27) {
// 是否节点已删除完毕
let delete_status = 0;
// 回退绘制节点
instance.dataset.forEach((item) => {
if (item.active === true && item.creating === true) {
// 矩形
if (item.type == 1) {
if (item.coor.length > 1) {
// 回退
item.coor = [];
// 删除完毕
delete_status = 1;
}
}
// 圆形
else if (item.type == 5) {
if (item.coor.length > 1) {
// 回退
item.coor = [];
item.radius = 0;
// 删除完毕
delete_status = 1;
}
}
// 其他图形
else {
if (item.coor.length > 1) {
// 回退上一个节点
item.coor.pop();
} else {
// 删除完毕
delete_status = 1;
}
}
// 更新视图
instance.update();
}
});
if (delete_status === 1) {
// 删除残余数据
instance.dataset = instance.dataset.filter((item) => !item.active);
// 更新视图
instance.update();
}
}
}
onMounted(() => {
// 播放视频
open();
// 添加键盘事件监听
document.addEventListener('keydown', changeSelect);
// 载入绘图数据
if (props.labelData == '') {
option.value = [];
} else {
option.value = JSON.parse(props.labelData);
}
// 创建实例
instance = new CanvasSelect('.container', hideUrl);
// 标签最大长度
instance.labelMaxLen = 10;
// 禁用滚动缩放
instance.scrollZoom = false;
// 图形数据赋值到实例
instance.setData(option.value);
// 形状边线宽度
instance.lineWidth = 2;
// 标签字体
instance.labelFont = '12px Arial';
// 标签填充颜色
instance.labelFillStyle = '#fa4545';
// 标签文字颜色
instance.textFillStyle = '#fff';
// 控制点半径
instance.ctrlRadius = 4;
// 图片加载完成
instance.on('load', (src: any) => {
// console.log('image load', src);
});
// 添加
instance.on('add', (info: any) => {
// 添加默认标签
switch (instance.createType) {
case 1:
info.label = '矩形防区';
break;
case 2:
info.label = '多边形防区';
break;
case 3:
info.label = '标记点位';
break;
case 4:
info.label = '报警界线';
break;
case 5:
info.label = '圆形防区';
break;
default:
info.label = '未定义';
}
// 更新画布
instance.update();
});
// 删除 - 弃用内置删除方法
// instance.on('delete', (info: any) => {
// console.log('删除', info);
// });
// 选中
instance.on('select', (shape: any) => {
// console.log('选中', shape);
});
// 更新
instance.on('updated', (result: any) => {
// console.log('更新', result);
});
});
// 在组件卸载时移除事件监听
// 考虑到你想要的是在页面卸载前移除事件监听,因此这里使用了`beforeUnmount`
// 如果你希望在页面卸载后再移除事件监听,可以使用`onUnmounted`
onBeforeUnmount(() => {
document.removeEventListener('keydown', changeSelect);
});
// 确定
async function handleOk() {
// 保存提交
save();
// 关闭窗口
handleCancel();
}
// 取消
function handleCancel() {
nextTick(() => {
// 关闭窗口
proxy.$parent.videoVisible = false;
// 关闭播放
close();
});
}
</script>
<style lang="less" scoped>
.video_area {
width: 100%;
height: 720px;
margin: 0 auto;
position: relative;
background: #000;
.container {
width: 1280px;
height: 100%;
position: absolute;
z-index: 9999;
left: 0;
top: 0;
}
.mpegPlayer {
width: 1280px;
height: 100%;
background: #000;
}
}
.operate_area {
width: 100%;
display: flex;
align-items: center;
justify-content: space-evenly;
.icon_style {
width: 24px;
height: 24px;
margin: 0 auto;
}
}
</style>