Example





What I did.
- 대량의 바운딩 박스를 메인 스레드 블로킹 없이 처리하기 위해 Web Worker와 OffscreenCanvas API를 활용해 개발했습니다.
* 예시 코드입니다.
// 메인 스레드
const canvasRef = useRef < HTMLCanvasElement > null;
const workerRef = (useRef < Worker) | (null > null);
useEffect(() => {
if (!canvasRef.current) return;
// OffscreenCanvas 생성 및 Worker로 전송
const offscreen = canvasRef.current.transferControlToOffscreen();
workerRef.current = new Worker("/bboxRenderer.worker.js");
workerRef.current.postMessage(
{
type: "INIT",
canvas: offscreen,
data: parsedOCRData,
dimensions: {
originWidth: originImgSize.width,
originHeight: originImgSize.height,
containerWidth,
containerHeight,
},
},
[offscreen]
);
return () => workerRef.current?.terminate();
}, [parsedOCRData, containerWidth, containerHeight]);
// bboxRenderer.worker.js
let canvas: OffscreenCanvas;
let ctx: OffscreenCanvasRenderingContext2D;
let bboxData: TextBBoxInfo[];
let sizeDiff: {widthDifference: number, heightDifference: number};
let selectedBBox: number | null = null;
self.onmessage = (e) => {
const { type, canvas: receivedCanvas, data, dimensions, selectedIndex } = e.data;
if (type === 'INIT') {
canvas = receivedCanvas;
ctx = canvas.getContext('2d')!;
bboxData = data.textBBoxInfo;
// 크기 비율 계산
sizeDiff = calculateSizeDifference(
dimensions.originWidth,
dimensions.originHeight,
dimensions.containerWidth,
dimensions.containerHeight
);
renderBoundingBoxes();
} else if (type === 'UPDATE_SELECTION') {
selectedBBox = selectedIndex;
renderBoundingBoxes();
}
};
function renderBoundingBoxes() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
bboxData.forEach(box => {
const x = box.x1 * sizeDiff.widthDifference;
const y = box.y1 * sizeDiff.heightDifference;
const width = Math.abs(box.x2 - box.x1) * sizeDiff.widthDifference;
const height = Math.abs(box.y2 - box.y1) * sizeDiff.heightDifference;
ctx.strokeStyle = '#A065F3';
ctx.lineWidth = 2;
// 선택된 박스 배경 처리
if (box.bboxIndex === selectedBBox) {
ctx.fillStyle = 'rgba(160, 101, 243, 0.2)';
ctx.fillRect(x, y, width, height);
}
ctx.strokeRect(x, y, width, height);
});
}
- 초기에는 OffscreenCanvas에 그려진 바운딩 박스와 사용자 클릭 이벤트 좌표가 일치하지 않아 정확한 선택이 불가능했습니다. 이를 해결하기 위해 이벤트 좌표 매핑 시스템 구현했습니다.
* 예시 코드입니다.
// 메인 스레드에 투명 오버레이 캔버스 추가
const handleCanvasClick = (e: React.MouseEvent<HTMLDivElement>) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 클릭 위치에 있는 바운딩 박스 찾기
const clickedBox = findBoxAtPosition(
x,
y,
parsedOCRData.textBBoxInfo,
sizeDifference
);
if (clickedBox) {
setSelectedBBox(clickedBox.bboxIndex);
workerRef.current?.postMessage({
type: "UPDATE_SELECTION",
selectedIndex: clickedBox.bboxIndex,
});
}
};
// 클릭 위치의 바운딩 박스 찾기
const findBoxAtPosition = (
x: number,
y: number,
boxes: TextBBoxInfo[],
sizeDiff: any
) => {
return boxes.find((box) => {
const boxX = box.x1 * sizeDiff.widthDifference;
const boxY = box.y1 * sizeDiff.heightDifference;
const boxW = Math.abs(box.x2 - box.x1) * sizeDiff.widthDifference;
const boxH = Math.abs(box.y2 - box.y1) * sizeDiff.heightDifference;
return x >= boxX && x <= boxX + boxW && y >= boxY && y <= boxY + boxH;
});
};