// ==UserScript== // @name MF-REID框大小检测 // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description 检测REID行人项目所有帧中宽<20或高<40的不合规框(行人主框+其他行人框),悬浮窗显示不合格帧号与数量 // @match https://label.mindflow.com.cn/image* // @grant none // @run-at document-start // ==/UserScript== (function () { 'use strict'; // ========== 数据存储 ========== const frameIndexMap = {}; // frameId -> 帧号(indexNo),来自 task/info const frameDataCache = {}; // frameId -> LOCATE 组列表,来自 result/find let scanTimer = null; // ========== 检测规则 ========== const MIN_W = 20; // 最小宽度 const MIN_H = 40; // 最小高度 // ========== 悬浮窗 UI ========== let panelDom = null; let contentDom = null; function showToast(msg) { const tip = document.createElement("div"); tip.style.cssText = "position:fixed;top:24px;left:50%;transform:translateX(-50%);background:#222;color:#fff;padding:8px 14px;border-radius:6px;z-index:999999999;font-size:13px;"; tip.innerText = msg; document.body.appendChild(tip); setTimeout(() => tip.remove(), 2500); } function makeBtn(text, click) { const s = document.createElement('span'); s.innerText = text; s.style.cssText = "width:26px;height:26px;border-radius:6px;background:#e5e7eb;display:flex;align-items:center;justify-content:center;cursor:pointer;font-weight:bold;"; s.onclick = click; return s; } function initPanel() { if (panelDom && document.body.contains(panelDom)) return; panelDom = document.createElement('div'); panelDom.style.cssText = ` position: fixed; z-index: 999999999; width: 460px; left:15px; top:70px; background:#fff; border:1px solid #d1d5db; border-radius:10px; box-shadow:0 4px 20px rgba(0,0,0,0.15); font-family:"Microsoft YaHei"; font-size:13px; overflow:hidden; `; const header = document.createElement('div'); header.style.cssText = "display:flex;gap:6px;align-items:center;padding:8px 12px;background:#f3f4f6;border-bottom:1px solid #ddd;cursor:move;"; header.innerHTML = `MF-REID框大小检测`; const btnWrap = document.createElement('div'); btnWrap.style.display = 'flex'; btnWrap.append(makeBtn("↻", () => runScan(true)), makeBtn("✕", () => panelDom.remove())); header.appendChild(btnWrap); panelDom.append(header); contentDom = document.createElement('div'); contentDom.style.padding = "12px"; contentDom.style.maxHeight = "500px"; contentDom.style.overflowY = "auto"; panelDom.append(contentDom); // 拖拽 let drag = false, ox, oy; header.onmousedown = e => { drag = true; const r = panelDom.getBoundingClientRect(); ox = e.clientX - r.left; oy = e.clientY - r.top; document.onmousemove = ev => { panelDom.style.left = (ev.clientX - ox) + 'px'; panelDom.style.top = (ev.clientY - oy) + 'px'; }; document.onmouseup = () => { drag = false; document.onmousemove = null; }; }; document.body.appendChild(panelDom); } // ========== 核心扫描 ========== function checkFrame(frameId) { const groups = frameDataCache[frameId]; const bad = { main: [], other: [] }; for (const group of groups) { const toolName = group.labelToolName || ''; const isMain = toolName.indexOf('主框') !== -1; const objs = group.labelObjects || {}; for (const uuid in objs) { const obj = objs[uuid]; if (obj.valid === false) continue; // 跳过无效框 const g = obj.geometry || {}; const w = g.width, h = g.height; if (typeof w !== 'number' || typeof h !== 'number') continue; if (w < MIN_W || h < MIN_H) { const item = { uuid: obj.uuid || uuid, indexNumber: obj.indexNumber, w, h, toolName }; if (isMain) bad.main.push(item); else bad.other.push(item); } } } return bad; } function runScan(manual) { if (!panelDom || !document.body.contains(panelDom)) initPanel(); const frameIds = Object.keys(frameDataCache); const results = []; // {frameNo, frameId, main:[], other:[]} let totalBad = 0; for (const fid of frameIds) { const bad = checkFrame(fid); const cnt = bad.main.length + bad.other.length; if (cnt > 0) { totalBad += cnt; results.push({ frameNo: frameIndexMap[fid] || '?', frameId: fid, main: bad.main, other: bad.other }); } } // 按帧号排序 results.sort((a, b) => (a.frameNo === '?' ? 99999 : a.frameNo) - (b.frameNo === '?' ? 99999 : b.frameNo)); let html = ''; if (results.length === 0) { html = `
✅ 已检测 ${frameIds.length} 帧,所有框均满足像素要求(宽≥${MIN_W} 且 高≥${MIN_H})
`; } else { html = `
⚠ 共 ${results.length} 帧 / ${totalBad} 个框不满足标注要求
像素要求:最小宽度 ≥${MIN_W},最小高度 ≥${MIN_H}
`; for (const r of results) { const items = []; for (const b of r.main) items.push(`主框#${b.indexNumber}(w${b.w.toFixed(1)}×h${b.h.toFixed(1)})`); for (const b of r.other) items.push(`其他框#${b.indexNumber}(w${b.w.toFixed(1)}×h${b.h.toFixed(1)})`); html += `
第 ${r.frameNo} 帧:${items.join('、') || '(空)'}
`; } } html += `
已缓存 ${frameIds.length} 帧(共 ${Object.keys(frameIndexMap).length} 帧)|点 ↻ 重新扫描
`; contentDom.innerHTML = html; if (manual) showToast("扫描完成"); } // 防抖扫描(接口分批到达,等都到了再扫) function scheduleScan() { clearTimeout(scanTimer); scanTimer = setTimeout(() => { if (panelDom && document.body.contains(panelDom)) runScan(false); }, 500); } // ========== 拦截 XHR(与 GL脚本同款套路) ========== const originOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url) { this._url = url; const originSend = this.send; this.send = function (body) { this._body = body; originSend.call(this, body); }; this.addEventListener('load', () => { if (this.status !== 200) return; try { const res = JSON.parse(this.responseText); if (res.code !== 200 || !res.data) return; // 1) task/info → 帧号映射 if (this._url.indexOf('task/info') !== -1) { const frameList = res.data.frameList; if (Array.isArray(frameList)) { for (const f of frameList) { frameIndexMap[f.frameId] = f.indexNo; } scheduleScan(); } } // 2) result/find → 框数据(分多批到达,合并缓存) if (this._url.indexOf('result/find') !== -1) { const data = res.data; for (const fid in data) { const groups = data[fid] && data[fid].LOCATE; if (Array.isArray(groups)) { frameDataCache[fid] = groups; } } scheduleScan(); } } catch (e) { /* 忽略解析错误 */ } }); return originOpen.call(this, method, url); }; // ========== 初始化 ========== function boot() { initPanel(); contentDom.innerHTML = `
正在等待数据加载(task/info + result/find 4批)...
`; // 每 3 秒自动重扫一次,直到数据齐 const iv = setInterval(() => { const loaded = Object.keys(frameDataCache).length; const total = Object.keys(frameIndexMap).length; if (loaded > 0 && (total === 0 || loaded >= total * 0.95)) { runScan(false); clearInterval(iv); } else if (panelDom && !document.body.contains(panelDom)) { clearInterval(iv); } }, 3000); setTimeout(() => clearInterval(iv), 60000); // 60秒兜底 } if (document.body) setTimeout(boot, 1000); else document.addEventListener('DOMContentLoaded', () => setTimeout(boot, 1000)); window.mfFrameCheck = { runScan, frameIndexMap, frameDataCache }; })();