44//!
55//! Runs YOLO ONNX models in the browser on WebGPU via ONNX Runtime Web (bridged
66//! by [`ort-web`](https://ort.pyke.io/backends/web)), reusing the core crate's
7- //! preprocessing/postprocessing so results match the native and Python paths .
7+ //! preprocessing/postprocessing so results match the native path .
88//! The published npm package wraps this behind a small `YOLO` class. The whole
99//! crate is gated to `wasm32`; elsewhere it compiles to an empty library.
1010#![ cfg( target_arch = "wasm32" ) ]
@@ -26,14 +26,17 @@ use ultralytics_inference::postprocessing::postprocess;
2626use ultralytics_inference:: preprocessing:: {
2727 preprocess_image_center_crop, preprocess_image_with_precision,
2828} ;
29- use ultralytics_inference:: results:: { Results , SemanticMask , Speed } ;
29+ use ultralytics_inference:: results:: Speed ;
3030use ultralytics_inference:: visualizer:: color:: Color ;
3131use ultralytics_inference:: visualizer:: skeleton:: {
3232 KPT_COLOR_INDICES , LIMB_COLOR_INDICES , SKELETON ,
3333} ;
3434use ultralytics_inference:: { InferenceConfig , Task } ;
3535
36+ use payload:: JsResults ;
37+
3638mod onnx_meta;
39+ mod payload;
3740
3841/// Default inference image size used when a model does not record `imgsz` in its
3942/// metadata. Mirrors the native crate's fallback.
@@ -206,8 +209,7 @@ impl YoloModel {
206209 ///
207210 /// Initializes the accelerated backend on first use, reads the embedded
208211 /// Ultralytics metadata from the model bytes, then commits an ONNX Runtime
209- /// session on the requested execution provider. Equivalent to Python's
210- /// `YOLO('model.onnx')`.
212+ /// session on the requested execution provider.
211213 ///
212214 /// `device` is `"webgpu"` or `"cpu"` (the browser picks the GPU adapter
213215 /// automatically). WebGPU is registered with `error_on_failure`, so if the
@@ -452,236 +454,9 @@ fn to_js<T: Serialize>(value: &T, what: &str) -> Result<JsValue, JsError> {
452454 . map_err ( |e| JsError :: new ( & format ! ( "failed to serialize {what}: {e}" ) ) )
453455}
454456
455- /// One detected box, mirroring `Boxes` in the Ultralytics API (pixel `xyxy`).
456- /// `color` is the Ultralytics palette color for the class (`#rrggbb`).
457- #[ derive( Serialize ) ]
458- struct JsBox {
459- x1 : f32 ,
460- y1 : f32 ,
461- x2 : f32 ,
462- y2 : f32 ,
463- conf : f32 ,
464- cls : usize ,
465- name : String ,
466- color : String ,
467- }
468-
469- /// One oriented box (`xywhr` + score), mirroring `Obb`.
470- #[ derive( Serialize ) ]
471- struct JsObb {
472- x : f32 ,
473- y : f32 ,
474- w : f32 ,
475- h : f32 ,
476- angle : f32 ,
477- conf : f32 ,
478- cls : usize ,
479- name : String ,
480- color : String ,
481- }
482-
483- /// Keypoints for one detection (`[[x, y, conf], ...]`), with the instance color.
484- #[ derive( Serialize ) ]
485- struct JsKeypoints {
486- points : Vec < [ f32 ; 3 ] > ,
487- color : String ,
488- }
489-
490- /// Per-stage timing in ms, mirroring `Speed` (decode+letterbox / inference+sync / decode+NMS).
491- #[ derive( Serialize ) ]
492- struct JsSpeed {
493- preprocess : f64 ,
494- inference : f64 ,
495- postprocess : f64 ,
496- }
497-
498- /// Classification probabilities, mirroring `Probs`.
499- #[ derive( Serialize ) ]
500- struct JsProbs {
501- top1 : usize ,
502- top5 : Vec < usize > ,
503- top1conf : f32 ,
504- top5conf : Vec < f32 > ,
505- name : String ,
506- color : String ,
507- }
508-
509- /// JS-facing results payload, mirroring the Ultralytics `Results` API. Per-task
510- /// fields are empty/`null` when unused.
511- #[ derive( Serialize ) ]
512- struct JsResults {
513- task : String ,
514- width : u32 ,
515- height : u32 ,
516- boxes : Vec < JsBox > ,
517- obb : Vec < JsObb > ,
518- keypoints : Vec < JsKeypoints > ,
519- probs : Option < JsProbs > ,
520- /// Segment masks as a translucent colored `RGBA` overlay (`width*height*4`);
521- /// empty for other tasks. A `Uint8Array`, drawable straight onto a canvas.
522- #[ serde( with = "serde_bytes" ) ]
523- masks : Vec < u8 > ,
524- /// Semantic class id per pixel, little-endian `u16` (`width*height*2` bytes),
525- /// row-major; empty for other tasks. The TS wrapper reinterprets it as a
526- /// `Uint16Array`. The IGNORE sentinel `65535` marks background or
527- /// class-filtered pixels.
528- #[ serde( with = "serde_bytes" ) ]
529- semantic : Vec < u8 > ,
530- /// Per-stage timing in ms: `{ preprocess, inference, postprocess }`.
531- speed : JsSpeed ,
532- }
533-
534- impl JsResults {
535- /// Convert core [`Results`] into the serializable JS payload, labeling it with
536- /// the model's declared `task` (the caller already holds it, so there is no
537- /// need to guess it back from which result fields are populated).
538- fn from_results ( r : & Results , task : Task ) -> Self {
539- // Class id -> display name and palette color, shared by every detection type.
540- let name = |c : usize | r. names . get ( & c) . cloned ( ) . unwrap_or_default ( ) ;
541- let hex = |c : usize | Color :: from_index ( c) . to_hex ( ) ;
542-
543- let boxes = r. boxes . as_ref ( ) . map_or_else ( Vec :: new, |b| {
544- let ( xyxy, conf, cls) = ( b. xyxy ( ) , b. conf ( ) , b. cls ( ) ) ;
545- ( 0 ..b. len ( ) )
546- . map ( |i| {
547- let c = cls[ i] as usize ;
548- JsBox {
549- x1 : xyxy[ [ i, 0 ] ] ,
550- y1 : xyxy[ [ i, 1 ] ] ,
551- x2 : xyxy[ [ i, 2 ] ] ,
552- y2 : xyxy[ [ i, 3 ] ] ,
553- conf : conf[ i] ,
554- cls : c,
555- name : name ( c) ,
556- color : hex ( c) ,
557- }
558- } )
559- . collect ( )
560- } ) ;
561-
562- let obb = r. obb . as_ref ( ) . map_or_else ( Vec :: new, |o| {
563- let ( xywhr, conf, cls) = ( o. xywhr ( ) , o. conf ( ) , o. cls ( ) ) ;
564- ( 0 ..conf. len ( ) )
565- . map ( |i| {
566- let c = cls[ i] as usize ;
567- JsObb {
568- x : xywhr[ [ i, 0 ] ] ,
569- y : xywhr[ [ i, 1 ] ] ,
570- w : xywhr[ [ i, 2 ] ] ,
571- h : xywhr[ [ i, 3 ] ] ,
572- angle : xywhr[ [ i, 4 ] ] ,
573- conf : conf[ i] ,
574- cls : c,
575- name : name ( c) ,
576- color : hex ( c) ,
577- }
578- } )
579- . collect ( )
580- } ) ;
581-
582- let keypoints = r. keypoints . as_ref ( ) . map_or_else ( Vec :: new, |k| {
583- let ( n, npt) = ( k. data . shape ( ) [ 0 ] , k. data . shape ( ) [ 1 ] ) ;
584- ( 0 ..n)
585- . map ( |i| JsKeypoints {
586- points : ( 0 ..npt)
587- . map ( |j| [ k. data [ [ i, j, 0 ] ] , k. data [ [ i, j, 1 ] ] , k. data [ [ i, j, 2 ] ] ] )
588- . collect ( ) ,
589- // Skeleton uses the matching detection's color.
590- color : boxes. get ( i) . map_or_else ( || hex ( 0 ) , |b| b. color . clone ( ) ) ,
591- } )
592- . collect ( )
593- } ) ;
594-
595- let probs = r. probs . as_ref ( ) . map ( |p| JsProbs {
596- top1 : p. top1 ( ) ,
597- top5 : p. top5 ( ) ,
598- top1conf : p. top1conf ( ) ,
599- top5conf : p. top5conf ( ) ,
600- name : name ( p. top1 ( ) ) ,
601- color : hex ( p. top1 ( ) ) ,
602- } ) ;
603-
604- Self {
605- task : task. as_str ( ) . to_owned ( ) ,
606- width : r. orig_shape . 1 ,
607- height : r. orig_shape . 0 ,
608- boxes,
609- obb,
610- keypoints,
611- probs,
612- masks : build_mask_overlay ( r) ,
613- semantic : r. semantic_mask . as_ref ( ) . map_or_else ( Vec :: new, |sem| {
614- let mut bytes = Vec :: with_capacity ( sem. data . len ( ) * 2 ) ;
615- for & v in & sem. data {
616- bytes. extend_from_slice ( & v. to_le_bytes ( ) ) ;
617- }
618- bytes
619- } ) ,
620- speed : JsSpeed {
621- preprocess : r. speed . preprocess . unwrap_or ( 0.0 ) ,
622- inference : r. speed . inference . unwrap_or ( 0.0 ) ,
623- postprocess : r. speed . postprocess . unwrap_or ( 0.0 ) ,
624- } ,
625- }
626- }
627- }
628-
629- /// Write one colored RGBA pixel at `(x, y)` into a `width`-wide buffer.
630- fn put ( buf : & mut [ u8 ] , w : usize , x : usize , y : usize , c : Color , a : u8 ) {
631- let i = ( y * w + x) * 4 ;
632- buf[ i..i + 4 ] . copy_from_slice ( & [ c. 0 , c. 1 , c. 2 , a] ) ;
633- }
634-
635- /// Build a translucent colored RGBA overlay (`width*height*4`) from the segment
636- /// instance masks or the semantic class map, colored by the class palette.
637- /// Empty for other tasks or if the mask resolution does not match the image.
638- fn build_mask_overlay ( r : & Results ) -> Vec < u8 > {
639- let ( h, w) = ( r. orig_shape . 0 as usize , r. orig_shape . 1 as usize ) ;
640- let mut buf = vec ! [ 0u8 ; w * h * 4 ] ;
641-
642- // Segment: per-instance binary masks, each colored by its detection class.
643- if let ( Some ( masks) , Some ( boxes) ) = ( & r. masks , & r. boxes ) {
644- let ( n, mh, mw) = masks. data . dim ( ) ;
645- if mh != h || mw != w {
646- return Vec :: new ( ) ;
647- }
648- let cls = boxes. cls ( ) ;
649- for i in 0 ..n. min ( cls. len ( ) ) {
650- let c = Color :: from_index ( cls[ i] as usize ) ;
651- for y in 0 ..h {
652- for x in 0 ..w {
653- if masks. data [ [ i, y, x] ] > 0.5 {
654- put ( & mut buf, w, x, y, c, 140 ) ;
655- }
656- }
657- }
658- }
659- return buf;
660- }
661-
662- // Semantic: per-pixel class map, blended 50/50 like the native renderer.
663- if let Some ( sem) = & r. semantic_mask {
664- if sem. data . dim ( ) != ( h, w) {
665- return Vec :: new ( ) ;
666- }
667- for y in 0 ..h {
668- for x in 0 ..w {
669- // Filtered-out classes carry the IGNORE sentinel; leave transparent.
670- let cls = sem. data [ [ y, x] ] ;
671- if cls != SemanticMask :: IGNORE {
672- put ( & mut buf, w, x, y, Color :: from_index ( cls as usize ) , 128 ) ;
673- }
674- }
675- }
676- return buf;
677- }
678-
679- Vec :: new ( )
680- }
681-
682457/// The Ultralytics pose drawing scheme: skeleton connectivity plus the per-limb
683458/// and per-keypoint palette colors. Lets the JS annotator draw pose exactly like
684- /// the native/Python renderer without duplicating any palette.
459+ /// the native renderer without duplicating any palette.
685460#[ derive( Serialize ) ]
686461struct PoseScheme {
687462 /// Keypoint index pairs connected by limbs.
0 commit comments