// ==UserScript== // @name 标注校验插件 // @namespace http://tampermonkey.net/ // @version 7.1 // @description 2D框遮挡校验(Canvas文字) + 3D框可见性校验 + 切帧自动检测 // @match http://121.37.95.217:8080/pointcloud/* // @grant none // @run-at document-start // ==/UserScript== (function () { 'use strict'; const VIEW_NAMES = { 'front_fisheye': '前鱼眼', 'front_wide': '前广角', 'back_fisheye': '后鱼眼', 'left_fisheye': '左鱼眼', 'right_fisheye': '右鱼眼' }; let isAutoValidating = false; let lastFrameKey = ''; let autoEnabled = true; let panel = null; let panelReady = false; // ========== 1. Canvas文字拦截(保留hook但已知无效) ========== let allCanvasTexts = []; let hookInstalled = false; function installCanvasHook() { if (hookInstalled) return; hookInstalled = true; const proto = CanvasRenderingContext2D.prototype; const origFillText = proto.fillText; const origStrokeText = proto.strokeText; proto.fillText = function (text, x, y, maxWidth) { try { if (text && typeof text === 'string' && text.length > 0) { let canvas = this.canvas; let camId = ''; if (canvas) { camId = canvas.getAttribute('data-value') || canvas.dataset.value || ''; if (!camId) { let parent = canvas.parentElement; let depth = 0; while (parent && !camId && depth < 5) { camId = parent.getAttribute('data-value') || parent.dataset.value || ''; parent = parent.parentElement; depth++; } } } allCanvasTexts.push({ text: text, x: Math.round(x), y: Math.round(y), camId: camId || 'unknown' }); } } catch (e) { } return origFillText.apply(this, arguments); }; proto.strokeText = function (text, x, y, maxWidth) { try { if (text && typeof text === 'string' && text.length > 0) { let canvas = this.canvas; let camId = ''; if (canvas) { camId = canvas.getAttribute('data-value') || canvas.dataset.value || ''; if (!camId) { let parent = canvas.parentElement; let depth = 0; while (parent && !camId && depth < 5) { camId = parent.getAttribute('data-value') || parent.dataset.value || ''; parent = parent.parentElement; depth++; } } } allCanvasTexts.push({ text: text, x: Math.round(x), y: Math.round(y), camId: camId || 'unknown' }); } } catch (e) { } return origStrokeText.apply(this, arguments); }; } installCanvasHook(); function clearCanvasText() { allCanvasTexts = []; } // ========== 2. Vue Store获取 ========== function getVueStore() { let el = document.getElementById('app') || document.querySelector('#app'); if (!el) return null; if (el.__vue_app__) { try { let gp = el.__vue_app__.config.globalProperties; if (gp && gp.$store) return gp.$store; } catch (e) { } } if (el.__vue__) { try { if (el.__vue__.$store) return el.__vue__.$store; } catch (e) { } } return null; } const VALID_OCCLUSION_CODES = new Set(['10', '11', '12', '13']); // ========== 2b. 2D遮挡校验(通过平台getShowAttrs获取显示文字) ========== function check2DOcclusion() { let store = getVueStore(); if (!store || !store.state || !store.state.marks) { return { errors: [], views: ['前鱼眼', '前广角', '后鱼眼', '左鱼眼', '右鱼眼'] }; } let marks = store.state.marks; if (!marks || typeof marks !== 'object') { return { errors: [], views: ['前鱼眼', '前广角', '后鱼眼', '左鱼眼', '右鱼眼'] }; } // 获取平台自己的getShowAttrs getter let getShowAttrs = null; try { getShowAttrs = store.getters['config/getShowAttrs']; } catch (e) { } // 获取className getter(获取中文类名) let getClassName = null; try { getClassName = store.getters['config/className']; } catch (e) { } // 获取classId getter let getClassId = null; try { getClassId = store.getters['marks/classId']; } catch (e) { } // 获取attrsDirection let attrsDirection = false; try { attrsDirection = store.state.status.attrsDirection; } catch (e) { } // 找当前帧ID let curFrameId = null; try { curFrameId = store.state.status.curFrameId; } catch (e) { } if (!curFrameId) { // fallback: 找出现次数最多的帧ID let frameIds = {}; for (let catKey in marks) { let category = marks[catKey]; if (!category || typeof category !== 'object') continue; for (let instKey in category) { let instance = category[instKey]; if (!instance || typeof instance !== 'object') continue; if (instance['3d'] && typeof instance['3d'] === 'object') { for (let fid in instance['3d']) { frameIds[fid] = (frameIds[fid] || 0) + 1; } } } } let maxCount = 0; for (let fid in frameIds) { if (frameIds[fid] > maxCount) { maxCount = frameIds[fid]; curFrameId = fid; } } } if (!curFrameId) { return { errors: [], views: ['前鱼眼', '前广角', '后鱼眼', '左鱼眼', '右鱼眼'] }; } let errors = []; let checkedViews = new Set(); for (let catKey in marks) { let category = marks[catKey]; if (!category || typeof category !== 'object') continue; for (let instKey in category) { let instance = category[instKey]; if (!instance || typeof instance !== 'object') continue; let d2 = instance['2d']; if (!d2 || typeof d2 !== 'object') continue; let frame2d = d2[curFrameId]; if (!frame2d) continue; let boxList = []; if (Array.isArray(frame2d)) { boxList = frame2d; } else if (typeof frame2d === 'object' && frame2d !== null) { for (let k in frame2d) { let v = frame2d[k]; if (Array.isArray(v)) boxList = boxList.concat(v); else if (typeof v === 'object' && v !== null) boxList.push(v); } } for (let i = 0; i < boxList.length; i++) { let box = boxList[i]; if (!box || typeof box !== 'object') continue; let camera = box.camera || ''; let viewName = VIEW_NAMES[camera] || camera || '未知视角'; checkedViews.add(viewName); let class_name = instance.class || instKey; // 尝试获取中文类名 let display_name = class_name + '-' + instKey; if (getClassName) { try { let classId = null; if (getClassId) classId = getClassId(instKey); if (!classId && instance.classId) classId = instance.classId; let toolType = box.type || instance.type || 'rect2d'; if (classId) { let cn = getClassName(classId, toolType); if (cn) display_name = cn + '-' + instKey; } } catch (e) { } } let attrs = box.attrs; if (!attrs) continue; if (typeof attrs === 'string') { try { attrs = JSON.parse(attrs); } catch (e) { } } // 用平台的getShowAttrs获取显示文字 let displayText = null; if (getShowAttrs) { try { let classId = null; if (getClassId) { classId = getClassId(instKey); } let boxType = box.originalType || box.type || instance.type || 'rect2d'; let result = getShowAttrs(boxType, classId, attrs, !attrsDirection); if (Array.isArray(result)) { displayText = result.join('\n'); } else if (typeof result === 'string') { displayText = result; } } catch (e) { console.log('[校验插件] getShowAttrs调用失败:', e.message, 'instKey:', instKey); } } if (displayText) { // 检查显示文字中"遮挡:"后面是数字还是汉字 let occMatch = displayText.match(/遮挡[::]\s*(.{1,30})/); if (occMatch) { let occValue = occMatch[1].trim(); occValue = occValue.split(/截断|运动|车门|可见性/)[0].trim(); // 如果遮挡值以数字开头且不含"可见" → 报错 if (!occValue.includes('可') && /^\d/.test(occValue)) { errors.push(`[2D] ${viewName} - ${display_name}:遮挡显示为"${occValue}"`); } } } else { // fallback: 直接检查attrs里的遮挡编码 let occlusion = null; if (attrs.occlusion) { occlusion = Array.isArray(attrs.occlusion) ? attrs.occlusion[0] : attrs.occlusion; } if (occlusion !== null && occlusion !== undefined && occlusion !== '') { let occlusionStr = String(occlusion); if (!VALID_OCCLUSION_CODES.has(occlusionStr)) { errors.push(`[2D] ${viewName} - ${display_name}:遮挡值"${occlusionStr}"`); } } } } } } return { errors, views: Array.from(checkedViews) }; } // ========== 3. 3D可见性校验 ========== function check3DVisibility() { let labels3D = document.querySelectorAll('.threejs-label'); let missing = []; let count = 0; labels3D.forEach(label => { let text = (label.textContent || '').trim(); if (text.length === 0) return; if (!text.includes(':') && !text.includes(':')) return; count++; if (!text.includes('可见性')) { missing.push(text.split('\n')[0].trim()); } }); return { count, missing }; } // ========== 4. 注入CSS ========== function injectStyles() { if (document.getElementById('chk-styles')) return; const style = document.createElement('style'); style.id = 'chk-styles'; style.textContent = ` #chk-panel { position: fixed; top: 80px; left: 320px; width: 340px; max-height: 70vh; background: linear-gradient(145deg, #1a1a2e 0%, #16213e 100%); color: #e0e0e0; border: 1px solid rgba(0,210,255,0.2); border-radius: 12px; z-index: 999999; font-family: "Microsoft YaHei", "PingFang SC", sans-serif; font-size: 13px; box-shadow: 0 8px 32px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.05); overflow: hidden; transition: box-shadow 0.2s; } #chk-panel:hover { box-shadow: 0 8px 32px rgba(0,0,0,0.6), 0 0 20px rgba(0,210,255,0.15); } #chk-header { background: linear-gradient(135deg, #0f3460 0%, #1a1a40 100%); padding: 10px 14px; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; border-bottom: 1px solid rgba(0,210,255,0.15); } #chk-header:active { cursor: grabbing; } .chk-title { font-weight: bold; font-size: 14px; color: #00d2ff; text-shadow: 0 0 10px rgba(0,210,255,0.3); display: flex; align-items: center; gap: 6px; } .chk-title-dot { width: 8px; height: 8px; background: #00d2ff; border-radius: 50%; box-shadow: 0 0 8px #00d2ff; animation: chk-pulse 2s ease-in-out infinite; } @keyframes chk-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } .chk-btn-check { background: linear-gradient(135deg, #00d2ff 0%, #0288d1 100%); color: #fff; border: none; padding: 5px 14px; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: bold; transition: all 0.2s; box-shadow: 0 2px 8px rgba(0,210,255,0.3); } .chk-btn-check:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,210,255,0.5); } .chk-btn-check:active { transform: translateY(0); } .chk-btn-check:disabled { opacity: 0.6; cursor: not-allowed; transform: none; } .chk-btn-toggle { background: rgba(255,255,255,0.08); color: #aaa; border: 1px solid rgba(255,255,255,0.1); width: 26px; height: 26px; border-radius: 6px; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; margin-left: 6px; } .chk-btn-toggle:hover { background: rgba(255,255,255,0.15); color: #fff; } #chk-content { padding: 12px 14px; max-height: calc(70vh - 50px); overflow-y: auto; } #chk-content::-webkit-scrollbar { width: 6px; } #chk-content::-webkit-scrollbar-track { background: transparent; } #chk-content::-webkit-scrollbar-thumb { background: rgba(0,210,255,0.3); border-radius: 3px; } #chk-content::-webkit-scrollbar-thumb:hover { background: rgba(0,210,255,0.5); } .chk-stats { margin-bottom: 10px; padding: 10px 12px; background: rgba(15,52,96,0.4); border-radius: 8px; border: 1px solid rgba(255,255,255,0.05); } .chk-stats-title { color: #00d2ff; font-weight: bold; margin-bottom: 6px; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; } .chk-stats-body { color: #8892b0; font-size: 12px; line-height: 1.8; } .chk-badge { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 11px; font-weight: bold; } .chk-badge-ok { background: rgba(78,204,163,0.15); color: #4ecca3; border: 1px solid rgba(78,204,163,0.3); } .chk-badge-err { background: rgba(233,69,96,0.15); color: #e94560; border: 1px solid rgba(233,69,96,0.3); } .chk-pass { color: #4ecca3; text-align: center; padding: 24px 0; font-size: 15px; font-weight: bold; } .chk-warn-header { color: #e94560; font-weight: bold; margin: 10px 0 6px; font-size: 13px; display: flex; align-items: center; gap: 4px; } .chk-warn-item { background: rgba(233,69,96,0.08); border-left: 3px solid #e94560; padding: 8px 10px; margin-bottom: 5px; border-radius: 0 6px 6px 0; font-size: 13px; line-height: 1.6; color: #ccc; transition: background 0.2s; } .chk-warn-item:hover { background: rgba(233,69,96,0.15); } .chk-loading { color: #8892b0; text-align: center; padding: 24px 0; } @keyframes chk-shake { 0%, 100% { transform: translateX(0); } 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } 20%, 40%, 60%, 80% { transform: translateX(5px); } } .chk-shake { animation: chk-shake 0.5s ease-in-out; } `; document.head.appendChild(style); } // ========== 5. 创建面板 ========== function createPanel() { if (panel && document.body.contains(panel)) return panel; injectStyles(); panel = document.createElement('div'); panel.id = 'chk-panel'; panel.innerHTML = `