From 1797d01a5d4bde6f2d1f396eb6c3687898a44821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20S=C5=82uszniak?= Date: Wed, 18 Mar 2026 17:37:26 +0100 Subject: [PATCH 1/3] chore: Fix bounding box annotation --- .../components/ImageWithBboxes.tsx | 13 ++++--- .../tasks/ObjectDetectionTask.tsx | 39 ++++++++++--------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/apps/computer-vision/components/ImageWithBboxes.tsx b/apps/computer-vision/components/ImageWithBboxes.tsx index 65d345d6cb..e5d8d04bfb 100644 --- a/apps/computer-vision/components/ImageWithBboxes.tsx +++ b/apps/computer-vision/components/ImageWithBboxes.tsx @@ -66,13 +66,18 @@ export default function ImageWithBboxes({ const top = y1 * scaleY + offsetY; const width = (x2 - x1) * scaleX; const height = (y2 - y1) * scaleY; + const labelTop = top < 22 ? top + height + 2 : top - 22; return ( - - + + + {detection.label} ({(detection.score * 100).toFixed(1)}%) - + ); })} @@ -96,8 +101,6 @@ const styles = StyleSheet.create({ }, label: { position: 'absolute', - top: -20, - left: 0, backgroundColor: 'rgba(255, 0, 0, 0.7)', color: 'white', fontSize: 12, diff --git a/apps/computer-vision/components/vision_camera/tasks/ObjectDetectionTask.tsx b/apps/computer-vision/components/vision_camera/tasks/ObjectDetectionTask.tsx index 0ac5ef4af4..8dc6871e3d 100644 --- a/apps/computer-vision/components/vision_camera/tasks/ObjectDetectionTask.tsx +++ b/apps/computer-vision/components/vision_camera/tasks/ObjectDetectionTask.tsx @@ -127,31 +127,36 @@ export default function ObjectDetectionTask({ const top = det.bbox.y1 * scale + offsetY; const w = (det.bbox.x2 - det.bbox.x1) * scale; const h = (det.bbox.y2 - det.bbox.y1) * scale; + const labelTop = top < 26 ? top + h + 2 : top - 26; return ( - + + - + {det.label} {(det.score * 100).toFixed(1)} - + ); })} @@ -167,8 +172,6 @@ const styles = StyleSheet.create({ }, bboxLabel: { position: 'absolute', - top: -22, - left: -2, paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, From 099d7b143f491dfc5b073596ed51aec3528e3fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20S=C5=82uszniak?= Date: Thu, 19 Mar 2026 13:03:15 +0100 Subject: [PATCH 2/3] chore: abstract logic of bounding box in to shared component --- .../components/BoundingBoxes.tsx | 85 +++++++++++++++++++ .../components/ImageWithBboxes.tsx | 54 +++--------- .../components/utils/colors.ts | 41 +++++++++ .../tasks/ObjectDetectionTask.tsx | 64 ++------------ .../components/vision_camera/utils/colors.ts | 42 +-------- 5 files changed, 150 insertions(+), 136 deletions(-) create mode 100644 apps/computer-vision/components/BoundingBoxes.tsx create mode 100644 apps/computer-vision/components/utils/colors.ts diff --git a/apps/computer-vision/components/BoundingBoxes.tsx b/apps/computer-vision/components/BoundingBoxes.tsx new file mode 100644 index 0000000000..6677c6d318 --- /dev/null +++ b/apps/computer-vision/components/BoundingBoxes.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { StyleSheet, Text, View } from 'react-native'; +import { Detection } from 'react-native-executorch'; +import { labelColor, labelColorBg } from './utils/colors'; + +interface Props { + detections: Detection[]; + scaleX: number; + scaleY: number; + offsetX: number; + offsetY: number; + mirrorLabels?: boolean; +} + +export default function BoundingBoxes({ + detections, + scaleX, + scaleY, + offsetX, + offsetY, + mirrorLabels = false, +}: Props) { + return ( + <> + {detections.map((det, i) => { + const left = det.bbox.x1 * scaleX + offsetX; + const top = det.bbox.y1 * scaleY + offsetY; + const width = (det.bbox.x2 - det.bbox.x1) * scaleX; + const height = (det.bbox.y2 - det.bbox.y1) * scaleY; + const labelTop = top < 26 ? top + height + 2 : top - 26; + + return ( + + + + + {det.label} ({(det.score * 100).toFixed(1)}%) + + + + ); + })} + + ); +} + +const styles = StyleSheet.create({ + bbox: { + position: 'absolute', + borderWidth: 2, + borderRadius: 4, + }, + label: { + position: 'absolute', + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 4, + }, + labelText: { + color: 'white', + fontSize: 11, + fontWeight: '600', + }, +}); diff --git a/apps/computer-vision/components/ImageWithBboxes.tsx b/apps/computer-vision/components/ImageWithBboxes.tsx index e5d8d04bfb..13be8dc385 100644 --- a/apps/computer-vision/components/ImageWithBboxes.tsx +++ b/apps/computer-vision/components/ImageWithBboxes.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { Image, StyleSheet, View, Text } from 'react-native'; +import { Image, StyleSheet, View } from 'react-native'; import { Detection } from 'react-native-executorch'; +import BoundingBoxes from './BoundingBoxes'; interface Props { imageUri: string; @@ -21,7 +22,7 @@ export default function ImageWithBboxes({ const imageRatio = imageWidth / imageHeight; const layoutRatio = layout.width / layout.height; - let sx, sy; // Scale in x and y directions + let sx, sy; if (imageRatio > layoutRatio) { // image is more "wide" sx = layout.width / imageWidth; @@ -35,11 +36,13 @@ export default function ImageWithBboxes({ return { scaleX: sx, scaleY: sy, - offsetX: (layout.width - imageWidth * sx) / 2, // Centering the image horizontally - offsetY: (layout.height - imageHeight * sy) / 2, // Centering the image vertically + offsetX: (layout.width - imageWidth * sx) / 2, + offsetY: (layout.height - imageHeight * sy) / 2, }; }; + const { scaleX, scaleY, offsetX, offsetY } = calculateAdjustedDimensions(); + return ( - {detections.map((detection, index) => { - const { scaleX, scaleY, offsetX, offsetY } = - calculateAdjustedDimensions(); - const { x1, y1, x2, y2 } = detection.bbox; - - const left = x1 * scaleX + offsetX; - const top = y1 * scaleY + offsetY; - const width = (x2 - x1) * scaleX; - const height = (y2 - y1) * scaleY; - const labelTop = top < 22 ? top + height + 2 : top - 22; - - return ( - - - - {detection.label} ({(detection.score * 100).toFixed(1)}%) - - - ); - })} + ); } @@ -94,17 +81,4 @@ const styles = StyleSheet.create({ width: '100%', height: '100%', }, - bbox: { - position: 'absolute', - borderWidth: 2, - borderColor: 'red', - }, - label: { - position: 'absolute', - backgroundColor: 'rgba(255, 0, 0, 0.7)', - color: 'white', - fontSize: 12, - paddingHorizontal: 4, - borderRadius: 4, - }, }); diff --git a/apps/computer-vision/components/utils/colors.ts b/apps/computer-vision/components/utils/colors.ts new file mode 100644 index 0000000000..c38493a3b0 --- /dev/null +++ b/apps/computer-vision/components/utils/colors.ts @@ -0,0 +1,41 @@ +export const CLASS_COLORS: number[][] = [ + [0, 0, 0, 0], + [51, 255, 87, 180], + [51, 87, 255, 180], + [255, 51, 246, 180], + [51, 255, 246, 180], + [243, 255, 51, 180], + [141, 51, 255, 180], + [255, 131, 51, 180], + [51, 255, 131, 180], + [131, 51, 255, 180], + [255, 255, 51, 180], + [51, 255, 255, 180], + [255, 51, 143, 180], + [127, 51, 255, 180], + [51, 255, 175, 180], + [255, 175, 51, 180], + [179, 255, 51, 180], + [255, 87, 51, 180], + [255, 51, 162, 180], + [51, 162, 255, 180], + [162, 51, 255, 180], +]; + +export function hashLabel(label: string): number { + let hash = 5381; + for (let i = 0; i < label.length; i++) { + hash = (hash + hash * 32 + label.charCodeAt(i)) % 1000003; + } + return 1 + (Math.abs(hash) % (CLASS_COLORS.length - 1)); +} + +export function labelColor(label: string): string { + const color = CLASS_COLORS[hashLabel(label)]!; + return `rgba(${color[0]},${color[1]},${color[2]},1)`; +} + +export function labelColorBg(label: string): string { + const color = CLASS_COLORS[hashLabel(label)]!; + return `rgba(${color[0]},${color[1]},${color[2]},0.75)`; +} diff --git a/apps/computer-vision/components/vision_camera/tasks/ObjectDetectionTask.tsx b/apps/computer-vision/components/vision_camera/tasks/ObjectDetectionTask.tsx index 8dc6871e3d..9918578b68 100644 --- a/apps/computer-vision/components/vision_camera/tasks/ObjectDetectionTask.tsx +++ b/apps/computer-vision/components/vision_camera/tasks/ObjectDetectionTask.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { StyleSheet, Text, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { Frame, useFrameOutput } from 'react-native-vision-camera'; import { scheduleOnRN } from 'react-native-worklets'; import { @@ -8,7 +8,7 @@ import { SSDLITE_320_MOBILENET_V3_LARGE, useObjectDetection, } from 'react-native-executorch'; -import { labelColor, labelColorBg } from '../utils/colors'; +import BoundingBoxes from '../../BoundingBoxes'; import { TaskProps } from './types'; type ObjModelId = 'objectDetectionSsdlite' | 'objectDetectionRfdetr'; @@ -122,59 +122,13 @@ export default function ObjectDetectionTask({ return ( - {detections.map((det, i) => { - const left = det.bbox.x1 * scale + offsetX; - const top = det.bbox.y1 * scale + offsetY; - const w = (det.bbox.x2 - det.bbox.x1) * scale; - const h = (det.bbox.y2 - det.bbox.y1) * scale; - const labelTop = top < 26 ? top + h + 2 : top - 26; - return ( - - - - - {det.label} {(det.score * 100).toFixed(1)} - - - - ); - })} + ); } - -const styles = StyleSheet.create({ - bbox: { - position: 'absolute', - borderWidth: 2, - borderColor: 'cyan', - borderRadius: 4, - }, - bboxLabel: { - position: 'absolute', - paddingHorizontal: 6, - paddingVertical: 2, - borderRadius: 4, - }, - bboxLabelText: { color: 'white', fontSize: 11, fontWeight: '600' }, -}); diff --git a/apps/computer-vision/components/vision_camera/utils/colors.ts b/apps/computer-vision/components/vision_camera/utils/colors.ts index c38493a3b0..5d59d9171d 100644 --- a/apps/computer-vision/components/vision_camera/utils/colors.ts +++ b/apps/computer-vision/components/vision_camera/utils/colors.ts @@ -1,41 +1 @@ -export const CLASS_COLORS: number[][] = [ - [0, 0, 0, 0], - [51, 255, 87, 180], - [51, 87, 255, 180], - [255, 51, 246, 180], - [51, 255, 246, 180], - [243, 255, 51, 180], - [141, 51, 255, 180], - [255, 131, 51, 180], - [51, 255, 131, 180], - [131, 51, 255, 180], - [255, 255, 51, 180], - [51, 255, 255, 180], - [255, 51, 143, 180], - [127, 51, 255, 180], - [51, 255, 175, 180], - [255, 175, 51, 180], - [179, 255, 51, 180], - [255, 87, 51, 180], - [255, 51, 162, 180], - [51, 162, 255, 180], - [162, 51, 255, 180], -]; - -export function hashLabel(label: string): number { - let hash = 5381; - for (let i = 0; i < label.length; i++) { - hash = (hash + hash * 32 + label.charCodeAt(i)) % 1000003; - } - return 1 + (Math.abs(hash) % (CLASS_COLORS.length - 1)); -} - -export function labelColor(label: string): string { - const color = CLASS_COLORS[hashLabel(label)]!; - return `rgba(${color[0]},${color[1]},${color[2]},1)`; -} - -export function labelColorBg(label: string): string { - const color = CLASS_COLORS[hashLabel(label)]!; - return `rgba(${color[0]},${color[1]},${color[2]},0.75)`; -} +export * from '../../utils/colors'; From a5ee386b22bc24ee66f41e10ebf1fda24a37d8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20S=C5=82uszniak?= Date: Mon, 23 Mar 2026 21:26:14 +0100 Subject: [PATCH 3/3] chore: address comments from code review --- .../components/BoundingBoxes.tsx | 21 ++++++++++++------- .../components/ImageWithBboxes.tsx | 1 + .../tasks/InstanceSegmentationTask.tsx | 2 +- .../tasks/ObjectDetectionTask.tsx | 1 + .../vision_camera/tasks/SegmentationTask.tsx | 2 +- .../components/vision_camera/utils/colors.ts | 1 - 6 files changed, 17 insertions(+), 11 deletions(-) delete mode 100644 apps/computer-vision/components/vision_camera/utils/colors.ts diff --git a/apps/computer-vision/components/BoundingBoxes.tsx b/apps/computer-vision/components/BoundingBoxes.tsx index 6677c6d318..0c1e41c261 100644 --- a/apps/computer-vision/components/BoundingBoxes.tsx +++ b/apps/computer-vision/components/BoundingBoxes.tsx @@ -1,25 +1,27 @@ import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; -import { Detection } from 'react-native-executorch'; +import { Detection, LabelEnum } from 'react-native-executorch'; import { labelColor, labelColorBg } from './utils/colors'; -interface Props { - detections: Detection[]; +interface Props { + detections: Detection[]; scaleX: number; scaleY: number; offsetX: number; offsetY: number; mirrorLabels?: boolean; + containerWidth?: number; } -export default function BoundingBoxes({ +export default function BoundingBoxes({ detections, scaleX, scaleY, offsetX, offsetY, mirrorLabels = false, -}: Props) { + containerWidth, +}: Props) { return ( <> {detections.map((det, i) => { @@ -39,7 +41,7 @@ export default function BoundingBoxes({ top, width, height, - borderColor: labelColor(det.label), + borderColor: labelColor(det.label as string), }, ]} /> @@ -49,13 +51,16 @@ export default function BoundingBoxes({ { left, top: labelTop, - backgroundColor: labelColorBg(det.label), + backgroundColor: labelColorBg(det.label as string), + ...(containerWidth !== undefined && { + maxWidth: containerWidth - left, + }), }, mirrorLabels && { transform: [{ scaleX: -1 }] }, ]} > - {det.label} ({(det.score * 100).toFixed(1)}%) + {String(det.label)} ({(det.score * 100).toFixed(1)}%) diff --git a/apps/computer-vision/components/ImageWithBboxes.tsx b/apps/computer-vision/components/ImageWithBboxes.tsx index 13be8dc385..9b13f314c6 100644 --- a/apps/computer-vision/components/ImageWithBboxes.tsx +++ b/apps/computer-vision/components/ImageWithBboxes.tsx @@ -66,6 +66,7 @@ export default function ImageWithBboxes({ scaleY={scaleY} offsetX={offsetX} offsetY={offsetY} + containerWidth={layout.width} /> ); diff --git a/apps/computer-vision/components/vision_camera/tasks/InstanceSegmentationTask.tsx b/apps/computer-vision/components/vision_camera/tasks/InstanceSegmentationTask.tsx index 829dfb2abe..e68b515946 100644 --- a/apps/computer-vision/components/vision_camera/tasks/InstanceSegmentationTask.tsx +++ b/apps/computer-vision/components/vision_camera/tasks/InstanceSegmentationTask.tsx @@ -11,7 +11,7 @@ import { CocoLabelYolo, } from 'react-native-executorch'; import { Canvas, Image as SkiaImage } from '@shopify/react-native-skia'; -import { labelColor, labelColorBg } from '../utils/colors'; +import { labelColor, labelColorBg } from '../../utils/colors'; import { TaskProps } from './types'; import { buildDisplayInstances, diff --git a/apps/computer-vision/components/vision_camera/tasks/ObjectDetectionTask.tsx b/apps/computer-vision/components/vision_camera/tasks/ObjectDetectionTask.tsx index 9918578b68..deb5368c90 100644 --- a/apps/computer-vision/components/vision_camera/tasks/ObjectDetectionTask.tsx +++ b/apps/computer-vision/components/vision_camera/tasks/ObjectDetectionTask.tsx @@ -128,6 +128,7 @@ export default function ObjectDetectionTask({ scaleY={scale} offsetX={offsetX} offsetY={offsetY} + containerWidth={canvasSize.width} /> ); diff --git a/apps/computer-vision/components/vision_camera/tasks/SegmentationTask.tsx b/apps/computer-vision/components/vision_camera/tasks/SegmentationTask.tsx index 5bdd33b8f1..31dcea51da 100644 --- a/apps/computer-vision/components/vision_camera/tasks/SegmentationTask.tsx +++ b/apps/computer-vision/components/vision_camera/tasks/SegmentationTask.tsx @@ -20,7 +20,7 @@ import { Skia, SkImage, } from '@shopify/react-native-skia'; -import { CLASS_COLORS } from '../utils/colors'; +import { CLASS_COLORS } from '../../utils/colors'; import { TaskProps } from './types'; type SegModelId = diff --git a/apps/computer-vision/components/vision_camera/utils/colors.ts b/apps/computer-vision/components/vision_camera/utils/colors.ts deleted file mode 100644 index 5d59d9171d..0000000000 --- a/apps/computer-vision/components/vision_camera/utils/colors.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '../../utils/colors';