1- import React , { useState } from 'react' ;
1+ import React , { useEffect , useState } from 'react' ;
22import { Image , StyleSheet , View , Text } from 'react-native' ;
33import {
44 Canvas ,
@@ -10,8 +10,6 @@ import {
1010 Rect ,
1111 Group ,
1212} from '@shopify/react-native-skia' ;
13- import type { SegmentedInstance } from 'react-native-executorch' ;
14- import type { LabelEnum } from 'react-native-executorch' ;
1513
1614const INSTANCE_COLORS = [
1715 [ 255 , 87 , 51 , 180 ] ,
@@ -26,39 +24,101 @@ const INSTANCE_COLORS = [
2624 [ 131 , 51 , 255 , 180 ] ,
2725] ;
2826
29- interface Props {
30- imageUri : string ;
31- instances : SegmentedInstance < LabelEnum > [ ] ;
32- imageWidth : number ;
33- imageHeight : number ;
27+ const MAX_MASK_DIM = 256 ;
28+
29+ /** Display-only data — no raw mask buffers. */
30+ export interface DisplayInstance {
31+ bbox : { x1 : number ; y1 : number ; x2 : number ; y2 : number } ;
32+ label : string ;
33+ score : number ;
34+ maskImage : SkImage ;
35+ }
36+
37+ /**
38+ * Convert raw segmentation output into lightweight display instances.
39+ * Call this eagerly (in the forward callback) so raw Uint8Array masks
40+ * can be garbage-collected immediately.
41+ */
42+ export function buildDisplayInstances (
43+ rawInstances : {
44+ bbox : { x1 : number ; y1 : number ; x2 : number ; y2 : number } ;
45+ mask : Uint8Array ;
46+ maskWidth : number ;
47+ maskHeight : number ;
48+ label : string | number ;
49+ score : number ;
50+ } [ ]
51+ ) : DisplayInstance [ ] {
52+ return rawInstances
53+ . map ( ( inst , i ) => {
54+ const color = INSTANCE_COLORS [ i % INSTANCE_COLORS . length ] ;
55+ const img = createMaskImage (
56+ inst . mask ,
57+ inst . maskWidth ,
58+ inst . maskHeight ,
59+ color
60+ ) ;
61+ if ( ! img ) return null ;
62+ return {
63+ bbox : inst . bbox ,
64+ label : String ( inst . label ) ,
65+ score : inst . score ,
66+ maskImage : img ,
67+ } ;
68+ } )
69+ . filter ( ( d ) : d is DisplayInstance => d !== null ) ;
3470}
3571
3672function createMaskImage (
3773 mask : Uint8Array ,
38- width : number ,
39- height : number ,
74+ srcW : number ,
75+ srcH : number ,
4076 color : number [ ]
4177) : SkImage | null {
42- const pixels = new Uint8Array ( width * height * 4 ) ;
43- for ( let j = 0 ; j < mask . length ; j ++ ) {
44- if ( mask [ j ] > 0 ) {
45- pixels [ j * 4 ] = color [ 0 ] ;
46- pixels [ j * 4 + 1 ] = color [ 1 ] ;
47- pixels [ j * 4 + 2 ] = color [ 2 ] ;
48- pixels [ j * 4 + 3 ] = color [ 3 ] ;
78+ const downscale = Math . min ( 1 , MAX_MASK_DIM / Math . max ( srcW , srcH ) ) ;
79+ const dstW = Math . max ( 1 , Math . round ( srcW * downscale ) ) ;
80+ const dstH = Math . max ( 1 , Math . round ( srcH * downscale ) ) ;
81+
82+ const pixels = new Uint8Array ( dstW * dstH * 4 ) ;
83+ const r = color [ 0 ] ,
84+ g = color [ 1 ] ,
85+ b = color [ 2 ] ,
86+ a = color [ 3 ] ;
87+
88+ for ( let dy = 0 ; dy < dstH ; dy ++ ) {
89+ const sy = Math . min ( Math . floor ( dy / downscale ) , srcH - 1 ) ;
90+ for ( let dx = 0 ; dx < dstW ; dx ++ ) {
91+ const sx = Math . min ( Math . floor ( dx / downscale ) , srcW - 1 ) ;
92+ if ( mask [ sy * srcW + sx ] > 0 ) {
93+ const idx = ( dy * dstW + dx ) * 4 ;
94+ pixels [ idx ] = r ;
95+ pixels [ idx + 1 ] = g ;
96+ pixels [ idx + 2 ] = b ;
97+ pixels [ idx + 3 ] = a ;
98+ }
4999 }
50100 }
101+
51102 const data = Skia . Data . fromBytes ( pixels ) ;
52- return Skia . Image . MakeImage (
103+ const image = Skia . Image . MakeImage (
53104 {
54- width,
55- height,
105+ width : dstW ,
106+ height : dstH ,
56107 alphaType : AlphaType . Premul ,
57108 colorType : ColorType . RGBA_8888 ,
58109 } ,
59110 data ,
60- width * 4
111+ dstW * 4
61112 ) ;
113+ data . dispose ( ) ;
114+ return image ;
115+ }
116+
117+ interface Props {
118+ imageUri : string ;
119+ instances : DisplayInstance [ ] ;
120+ imageWidth : number ;
121+ imageHeight : number ;
62122}
63123
64124export default function ImageWithMasks ( {
@@ -75,17 +135,12 @@ export default function ImageWithMasks({
75135 const offsetX = ( layout . width - imageWidth * scale ) / 2 ;
76136 const offsetY = ( layout . height - imageHeight * scale ) / 2 ;
77137
78- const maskImages = instances
79- . map ( ( instance , i ) => {
80- const color = INSTANCE_COLORS [ i % INSTANCE_COLORS . length ] ;
81- return createMaskImage (
82- instance . mask ,
83- instance . maskWidth ,
84- instance . maskHeight ,
85- color
86- ) ;
87- } )
88- . filter ( ( img ) : img is SkImage => img !== null ) ;
138+ // Dispose Skia images when instances are replaced or on unmount
139+ useEffect ( ( ) => {
140+ return ( ) => {
141+ instances . forEach ( ( inst ) => inst . maskImage . dispose ( ) ) ;
142+ } ;
143+ } , [ instances ] ) ;
89144
90145 return (
91146 < View
@@ -108,16 +163,15 @@ export default function ImageWithMasks({
108163 { instances . length > 0 && (
109164 < View style = { styles . overlay } >
110165 < Canvas style = { styles . canvas } >
111- { maskImages . map ( ( maskImg , idx ) => {
112- const inst = instances [ idx ] ;
166+ { instances . map ( ( inst , idx ) => {
113167 const mx = inst . bbox . x1 * scale + offsetX ;
114168 const my = inst . bbox . y1 * scale + offsetY ;
115169 const mw = ( inst . bbox . x2 - inst . bbox . x1 ) * scale ;
116170 const mh = ( inst . bbox . y2 - inst . bbox . y1 ) * scale ;
117171 return (
118172 < SkiaImage
119173 key = { `mask-${ idx } ` }
120- image = { maskImg }
174+ image = { inst . maskImage }
121175 fit = "fill"
122176 x = { mx }
123177 y = { my }
@@ -127,12 +181,12 @@ export default function ImageWithMasks({
127181 ) ;
128182 } ) }
129183
130- { instances . map ( ( instance , idx ) => {
184+ { instances . map ( ( inst , idx ) => {
131185 const color = INSTANCE_COLORS [ idx % INSTANCE_COLORS . length ] ;
132- const bboxX = instance . bbox . x1 * scale + offsetX ;
133- const bboxY = instance . bbox . y1 * scale + offsetY ;
134- const bboxW = ( instance . bbox . x2 - instance . bbox . x1 ) * scale ;
135- const bboxH = ( instance . bbox . y2 - instance . bbox . y1 ) * scale ;
186+ const bboxX = inst . bbox . x1 * scale + offsetX ;
187+ const bboxY = inst . bbox . y1 * scale + offsetY ;
188+ const bboxW = ( inst . bbox . x2 - inst . bbox . x1 ) * scale ;
189+ const bboxH = ( inst . bbox . y2 - inst . bbox . y1 ) * scale ;
136190
137191 return (
138192 < Group key = { `bbox-${ idx } ` } >
@@ -150,10 +204,10 @@ export default function ImageWithMasks({
150204 } ) }
151205 </ Canvas >
152206
153- { instances . map ( ( instance , idx ) => {
207+ { instances . map ( ( inst , idx ) => {
154208 const color = INSTANCE_COLORS [ idx % INSTANCE_COLORS . length ] ;
155- const bboxX = instance . bbox . x1 * scale + offsetX ;
156- const bboxY = instance . bbox . y1 * scale + offsetY ;
209+ const bboxX = inst . bbox . x1 * scale + offsetX ;
210+ const bboxY = inst . bbox . y1 * scale + offsetY ;
157211
158212 return (
159213 < View
@@ -168,8 +222,7 @@ export default function ImageWithMasks({
168222 ] }
169223 >
170224 < Text style = { styles . labelText } >
171- { String ( instance . label ) || 'Unknown' } { ' ' }
172- { ( instance . score * 100 ) . toFixed ( 0 ) } %
225+ { inst . label || 'Unknown' } { ( inst . score * 100 ) . toFixed ( 0 ) } %
173226 </ Text >
174227 </ View >
175228 ) ;
0 commit comments