// ==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 = `
标注校验
等待标注数据加载...
`; document.body.appendChild(panel); const contentDom = document.getElementById('chk-content'); // 点击事件委托 contentDom.addEventListener('click',(e)=>{ const targetItem = e.target.closest('.chk-warn-item'); if(!targetItem) return; const idx = Number(targetItem.dataset.errIdx); if(Number.isNaN(idx)) return; const errStr = latestErrors[idx]; if(!errStr) return; handleClickErrorItem(errStr); }); document.getElementById('chk-btn-check').addEventListener('click', ()=>validate(false)); document.getElementById('chk-btn-toggle').addEventListener('click', togglePanel); makeDraggable(); panelReady = true; return panel; } function makeDraggable() { const header = document.getElementById('chk-header'); let isDragging = false, startX, startY, startLeft, startTop; header.addEventListener('mousedown', function (e) { if (e.target.tagName === 'BUTTON') return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; e.preventDefault(); }); document.addEventListener('mousemove', function (e) { if (!isDragging) return; let newLeft = startLeft + (e.clientX - startX); let newTop = startTop + (e.clientY - startY); newLeft = Math.max(-260, Math.min(newLeft, window.innerWidth - 80)); newTop = Math.max(0, Math.min(newTop, window.innerHeight - 50)); panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px'; }); document.addEventListener('mouseup', ()=> isDragging = false); } function togglePanel() { const content = document.getElementById('chk-content'); const btn = document.getElementById('chk-btn-toggle'); if (content.style.display === 'none') { content.style.display = ''; btn.innerHTML = '−'; } else { content.style.display = 'none'; btn.innerHTML = '+'; } } function renderResults(errors, info, isAuto) { latestErrors = [...errors]; if (!panelReady) createPanel(); const content = document.getElementById('chk-content'); const btn = document.getElementById('chk-btn-check'); btn.textContent = '校验当前帧'; btn.disabled = false; const MAX_RENDER_ERR = 150; const hasErrors = errors.length > 0; let html = ''; const statsBorder = hasErrors ? 'rgba(233,69,96,0.3)' : 'rgba(78,204,163,0.3)'; const statsColor = hasErrors ? '#e94560' : '#4ecca3'; let statusBadge = hasErrors ? '有异常' : '通过'; if (isAuto) statusBadge += ' 自动'; html += `
校验结果
状态: ${statusBadge}
${info.join('
')}
`; if (hasErrors) { html += `
⚠ 发现问题(${errors.length}条)
`; const showList = errors.slice(0, MAX_RENDER_ERR); for (let i = 0; i < showList.length; i++) { html += `
${showList[i]}
`; } if(errors.length > MAX_RENDER_ERR){ html += `
仅展示前${MAX_RENDER_ERR}条,总错误${errors.length}条
`; } } else { html += `
✓ 校验通过,无问题
`; } content.innerHTML = html; const statusBadgeEl = document.getElementById('chk-status-badge'); if (statusBadgeEl) { statusBadgeEl.className = hasErrors ? 'chk-status-badge chk-status-error' : 'chk-status-badge chk-status-done'; statusBadgeEl.textContent = '校验已完成'; } if (hasErrors) { panel.classList.remove('chk-shake'); void panel.offsetWidth; panel.classList.add('chk-shake'); } } function validate(isAuto) { // 帧不变直接跳过2D校验,刷新缓存 const store = getVueStore(); const newFrame = store?.state?.status?.curFrameId; if(newFrame && newFrame !== cachedCurFrameId){ cachedCurFrameId = newFrame; // 帧变化时清空getter缓存 cachedGetShowAttrs = null; cachedGetClassName = null; cachedGetClassId = null; } if (!panelReady) createPanel(); const btn = document.getElementById('chk-btn-check'); btn.textContent = '校验中...'; btn.disabled = true; const statusBadge = document.getElementById('chk-status-badge'); if(statusBadge){ statusBadge.className = 'chk-status-badge chk-status-validating'; statusBadge.textContent = '校验中'; } const content = document.getElementById('chk-content'); content.innerHTML = `
校验结果
状态: 校验中
🔍 正在校验...
2D遮挡 + 3D可见性 + 3D遮挡
`; // 用requestIdleCallback,浏览器空闲再跑计算,不阻塞UI const run = ()=>{ const errors = []; const info = []; const result2D = check2DOcclusion(); if (result2D.views.length > 0) { info.push(`2D遮挡校验(${result2D.views.join('/')})`); } errors.push(...result2D.errors); const result3D = check3DVisibility(); info.push(`3D可见性+遮挡校验(共${result3D.count}个)`); for(const item of result3D.missing) errors.push(`[3D] ${item}:缺少"可见性"字段`); for(const item of result3D.occlusionErrors) errors.push(`[3D] ${item}`); renderResults(errors, info, isAuto); }; if(window.requestIdleCallback){ requestIdleCallback(run, {timeout:150}); }else{ setTimeout(run, 30); } } // ========== 自动校验,优化防抖,移除多余轮询 ========== let debounceTimer = null; function getFrameKey() { const labels = document.querySelectorAll('.threejs-label'); if (labels.length === 0) return ''; const parts = []; for (let i = 0; i < Math.min(labels.length, 5); i++) { parts.push((labels[i].textContent || '').substring(0, 50)); } return labels.length + '|' + parts.join('||'); } function doAutoValidate() { if (isAutoValidating || !autoEnabled) return; const frameKey = getFrameKey(); if (!frameKey || frameKey === lastFrameKey) return; lastFrameKey = frameKey; isAutoValidating = true; // 帧切换,清空缓存 cachedCurFrameId = null; cachedGetShowAttrs = null; cachedGetClassName = null; cachedGetClassId = null; setTimeout(()=>{ try { validate(true); } catch (e) { console.log('[校验插件自动校验]', e); } setTimeout(()=>{ isAutoValidating = false; }, 80); }, 250); } const observer = new MutationObserver(()=>{ if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(doAutoValidate, 350); }); function startObserver() { const target = document.getElementById('threejs-label') || document.body; observer.observe(target, { childList: true, subtree: true, characterData: true }); } function init() { let attempts = 0; const interval = setInterval(()=>{ attempts++; if (document.getElementById('app') || attempts > 60) { clearInterval(interval); setTimeout(()=>{ createPanel(); startObserver(); console.log('[校验插件V8.3 性能优化版]已加载'); setTimeout(doAutoValidate, 1200); }, 500); } }, 500); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } document.addEventListener('keydown', function (e) { if (e.ctrlKey && e.shiftKey && e.key === 'V') { e.preventDefault(); doAutoValidate(); } }); window.__validate = validate; window.__dump2D = function () { const store = getVueStore(); if (!store?.state?.marks) { console.log('无Vue Store'); return; } const marks = store.state.marks; const frameIds = {}; for (const catKey in marks) { const category = marks[catKey]; for (const instKey in category) { const inst = category[instKey]; if (inst?.['3d']) for (const fid in inst['3d']) frameIds[fid] = (frameIds[fid] || 0) + 1; } } const curFrame = Object.keys(frameIds).sort((a,b)=>frameIds[b]-frameIds[a])[0]; console.log('当前帧:', curFrame); const results = []; for (const catKey in marks) { const category = marks[catKey]; for (const instKey in category) { const inst = category[instKey]; if (!inst?.['2d']) continue; const d2 = inst['2d']; const frame2d = d2[curFrame]; if (!frame2d) continue; const boxList = []; if (Array.isArray(frame2d)) boxList.push(...frame2d); else 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) continue; const cam = box.camera || '?'; const viewName = VIEW_NAMES[cam] || cam; let attrs = box.attrs; if(typeof attrs === 'string') try{attrs=JSON.parse(attrs);}catch(e){} let occ = attrs?.occlusion ?? '无occlusion字段'; if(Array.isArray(occ)) occ = occ[0]; results.push({view:viewName, inst:instKey, occlusion:occ}); } } } console.table(results); }; })();