// ==UserScript== // @name 标注校验插件 // @namespace http://tampermonkey.net/ // @version 8.3 // @description 【性能优化版】2D框遮挡校验 + 3D可见性校验 + 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': '右鱼眼' }; const VIEW_NAME_TO_CAM = Object.fromEntries(Object.entries(VIEW_NAMES).map(([k, v]) => [v, k])); let isAutoValidating = false; let lastFrameKey = ''; let autoEnabled = true; let panel = null; let panelReady = false; let latestErrors = []; let activeHighlightDom = null; let cachedCurFrameId = null; let cachedGetShowAttrs = null; let cachedGetClassName = null; let cachedGetClassId = null; let cachedAttrsDirection = false; const VALID_OCCLUSION_CODES = new Set(['10', '11', '12', '13']); // ========== Vue Store获取 ========== function getVueStore() { const el = document.getElementById('app') || document.querySelector('#app'); if (!el) return null; try { if (el.__vue_app__) { const gp = el.__vue_app__.config.globalProperties; if (gp && gp.$store) return gp.$store; } if (el.__vue__ && el.__vue__.$store) return el.__vue__.$store; } catch (e) { } return null; } function getRootVueInstance() { const el = document.querySelector('#app'); if (!el) return null; try { if (el.__vue_app__) return el.__vue_app__.instance?.proxy ?? null; if (el.__vue__) return el.__vue__; } catch (e) { } return null; } // ========== 获取当前帧ID(带缓存) ========== function getCurFrameId(marks) { if (cachedCurFrameId) return cachedCurFrameId; const store = getVueStore(); if (store?.state?.status?.curFrameId) { cachedCurFrameId = store.state.status.curFrameId; return cachedCurFrameId; } const frameIds = {}; for (const catKey in marks) { const category = marks[catKey]; if (!category || typeof category !== 'object') continue; for (const instKey in category) { const instance = category[instKey]; if (!instance?.['3d']) continue; for (const fid in instance['3d']) { frameIds[fid] = (frameIds[fid] || 0) + 1; } } } let maxCount = 0; let bestFid = null; for (const fid in frameIds) { if (frameIds[fid] > maxCount) { maxCount = frameIds[fid]; bestFid = fid; } } cachedCurFrameId = bestFid; return cachedCurFrameId; } // ========== 2D遮挡校验(优化循环,缓存getter) ========== function check2DOcclusion() { const store = getVueStore(); if (!store?.state?.marks) { return { errors: [], views: ['前鱼眼', '前广角', '后鱼眼', '左鱼眼', '右鱼眼'] }; } const marks = store.state.marks; if (!marks || typeof marks !== 'object') { return { errors: [], views: ['前鱼眼', '前广角', '后鱼眼', '左鱼眼', '右鱼眼'] }; } // 只初始化一次getter,不要循环内部重复调用 if (cachedGetShowAttrs === null) { try { cachedGetShowAttrs = store.getters['config/getShowAttrs']; } catch (e) { cachedGetShowAttrs = undefined; } } if (cachedGetClassName === null) { try { cachedGetClassName = store.getters['config/className']; } catch (e) { cachedGetClassName = undefined; } } if (cachedGetClassId === null) { try { cachedGetClassId = store.getters['marks/classId']; } catch (e) { cachedGetClassId = undefined; } } if (cachedAttrsDirection === false) { try { cachedAttrsDirection = store.state.status.attrsDirection; } catch (e) { } } const curFrameId = getCurFrameId(marks); if (!curFrameId) return { errors: [], views: [] }; const errors = []; const checkedViews = new Set(); for (const catKey in marks) { const category = marks[catKey]; if (!category || typeof category !== 'object') continue; for (const instKey in category) { const instance = category[instKey]; if (!instance || typeof instance !== 'object') continue; const d2 = instance['2d']; if (!d2 || typeof d2 !== 'object') continue; const frame2d = d2[curFrameId]; if (!frame2d) continue; const boxList = []; if (Array.isArray(frame2d)) { boxList.push(...frame2d); } else if (typeof frame2d === 'object' && frame2d !== null) { for (const k in frame2d) { const v = frame2d[k]; if (Array.isArray(v)) boxList.push(...v); else if (typeof v === 'object' && v) boxList.push(v); } } for (const box of boxList) { if (!box || typeof box !== 'object') continue; const camera = box.camera || ''; const viewName = VIEW_NAMES[camera] || camera || '未知视角'; checkedViews.add(viewName); let display_name = instance.class || instKey; if (cachedGetClassName) { try { let classId = cachedGetClassId ? cachedGetClassId(instKey) : null; if (!classId && instance.classId) classId = instance.classId; const toolType = box.type || instance.type || 'rect2d'; if (classId) { const cn = cachedGetClassName(classId, toolType); if (cn) display_name = cn; } } catch (e) { } } display_name = display_name + '-' + instKey; let attrs = box.attrs; if (!attrs) continue; if (typeof attrs === 'string') { try { attrs = JSON.parse(attrs); } catch (e) { continue; } } let displayText = null; if (cachedGetShowAttrs) { try { const classId = cachedGetClassId ? cachedGetClassId(instKey) : null; const boxType = box.originalType || box.type || instance.type || 'rect2d'; const result = cachedGetShowAttrs(boxType, classId, attrs, !cachedAttrsDirection); if (Array.isArray(result)) displayText = result.join('\n'); else if (typeof result === 'string') displayText = result; } catch (e) { // 静默跳过,减少控制台垃圾日志 } } if (displayText) { const 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 { let occlusion = Array.isArray(attrs.occlusion) ? attrs.occlusion[0] : attrs.occlusion; if (occlusion !== null && occlusion !== undefined && occlusion !== '') { const occlusionStr = String(occlusion); if (!VALID_OCCLUSION_CODES.has(occlusionStr)) { errors.push(`[2D] ${viewName} - ${display_name}:遮挡值"${occlusionStr}"`); } } } } } } return { errors, views: Array.from(checkedViews) }; } // ========== 3D可见性+遮挡校验(减少DOM重复查询) ========== function check3DVisibility() { const labels3D = document.querySelectorAll('.threejs-label'); const missing = []; const occlusionErrors = []; let count = 0; for (const label of labels3D) { const text = (label.textContent || '').trim(); if (!text) continue; count++; const firstLine = text.split('\n')[0].trim(); if (!text.includes('可见性')) missing.push(firstLine); const occMatch = text.match(/遮挡[::]\s*(.{1,30})/); if (occMatch) { let occValue = occMatch[1].trim(); occValue = occValue.split(/[\n_]|截断|运动|车门|种类|可见性/)[0].trim(); if (!occValue.includes('可见')) { occlusionErrors.push(`${firstLine}:遮挡显示为"${occValue}"`); } } } return { count, missing, occlusionErrors }; } // ========== 点击条目定位实例,修复点击无响应 ========== function parseErrorStr(errStr) { const reg = /^\[(2D|3D)\]\s*(.*?)(?:\s[‑‑]\s(.+?))[::]/; const match = errStr.match(reg); if (!match) return null; const type = match[1]; const viewName = match[2].trim(); const displayFull = match[3] ?? ''; const parts = displayFull.split(/[‑‑]/); const instKey = parts.length >= 2 ? parts[parts.length -1].trim() : null; return { type, viewName, instKey, raw: errStr }; } async function handleClickErrorItem(errStr) { const parsed = parseErrorStr(errStr); if (!parsed?.instKey) return; const {type, viewName, instKey} = parsed; const store = getVueStore(); if (!store) return; const marks = store.state.marks; let targetInstance = null; outer: for(const cat in marks){ const catItem = marks[cat]; for(const k in catItem){ if(k === instKey){ targetInstance = catItem[k]; break outer; } } } if(!targetInstance) return; const rootVm = getRootVueInstance(); let selectSuccess = false; if(rootVm){ const candidates = ['selectMark','selectInstance','setSelectedMark','onSelectMark']; for(const fnName of candidates){ if(rootVm[fnName] && typeof rootVm[fnName] === 'function'){ try{ rootVm[fnName](instKey); selectSuccess = true; break; }catch(e){} } if(rootVm.$children){ for(const child of rootVm.$children){ if(child[fnName] && typeof child[fnName] === 'function'){ try{ child[fnName](instKey); selectSuccess = true; break; }catch(e){} } } if(selectSuccess) break; } } } if(type === '2D' && VIEW_NAME_TO_CAM[viewName]){ const camId = VIEW_NAME_TO_CAM[viewName]; const rootVm2 = getRootVueInstance(); if(rootVm2){ const camFuncs = ['switchCamera','changeCamera','setCurrentCamera']; for(const fn of camFuncs){ if(rootVm2[fn]){ try{ rootVm2[fn](camId); break; }catch(e){} } } } } if(activeHighlightDom){ activeHighlightDom.classList.remove('chk-label-flash'); activeHighlightDom = null; } const allLabels = document.querySelectorAll('.threejs-label'); for(const label of allLabels){ if(label.textContent.includes(instKey)){ label.classList.add('chk-label-flash'); activeHighlightDom = label; break; } } } // ========== 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; } #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-title { font-weight: bold; font-size: 14px; color: #00d2ff; 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; } .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; margin-left: 6px; } .chk-status-badge { display: none; align-items: center; gap:4px; padding:2px 8px; border-radius:8px; font-size:11px; font-weight:bold; margin-left:8px; white-space:nowrap; } .chk-status-badge.chk-status-validating { display:flex; background:rgba(255,193,7,0.15); color:#ffc107; border:1px solid rgba(255,193,7,0.3); } .chk-status-badge.chk-status-done { display:flex; background:rgba(78,204,163,0.15); color:#4ecca3; border:1px solid rgba(78,204,163,0.3); } .chk-status-badge.chk-status-error { display:flex; background:rgba(233,69,96,0.15); color:#e94560; border:1px solid rgba(233,69,96,0.3); } #chk-content { padding:12px 14px; max-height: calc(70vh - 50px); overflow-y:auto; } #chk-content::-webkit-scrollbar { width:6px; } #chk-content::-webkit-scrollbar-thumb { background:rgba(0,210,255,0.3); border-radius:3px; } .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; } .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; cursor:pointer; } .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; } @keyframes chk-label-flash-anim { 0%,100% {outline:3px solid #ff4444; outline-offset:2px;} 50% {outline:3px solid transparent;} } .chk-label-flash { animation:chk-label-flash-anim 0.6s ease-in-out infinite; } `; document.head.appendChild(style); } // ========== 创建面板 ========== function createPanel() { if (panel && document.body.contains(panel)) return panel; injectStyles(); panel = document.createElement('div'); panel.id = 'chk-panel'; panel.innerHTML = `