// ==UserScript== // @name 手势归组校验工具 - 分组检查 // @namespace http://113.207.49.90/ // @version 1.3.0 // @description 在标注平台审核/质检工序中自动校验半身框/手势框分组问题 // @author 粟镇 // @match *://*/w/task.html* // @match *://*/w/task.html* // @grant none // @run-at document-start // ==/UserScript== (function () { 'use strict'; // ============================================================ // 配置区 // ============================================================ const CHECK_CLASSES = ['half_bbox_xywh', 'handbboxxywh']; let cachedData = null; let panel = null; let panelReady = false; // ============================================================ // 拦截平台的 get-mark-data 请求 // ============================================================ function hookXHR() { const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this._szUrl = url; return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { const self = this; const origOnReady = this.onreadystatechange; this.onreadystatechange = function () { if (self.readyState === 4 && self.status === 200) { if (self._szUrl && self._szUrl.indexOf('get-mark-data') !== -1) { try { const resp = JSON.parse(self.responseText); if (resp.code === 0 && resp.data) { cachedData = resp.data; if (panelReady) { autoRunCheck(); } } } catch (e) {} } } if (origOnReady) { return origOnReady.apply(this, arguments); } }; return origSend.apply(this, arguments); }; } // ============================================================ // 注入CSS // ============================================================ function injectStyles() { if (document.getElementById('sz-check-styles')) return; const style = document.createElement('style'); style.id = 'sz-check-styles'; style.textContent = ` #sz-check-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(233,69,96,0.3); 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; } #sz-check-panel:hover { box-shadow: 0 8px 32px rgba(0,0,0,0.6), 0 0 20px rgba(233,69,96,0.15); } #sz-check-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(233,69,96,0.2); } #sz-check-header:active { cursor: grabbing; } .sz-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; } .sz-title-dot { width: 8px; height: 8px; background: #00d2ff; border-radius: 50%; box-shadow: 0 0 8px #00d2ff; animation: sz-pulse 2s ease-in-out infinite; } @keyframes sz-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } .sz-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); } .sz-btn-check:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,210,255,0.5); } .sz-btn-check:active { transform: translateY(0); } .sz-btn-check:disabled { opacity: 0.6; cursor: not-allowed; transform: none; } .sz-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; } .sz-btn-toggle:hover { background: rgba(255,255,255,0.15); color: #fff; } #sz-check-content { padding: 12px 14px; max-height: calc(70vh - 50px); overflow-y: auto; } #sz-check-content::-webkit-scrollbar { width: 6px; } #sz-check-content::-webkit-scrollbar-track { background: transparent; } #sz-check-content::-webkit-scrollbar-thumb { background: rgba(233,69,96,0.3); border-radius: 3px; } #sz-check-content::-webkit-scrollbar-thumb:hover { background: rgba(233,69,96,0.5); } .sz-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); } .sz-stats-title { color: #e94560; font-weight: bold; margin-bottom: 6px; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; } .sz-stats-body { color: #8892b0; font-size: 12px; line-height: 1.8; } .sz-badge { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 11px; font-weight: bold; } .sz-badge-ok { background: rgba(78,204,163,0.15); color: #4ecca3; border: 1px solid rgba(78,204,163,0.3); } .sz-badge-warn { background: rgba(255,165,0,0.15); color: #ffa500; border: 1px solid rgba(255,165,0,0.3); } .sz-badge-err { background: rgba(233,69,96,0.15); color: #e94560; border: 1px solid rgba(233,69,96,0.3); } .sz-pass { color: #4ecca3; text-align: center; padding: 24px 0; font-size: 15px; font-weight: bold; } .sz-warn-header { color: #e94560; font-weight: bold; margin: 10px 0 6px; font-size: 13px; display: flex; align-items: center; gap: 4px; } .sz-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: 14px; line-height: 1.6; color: #ccc; transition: background 0.2s; } .sz-warn-item:hover { background: rgba(233,69,96,0.15); } .sz-info { color: #8892b0; padding: 8px 0; text-align: center; } .sz-loading { color: #8892b0; text-align: center; padding: 24px 0; } @keyframes sz-shake { 0%, 100% { transform: translateX(0); } 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } 20%, 40%, 60%, 80% { transform: translateX(5px); } } .sz-shake { animation: sz-shake 0.5s ease-in-out; } `; document.head.appendChild(style); } // ============================================================ // 创建面板 // ============================================================ function createPanel() { if (panel && document.body.contains(panel)) return panel; injectStyles(); panel = document.createElement('div'); panel.id = 'sz-check-panel'; panel.innerHTML = `
手势标注校验
等待标注数据加载...
`; document.body.appendChild(panel); document.getElementById('sz-btn-check').addEventListener('click', manualCheck); document.getElementById('sz-btn-toggle').addEventListener('click', togglePanel); makeDraggable(); panelReady = true; return panel; } // ============================================================ // 拖拽功能 // ============================================================ function makeDraggable() { const header = document.getElementById('sz-check-header'); let isDragging = false; let startX = 0; let startY = 0; let startLeft = 0; let startTop = 0; 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); // 边界限制 const maxLeft = window.innerWidth - 80; const maxTop = window.innerHeight - 50; newLeft = Math.max(-260, Math.min(newLeft, maxLeft)); newTop = Math.max(0, Math.min(newTop, maxTop)); panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px'; panel.style.right = 'auto'; }); document.addEventListener('mouseup', function () { isDragging = false; }); } function togglePanel() { const content = document.getElementById('sz-check-content'); const btn = document.getElementById('sz-btn-toggle'); if (content.style.display === 'none') { content.style.display = ''; btn.innerHTML = '−'; } else { content.style.display = 'none'; btn.innerHTML = '+'; } } // ============================================================ // 校验逻辑 // ============================================================ function validateMarks(respData) { const results = []; const markStatus = respData.mark_status; const markData = respData.markData || {}; const marks = markData.marks || []; const hasMarks = marks.length > 0; if (markStatus === 1 && !hasMarks) { results.push({ type: 'info', msg: '无需标注,跳过检查' }); return results; } if (markStatus === 1 && hasMarks) { results.push({ type: 'warning', msg: `[状态异常] 标注了${marks.length}个框但点了"无需标注",可能误操作` }); } if (markStatus === 0 && !hasMarks) { results.push({ type: 'warning', msg: '[可能漏标] 标注状态为正常但无任何标注数据' }); return results; } if (!hasMarks) { results.push({ type: 'info', msg: '当前帧无标注数据' }); return results; } for (let i = 0; i < marks.length; i++) { const mark = marks[i]; const cls = mark.class ? mark.class.pname : ''; const groupId = mark.groupId; const markId = mark.markId || ('index_' + i); if (CHECK_CLASSES.indexOf(cls) !== -1 && (groupId === undefined || groupId === null)) { const clsName = cls === 'half_bbox_xywh' ? '半身框' : '手势框'; results.push({ type: 'warning', msg: `[未归组] ${clsName}(${markId}) 没有groupId,应与对应全身框归组` }); } } return results; } function autoRunCheck() { if (!cachedData) return; const results = validateMarks(cachedData); renderResults(results, cachedData); } function manualCheck() { const btn = document.getElementById('sz-btn-check'); const content = document.getElementById('sz-check-content'); if (!cachedData) { content.innerHTML = '
暂无标注数据,请先切换到一帧
'; return; } btn.textContent = '校验中...'; btn.disabled = true; setTimeout(function () { const results = validateMarks(cachedData); renderResults(results, cachedData); btn.textContent = '校验当前帧'; btn.disabled = false; }, 100); } // ============================================================ // 渲染结果 // ============================================================ function renderResults(results, respData) { const content = document.getElementById('sz-check-content'); const markStatus = respData.mark_status; const markData = respData.markData || {}; const marks = markData.marks || []; let warnings = results.filter(r => r.type === 'warning'); let infos = results.filter(r => r.type === 'info'); let html = ''; const isNoMark = markStatus === 1; const hasProblem = warnings.length > 0; // 状态颜色:无需标注→绿,正常标注+有问题→红,正常标注+无问题→绿 let statusText, statusClass, statsBorder, statsTitleColor; if (isNoMark) { statusText = '无需标注'; statusClass = 'sz-badge-warn'; statsBorder = 'rgba(255,165,0,0.3)'; statsTitleColor = '#ffa500'; } else if (hasProblem) { statusText = '正常标注'; statusClass = 'sz-badge-err'; statsBorder = 'rgba(233,69,96,0.3)'; statsTitleColor = '#e94560'; } else { statusText = '正常标注'; statusClass = 'sz-badge-ok'; statsBorder = 'rgba(78,204,163,0.3)'; statsTitleColor = '#4ecca3'; } const fullCount = marks.filter(m => m.class && m.class.pname === 'full_bbox').length; const halfCount = marks.filter(m => m.class && m.class.pname === 'half_bbox_xywh').length; const handCount = marks.filter(m => m.class && m.class.pname === 'handbboxxywh').length; html += `
当前帧统计
状态: ${statusText}
标注总数: ${marks.length} | 全身框: ${fullCount} | 半身框: ${halfCount} | 手势框: ${handCount}
`; if (infos.length > 0) { for (let info of infos) { html += `
${info.msg}
`; } } if (warnings.length === 0 && infos.length === 0) { html += `
✓ 校验通过,无问题
`; } else if (warnings.length > 0) { html += `
⚠ 发现问题(${warnings.length}条)
`; for (let i = 0; i < warnings.length; i++) { html += `
${warnings[i].msg}
`; } } content.innerHTML = html; // 有问题时震动面板 if (warnings.length > 0) { panel.classList.remove('sz-shake'); void panel.offsetWidth; panel.classList.add('sz-shake'); } } // ============================================================ // 初始化 // ============================================================ function init() { hookXHR(); const startPanel = function () { if (document.body) { createPanel(); if (cachedData) { autoRunCheck(); } } else { setTimeout(startPanel, 200); } }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', startPanel); } else { setTimeout(startPanel, 500); } } init(); })();