/
vshmidt
/
label-studio
Обзор
Документация
Войти
/
vshmidt
/
label-studio
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
web/libs/editor/src/components/ImageView/LabelOnRegion.jsx
368 строк
12 KB
Nick Skriabin
feat: BROS-833: Video vectors (#9504)
13 мар 2026, 18:25
Не верифицирован
13 мар 2026, 18:25
e8d826e
Код
Авторство
О чём код?
import { Fragment, useCallback, useContext, useMemo, useState } from "react"; import { Group, Label, Path, Rect, Tag, Text } from "react-konva"; import { observer } from "mobx-react"; import { getRoot } from "mobx-state-tree"; import Utils from "../../utils"; import Constants from "../../core/Constants"; import { ImageViewContext } from "./ImageViewContext"; const NON_ADJACENT_CORNER_RADIUS = 4; const ADJACENT_CORNER_RADIUS = [4, 4, 0, 0]; const TAG_PATH = "M13.47,2.52c-0.27-0.27-0.71-0.27-1.59-0.27h-0.64c-1.51,0-2.26,0-2.95,0.29C7.61,2.82,7.07,3.35,6,4.43L3.65,6.78c-0.93,0.93-1.4,1.4-1.4,1.97c0,0.58,0.46,1.04,1.39,1.97l1.63,1.63c0.93,0.93,1.39,1.39,1.97,1.39s1.04-0.46,1.97-1.39L11.57,10c1.07-1.07,1.61-1.61,1.89-2.29c0.28-0.68,0.28-1.44,0.28-2.96V4.11C13.74,3.23,13.74,2.8,13.47,2.52z M10.5,6.9c-0.77,0-1.4-0.63-1.4-1.4s0.63-1.39,1.4-1.39s1.39,0.63,1.39,1.4S11.27,6.9,10.5,6.9z"; const OCR_PATH = "M13,1v2H6C4.11,3,3.17,3,2.59,3.59C2,4.17,2,5.11,2,7v2c0,1.89,0,2.83,0.59,3.41C3.17,13,4.11,13,6,13h7v2h1V1H13z M6,9.5C5.17,9.5,4.5,8.83,4.5,8S5.17,6.5,6,6.5S7.5,7.17,7.5,8S6.83,9.5,6,9.5z M11,9.5c-0.83,0-1.5-0.67-1.5-1.5s0.67-1.5,1.5-1.5s1.5,0.67,1.5,1.5S11.83,9.5,11,9.5z"; const LabelOnBbox = ({ x, y, text, score, showLabels, rotation = 0, zoomScale = 1, color, maxWidth, onClickLabel, onMouseEnterLabel, onMouseLeaveLabel, adjacent = false, isTexting = false, }) => { const fontSize = 13; const height = 20; const scale = 1 / zoomScale; const [textEl, setTextEl] = useState(); const paddingLeft = 20; const paddingRight = 5; const scoreSpace = score ? 34 : 0; const horizontalPaddings = paddingLeft + paddingRight; const textMaxWidth = Math.max(0, maxWidth * zoomScale - horizontalPaddings - scoreSpace); const isSticking = !!textMaxWidth; const { suggestion } = useContext(ImageViewContext) ?? {}; const width = useMemo(() => { if (!showLabels || !textEl || !maxWidth) return null; const currentTextWidth = text ? textEl.measureSize(text).width : 0; if (currentTextWidth > textMaxWidth) { return textMaxWidth; } return null; }, [textEl, text, maxWidth, scale]); const tagSceneFunc = useCallback( (context, shape) => { const cornerRadius = adjacent && isSticking ? ADJACENT_CORNER_RADIUS : NON_ADJACENT_CORNER_RADIUS; const width = maxWidth ? Math.min(shape.width() + horizontalPaddings, isSticking ? maxWidth * zoomScale : paddingLeft) : shape.width() + horizontalPaddings; const height = shape.height(); context.beginPath(); if (!cornerRadius) { context.rect(0, 0, width, height); } else { let topLeft = 0; let topRight = 0; let bottomLeft = 0; let bottomRight = 0; if (typeof cornerRadius === "number") { topLeft = topRight = bottomLeft = bottomRight = Math.min(cornerRadius, width / 2, height / 2); } else { topLeft = Math.min(cornerRadius[0], width / 2, height / 2); topRight = Math.min(cornerRadius[1], width / 2, height / 2); bottomRight = Math.min(cornerRadius[2], width / 2, height / 2); bottomLeft = Math.min(cornerRadius[3], width / 2, height / 2); } context.moveTo(topLeft, 0); context.lineTo(width - topRight, 0); context.arc(width - topRight, topRight, topRight, (Math.PI * 3) / 2, 0, false); context.lineTo(width, height - bottomRight); context.arc(width - bottomRight, height - bottomRight, bottomRight, 0, Math.PI / 2, false); context.lineTo(bottomLeft, height); context.arc(bottomLeft, height - bottomLeft, bottomLeft, Math.PI / 2, Math.PI, false); context.lineTo(0, topLeft); context.arc(topLeft, topLeft, topLeft, Math.PI, (Math.PI * 3) / 2, false); } context.closePath(); context.fillStrokeShape(shape); }, [adjacent, isSticking, maxWidth], ); if (!showLabels) return null; return ( <Group strokeScaleEnabled={false} x={x} y={y} rotation={rotation}> {!!score && ( <Label y={-height * scale} scaleX={scale} scaleY={scale} onClick={() => { return false; }} > <Tag fill={Utils.Colors.getScaleGradient(score)} cornerRadius={2} /> <Text text={score.toFixed(2)} fontFamily="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif" fontSize={fontSize} fill="white" padding={0} lineHeight={(1 / fontSize) * height} /> </Label> )} <Label x={paddingLeft * scale + scoreSpace * scale} y={-height * scale} scaleX={scale} scaleY={scale} onClick={onClickLabel} onMouseEnter={onClickLabel ? onMouseEnterLabel : null} onMouseLeave={onClickLabel ? onMouseLeaveLabel : null} listening={!suggestion} > <Tag fill={color} cornerRadius={4} sceneFunc={tagSceneFunc} offsetX={paddingLeft} /> <Text ref={setTextEl} text={text} fontFamily="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif" fontSize={fontSize} lineHeight={(1 / fontSize) * height} height={height} width={width} wrap="none" ellipsis="true" fill={Constants.SHOW_LABEL_FILL} padding={0} /> </Label> <Path x={2 * scale + scoreSpace * scale} y={2 * scale - height * scale} scaleX={scale} scaleY={scale} fill={Constants.SHOW_LABEL_FILL} data={isTexting ? OCR_PATH : TAG_PATH} /> </Group> ); }; const LabelOnEllipse = observer(({ item, color, strokewidth }) => { if (!item.parent) return null; const isTexting = !!item.texting; const labelText = item.getLabelText(","); const obj = item.parent; const zoomScale = item.parent.zoomScale || 1; return ( <LabelOnBbox x={obj.internalToCanvasX(item.x - item.radiusX) - strokewidth / 2 / zoomScale} y={obj.internalToCanvasY(item.y - item.radiusY) - strokewidth / 2 / zoomScale} isTexting={isTexting} text={labelText} score={item.score} showLabels={getRoot(item).settings.showLabels} zoomScale={item.parent.zoomScale} color={color} onClickLabel={item.onClickLabel} /> ); }); const LabelOnRect = observer(({ item, color, strokewidth }) => { if (!item.parent) return null; const isTexting = !!item.texting; const labelText = item.getLabelText(","); const obj = item.parent; const zoomScale = item.parent.zoomScale || 1; return ( <LabelOnBbox x={obj.internalToCanvasX(item.x) - strokewidth / 2 / zoomScale} y={obj.internalToCanvasY(item.y) - strokewidth / 2 / zoomScale} isTexting={isTexting} text={labelText} score={item.score} showLabels={getRoot(item).settings.showLabels} zoomScale={item.parent.zoomScale} rotation={item.rotation} color={color} maxWidth={obj.internalToCanvasX(item.width) + strokewidth} adjacent onClickLabel={item.onClickLabel} /> ); }); const LabelOnPolygon = observer(({ item, color }) => { if (!item.parent) return null; const isTexting = !!item.texting; const labelText = item.getLabelText(","); const bbox = item.bboxCoordsCanvas; if (!bbox) return null; const settings = getRoot(item).settings; return ( <Fragment> {settings.showLabels && ( <Rect x={bbox.left} y={bbox.top} fillEnabled={false} width={bbox.right - bbox.left} height={bbox.bottom - bbox.top} stroke={item.style?.strokecolor} strokeWidth={1} strokeScaleEnabled={false} shadowBlur={0} /> )} <LabelOnBbox x={bbox.left} y={bbox.top + 2 / item.parent.zoomScale} isTexting={isTexting} text={labelText} score={item.score} showLabels={settings.showLabels} zoomScale={item.parent.zoomScale} color={color} onClickLabel={item.onClickLabel} /> </Fragment> ); }); const LabelOnMask = observer(({ item, color }) => { if (!item.parent) return null; const settings = getRoot(item).settings; if (!settings.showLabels) return null; const isTexting = !!item.texting; const labelText = item.getLabelText(","); const bbox = item.bboxCoordsCanvas; if (!bbox) return null; return ( <Group name="region-label"> <Rect x={bbox.left} y={bbox.top} fillEnabled={false} width={bbox.right - bbox.left} height={bbox.bottom - bbox.top} stroke={item.style?.strokecolor} strokeWidth={1} strokeScaleEnabled={false} shadowBlur={0} /> <LabelOnBbox x={bbox.left} y={bbox.top + 2 / item.parent.zoomScale} isTexting={isTexting} text={labelText} score={item.score} showLabels={settings.showLabels} zoomScale={item.parent.zoomScale} color={color} onClickLabel={item.onClickLabel} /> </Group> ); }); const LabelOnKP = observer(({ item, color }) => { if (!item.parent) return null; const isTexting = !!item.texting; const labelText = item.getLabelText(","); return ( <LabelOnBbox // keypoints' width scaled back to stay always small, so scale it here too x={item.canvasX + (item.canvasWidth + 2) / item.parent.zoomScale} y={item.canvasY + (item.canvasWidth + 2) / item.parent.zoomScale} isTexting={isTexting} text={labelText} score={item.score} showLabels={getRoot(item).settings.showLabels} zoomScale={item.parent.zoomScale} color={color} onClickLabel={item.onClickLabel} /> ); }); const LabelOnVideoBbox = observer(({ reg, box, color, scale, strokeWidth, adjacent = false }) => { if (!reg?.store) return null; const isTexting = !!reg.texting; const labelText = reg.getLabelText(","); const showLabels = reg.store.settings?.showLabels; return ( <LabelOnBbox x={box.x} y={box.y} rotation={box.rotation} isTexting={isTexting} text={labelText} score={reg.score} showLabels={showLabels} zoomScale={scale} color={color} maxWidth={box.width + strokeWidth} adjacent={adjacent} onClickLabel={reg.onClickRegion} /> ); }); const LabelOnOcrBox = observer(({ region, color, strokeWidth = 1, viewRect, pageRotationDeg = 0, zoomScale = 1 }) => { if (!region?.store) return null; const labelText = region.getLabelText(",") || region?.ocrtext || region?.noLabelView || ""; const showLabels = region.store.settings?.showLabels; const isTexting = Boolean(region.texting || region.ocrtext); const rotation = ((((region.rotation ?? 0) + (pageRotationDeg ?? 0)) % 360) + 360) % 360; const safeStrokeWidth = typeof strokeWidth === "number" ? strokeWidth : 0; const rect = viewRect ?? { x: 0, y: 0, width: 0, height: 0 }; const width = Math.max(1, rect.width); return ( <LabelOnBbox x={rect.x - safeStrokeWidth / 2 / zoomScale} y={rect.y - safeStrokeWidth / 2 / zoomScale} isTexting={isTexting} text={labelText} score={region.score} showLabels={showLabels} zoomScale={zoomScale} rotation={rotation} color={color} maxWidth={width + safeStrokeWidth} adjacent onClickLabel={region.onClickRegion} /> ); }); export { LabelOnBbox, LabelOnPolygon, LabelOnRect, LabelOnEllipse, LabelOnKP, LabelOnMask, LabelOnVideoBbox, LabelOnOcrBox, };