|
|
|
<!--
|
|
|
|
标注组件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>
|