// ==UserScript==
// @name 比邻云行人助手
// @namespace https://label.bilinyun.net
// @version 1.4.1
// @description 比邻云行人(2DPedestrian)质检助手:① 进题包全量预加载全部帧,消除首次切帧的灰屏转圈等待 ② 切帧去黑屏遮罩 ③ 帧级标注缓存 + 缺帧自动补请求 ④ 面板实时显示本帧框数与整包统计 ⑤ 整包框尺寸检查(宽≥20 高≥40,忽略框跳过,错误帧点击跳转)⑥ 一键复制「包号 + 有效帧数 + 框数」(有效帧 = 有框的帧;包号与帧数之间空一列)
// @author CC
// @match https://label.bilinyun.net/*
// @match http://118.196.122.17/*
// @grant none
// @run-at document-start
// @license CC
// ==/UserScript==
(function () {
'use strict';
/* ==================== 常量 ==================== */
const API_MARK = '/api/mark/image';
const API_STATIC = '/api/mark/contents/static';
const WARMUP_KEY = '_blyped_warmup';
// ---- 框尺寸规则(来自 BB 平台「行人验收框大小」脚本,单位:图像像素)----
// 不分主/非主行人,统一按此标准;标了「忽略」的框跳过检查
const MIN_W = 20;
const MIN_H = 40;
let WARMUP_ON = true;
try { WARMUP_ON = localStorage.getItem(WARMUP_KEY) !== '0'; } catch (e) { }
/* ==================== 全局状态 ==================== */
let CMAP = null; // 类目映射(从 projectClass 动态构建)
let IMG_SIZE = null; // 图像宽高缓存
let AUTH_HDRS = null; // 页面真实请求里的鉴权头(用于兜底补请求)
let FRAME_IDS = []; // 帧序号(1-based) → frameId
const FRAME_CACHE = {}; // frameId → shapeList
let PROJECT_INFO = {}; // taskNo / projectName 等
let lastFrameNo = null;
let lastRenderKey = null;
let PACK_STAT = null; // 整包统计结果
let WARMUP_RUNNING = false;
let WARMUP_DONE = false;
let warmupTried = false;
let staticTried = false;
/* ==================== 工具 ==================== */
function num(v) {
const n = parseFloat(v);
return isNaN(n) ? null : n;
}
function parseJson(s) {
try { return JSON.parse(s) || {}; } catch (e) { return {}; }
}
function esc(s) {
return String(s === null || s === undefined ? '' : s)
.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}
function fmt1(n) {
if (n === null || n === undefined || isNaN(n)) return '?';
return (Math.round(n * 10) / 10).toFixed(1);
}
function taskIdFromUrl() {
const m = /[?&]recordId=([^&]+)/.exec(location.search || '');
return m ? m[1] : null;
}
function frameIdFromUrl(u) {
const m = /frame_id=([^&]+)/.exec(u || '');
return m ? m[1] : null;
}
function currentFrameNo() {
const el = document.querySelector('.frame-counter-clickable');
if (!el) return null;
const m = /(\d+)\s*\/\s*(\d+)/.exec(el.innerText || '');
return m ? parseInt(m[1], 10) : null;
}
/* ==================== 类目映射(自适应 projectClass) ==================== */
// 行人项目实测两个类目:
// full_bbox → 行人主框(含 是否忽略 / 遮挡 / 截断)
// Other pedestrian boxes → 其他行人框(含 忽略 / 遮挡 / 截断)
function buildClassMap(pcStr) {
let pc;
if (!pcStr) return null;
try { pc = (typeof pcStr === 'string') ? JSON.parse(pcStr) : pcStr; } catch (e) { return null; }
if (!pc || !pc.id || !pc.id.length) return null;
const ids = pc.id || [];
const exps = pc.exportName || [];
const names = pc.name || [];
const attrs = pc.attributes || [];
const m = {
mainId: null, // 行人主框类目 id
otherId: null, // 其他行人框类目 id
idName: {}, // 类目 id → 名称
ignoreAttr: {}, // 类目 id → { attrId, values:Set(忽略值id) }
occAttr: {}, // 类目 id → { attrId, values:{} }
truncAttr: {}, // 类目 id → { attrId, values:{} }
allIds: ids.slice()
};
ids.forEach(function (id, i) {
const ex = String(exps[i] || '');
const nm = String(names[i] || '');
m.idName[id] = nm || ex || id;
if (/other/i.test(ex) || nm.indexOf('其他') !== -1) m.otherId = id;
else if (!m.mainId) m.mainId = id;
const collect = function (a, target) {
const vals = {};
(a.attributes || []).forEach(function (v) { vals[v.id] = String(v.name || v.exportName || ''); });
target[id] = { attrId: a.id, values: vals };
};
(attrs[i] || []).forEach(function (a) {
const ax = String(a.exportName || '');
const an = String(a.name || '');
// 是否忽略:主框是「Ignore / 0=是 1=否」,其他框是「ignore / 0=有效 1=忽略」
if (/ignore/i.test(ax) || an.indexOf('忽略') !== -1) {
const set = {};
(a.attributes || []).forEach(function (v) {
const vn = String(v.name || '').trim();
const ve = String(v.exportName || '').trim();
if (vn === '忽略' || ve === '忽略' || vn === '是' || ve === '是') set[v.id] = true;
});
m.ignoreAttr[id] = { attrId: a.id, values: set };
}
if (/occlusion/i.test(ax) || an.indexOf('遮挡') !== -1) collect(a, m.occAttr);
if (/truncation/i.test(ax) || an.indexOf('截断') !== -1) collect(a, m.truncAttr);
});
});
if (!m.mainId) m.mainId = m.otherId;
return m;
}
function getMap() { return CMAP; }
function isIgnored(box) {
const M = getMap();
if (!M || !M.ignoreAttr) return false;
const conf = M.ignoreAttr[box.typeId];
if (!conf) return false;
const v = box.attrs ? box.attrs[conf.attrId] : null;
return !!(v && conf.values[v]);
}
/* ==================== 标注归一化 ==================== */
// 比邻云 shapeList 元素:{ labelId, imageId, shapeJson, attrJson }(后两者是字符串化 JSON)
function boxOf(s) {
if (!s || s.labelId === undefined) return null;
const sh = parseJson(s.shapeJson);
const at = parseJson(s.attrJson);
const g = sh.shape || {};
let lx = num(g.lx), ly = num(g.ly), rx = num(g.rx), ry = num(g.ry);
// 平台有两种坐标格式:
// 老格式 { lx, ly, rx, ry }
// 新格式 { x, y, width, height, rotation } + geometryVersion:2(AI 预标注/旋转框)
// 统一折算成 lx/ly/rx/ry,否则新格式会算不出宽高、误报「无有效尺寸数据」
if (lx === null || rx === null) {
const gx = num(g.x), gw = num(g.width);
if (gx !== null && gw !== null) { lx = gx; rx = gx + gw; }
}
if (ly === null || ry === null) {
const gy = num(g.y), gh = num(g.height);
if (gy !== null && gh !== null) { ly = gy; ry = gy + gh; }
}
return {
labelId: s.labelId,
typeId: at.type_id,
attrs: at.attributes || {},
isRect: String(sh.shapeType) === '0',
lx: lx, ly: ly, rx: rx, ry: ry,
w: (lx !== null && rx !== null) ? Math.abs(rx - lx) : null,
h: (ly !== null && ry !== null) ? Math.abs(ry - ly) : null,
clsName: (getMap() && getMap().idName[s.attrJson ? at.type_id : '']) || ''
};
}
/* ==================== 图像真实尺寸 ==================== */
// 页面 里有 logo,不能直接用;优先从 Konva 舞台的 Image 节点取
function getImageSize() {
let w = 0, h = 0;
try {
if (typeof Konva !== 'undefined' && Konva.stages && Konva.stages.length) {
const nodes = Konva.stages[0].find('Image');
for (let i = 0; i < nodes.length; i++) {
const im = nodes[i].image && nodes[i].image();
if (im && im.naturalWidth > 200 && im.naturalHeight > 200) {
w = im.naturalWidth; h = im.naturalHeight; break;
}
}
}
} catch (e) { }
if (!w) {
const imgs = document.querySelectorAll('img');
for (let i = 0; i < imgs.length; i++) {
const iw = imgs[i].naturalWidth, ih = imgs[i].naturalHeight;
if (iw > 1000 && ih > 1000) { w = iw; h = ih; break; }
}
}
if (!w) { IMG_SIZE = null; return null; }
if (!IMG_SIZE || IMG_SIZE.w !== w || IMG_SIZE.h !== h) IMG_SIZE = { w: w, h: h };
return IMG_SIZE;
}
/* ==================== 本帧分析 ==================== */
let lastBoxes = null;
function analyzeShapes(shapeList, size) {
const boxes = [];
(shapeList || []).forEach(function (s) {
const b = boxOf(s);
if (b && b.isRect) boxes.push(b);
});
const ignored = boxes.filter(function (b) { return isIgnored(b); });
lastBoxes = { boxes: boxes, valid: boxes.length - ignored.length, ignored: ignored.length };
const rk = boxes.map(function (b) {
return b.labelId + '|' + b.lx + ',' + b.ly + ',' + b.rx + ',' + b.ry + '|' + JSON.stringify(b.attrs);
}).join(';') + '|' + currentFrameNo() + '|' + (size ? size.w + 'x' + size.h : '-');
if (rk === lastRenderKey) return; // 数据未变不重渲染,避免面板闪烁
lastRenderKey = rk;
renderPanel();
}
function showWaiting(no) {
if (WARMUP_RUNNING) return;
lastBoxes = null;
lastRenderKey = null;
renderPanel(no);
}
/* ==================== 面板 UI ==================== */
let panelContainer = null, panelBody = null, panelContent = null;
let isMinimized = false, isDragging = false, dragOffsetX = 0, dragOffsetY = 0;
let panelX = null, panelY = 60;
function createPanel() {
if (panelContainer && document.body && document.body.contains(panelContainer)) return;
const exist = document.getElementById('_blyped_panel');
if (exist) { panelContainer = exist; panelBody = exist.querySelector('#_blyped_body'); panelContent = exist.querySelector('#_blyped_content'); return; }
panelContainer = document.createElement('div');
panelContainer.id = '_blyped_panel';
if (panelX === null) panelX = Math.max(20, window.innerWidth - 372);
panelContainer.style.cssText = 'position:fixed;z-index:999999;width:352px;background:#ffffff;'
+ 'border:1px solid #d1d5db;border-radius:10px;box-shadow:0 4px 20px rgba(0,0,0,0.15);'
+ 'font-family:"Microsoft YaHei","PingFang SC",Arial,sans-serif;font-size:13px;color:#1f2937;'
+ 'overflow:hidden;left:' + panelX + 'px;top:' + panelY + 'px;user-select:none;';
const titleBar = document.createElement('div');
titleBar.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:8px 12px;'
+ 'background:#f3f4f6;cursor:move;border-bottom:1px solid #d1d5db;';
titleBar.innerHTML = '🚶 比邻云行人助手';
titleBar.addEventListener('mousedown', startDrag);
const btns = document.createElement('div');
btns.style.cssText = 'display:flex;gap:6px;';
btns.appendChild(createBtn('—', '#e5e7eb', '#d1d5db', function (e) { e.stopPropagation(); toggleMinimize(); }));
btns.appendChild(createBtn('✕', '#fca5a5', '#f87171', function (e) { e.stopPropagation(); closePanel(); }));
titleBar.appendChild(btns);
panelContainer.appendChild(titleBar);
panelBody = document.createElement('div');
panelBody.id = '_blyped_body';
panelBody.style.cssText = 'max-height:520px;overflow-y:auto;';
panelContent = document.createElement('div');
panelContent.id = '_blyped_content';
panelContent.style.cssText = 'padding:11px;background:#ffffff;';
panelBody.appendChild(panelContent);
panelContainer.appendChild(panelBody);
document.body.appendChild(panelContainer);
}
function createBtn(text, color, hover, onClick) {
const b = document.createElement('span');
b.textContent = text;
b.style.cssText = 'width:26px;height:26px;display:flex;align-items:center;justify-content:center;'
+ 'border-radius:6px;cursor:pointer;font-size:14px;font-weight:bold;color:#374151;background:' + color + ';';
b.addEventListener('mouseenter', function () { b.style.background = hover; });
b.addEventListener('mouseleave', function () { b.style.background = color; });
b.addEventListener('click', onClick);
return b;
}
function toggleMinimize() {
isMinimized = !isMinimized;
if (panelBody) panelBody.style.maxHeight = isMinimized ? '0' : '520px';
}
function closePanel() {
if (panelContainer) { panelContainer.remove(); panelContainer = null; }
}
function startDrag(e) {
if (e.button !== 0 || !panelContainer) return;
isDragging = true;
const r = panelContainer.getBoundingClientRect();
dragOffsetX = e.clientX - r.left;
dragOffsetY = e.clientY - r.top;
document.addEventListener('mousemove', onDrag);
document.addEventListener('mouseup', stopDrag);
e.preventDefault();
}
function onDrag(e) {
if (!isDragging || !panelContainer) return;
panelX = Math.max(0, e.clientX - dragOffsetX);
panelY = Math.max(0, e.clientY - dragOffsetY);
panelContainer.style.left = panelX + 'px';
panelContainer.style.top = panelY + 'px';
}
function stopDrag() {
isDragging = false;
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('mouseup', stopDrag);
}
function row(label, value) {
return '