Skip to content

feat(detection): in-browser ONNX/YOLO object detection (#902)#905

Open
giswqs wants to merge 5 commits into
mainfrom
feat/issue-902-onnx-yolo-detection
Open

feat(detection): in-browser ONNX/YOLO object detection (#902)#905
giswqs wants to merge 5 commits into
mainfrom
feat/issue-902-onnx-yolo-detection

Conversation

@giswqs

@giswqs giswqs commented Jun 26, 2026

Copy link
Copy Markdown
Member

What

Adds an Object Detection toolbox that complements the existing AI Segmentation, addressing the request in #902 for custom ONNX/YOLO detection models with multiple classes, where each class becomes a layer.

Unlike AI Segmentation (which proxies SAM3 through the Python sidecar), detection runs fully in the browser with onnxruntime-web, so it works in both the web and desktop builds with no server.

How it works

  1. Pick a GeoTIFF and a YOLO model exported to ONNX, optionally list class names in order, and set confidence / IoU / input size.
  2. The raster is decoded with geotiff.js (reusing readRasterData), letterboxed into the model input, and run client-side (single-threaded WASM, no cross-origin-isolation requirement).
  3. The output is decoded for the standard Ultralytics layouts (YOLOv8/v11 [1, 4+nc, anchors] and YOLOv5 [1, anchors, 5+nc], auto-detected), followed by per-class non-maximum suppression.
  4. Detected boxes are georeferenced via the raster geotransform, tagged with the source CRS, reprojected to WGS84 (reusing the DuckDB-WASM reprojection path), and each detected class is added as its own GeoJSON layer with class and score properties.

Files

  • packages/processing/src/object-detection.ts - the in-browser detection engine (preprocess, lazy ORT inference, decode + NMS).
  • apps/geolibre-desktop/src/components/processing/ObjectDetectionDialog.tsx - the dialog (georeference + reproject + per-class layers).
  • Store flag, Processing menu entry, command-palette command, lazy mount, ui-profile feature, and i18n strings.

Notes

  • onnxruntime-web loads its WASM from the jsdelivr CDN, which is already in the Tauri CSP allowlist.
  • Inference is single-threaded by design to avoid needing COOP/COEP headers.

Verification

Driven in the real app with Playwright in both light and dark themes, using a real yolov8n.onnx (COCO) over a georeferenced GeoTIFF. Detection produced the expected boxes, correctly georeferenced and reprojected onto the basemap, with one named layer per detected class. npm run build, the full frontend test suite (1637 passing), and pre-commit (eslint + build) are green.

This is an initial MVP slice; follow-ups could add embedded class-name metadata parsing, draw-a-box prompts, and a model library.

Fixes #902

Summary by CodeRabbit

  • New Features
    • Added an Object Detection tool that runs client-side YOLO inference on GeoTIFF imagery using ONNX models (built-in COCO models can be downloaded or you can supply your own).
    • Added an Object Detection command to the Processing menu and toolbar to open the new dialog.
    • Detects objects, georeferences results, creates one map layer per detected class, and auto-zooms to the output.
  • Bug Fixes
    • Improved desktop shell resilience by safely handling lazy loading of the Object Detection dialog.
  • Tests
    • Added a version check ensuring the pinned ONNX Runtime Web version matches the dependency.

Add an Object Detection toolbox that complements AI Segmentation. A
user-supplied YOLO model exported to ONNX runs fully in the browser via
onnxruntime-web against a chosen GeoTIFF; detected boxes are georeferenced
with the raster's geotransform, reprojected to WGS84, and each class is
added as its own GeoJSON layer.

- packages/processing/object-detection.ts: letterbox preprocess, lazy
  single-threaded onnxruntime-web inference, YOLOv5/v8/v11 output decode +
  per-class NMS, returning source-pixel detections.
- ObjectDetectionDialog: pick imagery + .onnx model, optional class names,
  confidence/IoU/input-size, georeference + reproject + per-class layers.
- Wire store flag, Processing menu, command palette, lazy mount, and i18n.

Fixes #902
@netlify

netlify Bot commented Jun 26, 2026

Copy link
Copy Markdown

Deploy Preview for geolibre-app ready!

Name Link
🔨 Latest commit a7e7c77
🔍 Latest deploy log https://app.netlify.com/projects/geolibre-app/deploys/6a3df7e6716948000870a3c4
😎 Deploy Preview https://deploy-preview-905--geolibre-app.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0db7766b-7536-4ba1-8d8f-b9f766defa0d

📥 Commits

Reviewing files that changed from the base of the PR and between 2eea4d0 and a7e7c77.

📒 Files selected for processing (5)
  • apps/geolibre-desktop/src/components/layout/toolbar/ProcessingMenu.tsx
  • apps/geolibre-desktop/src/components/processing/ObjectDetectionDialog.tsx
  • apps/geolibre-desktop/src/lib/detection-models.ts
  • packages/processing/src/object-detection.ts
  • tests/object-detection.test.ts

📝 Walkthrough

Walkthrough

Adds client-side YOLO object detection to the desktop app. Users can open a new object detection dialog, choose a GeoTIFF and ONNX model, run inference, and create georeferenced GeoJSON layers for each detected class.

Changes

Object detection feature

Layer / File(s) Summary
Detection runtime and preprocessing
packages/processing/package.json, apps/geolibre-desktop/src/lib/detection-models.ts, packages/processing/src/object-detection.ts, packages/processing/src/index.ts, tests/object-detection.test.ts
Adds the ONNX runtime dependency, model metadata and downloader, detection types, runtime loading, raster normalization, preprocessing, IoU, NMS, YOLO decoding, public re-exports, and a version-pinning test used by object detection.
Dialog conversion and run flow
apps/geolibre-desktop/src/components/processing/ObjectDetectionDialog.tsx
Adds GeoTIFF CRS handling, class labeling, detection-to-GeoJSON conversion, file selection, dialog state reset, and the async run flow that creates layers and fits the map.
Desktop entry points and store state
packages/core/src/store.ts, apps/geolibre-desktop/src/lib/ui-profile.ts, apps/geolibre-desktop/src/i18n/locales/en.json, apps/geolibre-desktop/src/components/layout/...
Adds the object detection store flag and action, menu catalog and localization entries, processing commands and menu item wiring, and lazy dialog mounting in the desktop shell.

Sequence Diagram(s)

Detection run

sequenceDiagram
  participant ObjectDetectionDialog
  participant fetchDetectionModel
  participant detectObjects
  participant AppStore
  participant mapControllerRef
  ObjectDetectionDialog->>fetchDetectionModel: download ONNX bytes
  ObjectDetectionDialog->>detectObjects: raster bytes, model bytes, options
  detectObjects-->>ObjectDetectionDialog: Detection[]
  ObjectDetectionDialog->>AppStore: add GeoJSON layer per class
  ObjectDetectionDialog->>mapControllerRef: fit to first created layer
Loading

Opening the dialog

sequenceDiagram
  participant TopToolbar
  participant ProcessingMenu
  participant AppStore
  participant DesktopShell
  participant ObjectDetectionDialog
  TopToolbar->>AppStore: setObjectDetectionOpen(true)
  ProcessingMenu->>AppStore: setObjectDetectionOpen(true)
  DesktopShell->>ObjectDetectionDialog: lazy load and mount under Suspense
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hopped through pixels, bright and keen,
and found a model in the GeoTIFF scene.
I boxed each bunny-shaped surprise,
then planted layers where the classes rise.
Hooray—my carrots now map the skies!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding in-browser ONNX/YOLO object detection.
Linked Issues check ✅ Passed The PR adds custom ONNX/YOLO detection with multiple classes and separate layers per class, matching #902.
Out of Scope Changes check ✅ Passed The changes stay focused on detection UI, processing, model loading, and tests, with no obvious unrelated additions.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-902-onnx-yolo-detection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread apps/geolibre-desktop/src/components/processing/ObjectDetectionDialog.tsx Outdated
Comment thread packages/processing/src/object-detection.ts Outdated
Comment thread packages/processing/src/object-detection.ts Outdated
Comment thread packages/processing/src/object-detection.ts Outdated
Comment thread packages/processing/src/object-detection.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/geolibre-desktop/src/components/layout/toolbar/ProcessingMenu.tsx (1)

85-89: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Include object detection in showGeolibreActions.

If a UI profile leaves processing.objectDetection as the only visible GeoLibre action, showGeolibre stays false and the whole parent submenu disappears, so the new item is unreachable despite its own visibility check below.

Suggested fix
   const showGeolibreActions =
     show("processing.geocode") ||
     show("processing.modelBuilder") ||
-    (!mobile && show("processing.segmentation"));
+    (!mobile && show("processing.segmentation")) ||
+    show("processing.objectDetection");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/components/layout/toolbar/ProcessingMenu.tsx`
around lines 85 - 89, `showGeolibreActions` in `ProcessingMenu` is missing the
`processing.objectDetection` visibility check, which can hide the entire
GeoLibre submenu when object detection is the only enabled action. Update the
`showGeolibreActions` condition to include the object detection flag alongside
the existing geocode, modelBuilder, and segmentation checks, so `showGeolibre`
stays true whenever the new action is available.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/components/layout/toolbar/ProcessingMenu.tsx`:
- Around line 506-509: The ProcessingMenu object detection item is incorrectly
gated by the mobile check, which hides a browser/client-side feature that should
stay available on web-capable clients. Update the conditional around the
DropdownMenuItem in ProcessingMenu so it no longer depends on !mobile, while
keeping the existing show("processing.objectDetection") permission/feature gate
intact. Use the surrounding objectDetection menu entry and
setObjectDetectionOpen handler as the location to make this adjustment.

In `@apps/geolibre-desktop/src/components/processing/ObjectDetectionDialog.tsx`:
- Around line 414-419: The ObjectDetectionDialog inputSize handler only enforces
the lower bound, so user-entered values above the Input max can still reach
detectObjects and cause oversized allocations. Update the onChange logic in
ObjectDetectionDialog to mirror the confidence/IoU clamping pattern: validate
parsed input and reject or clamp values outside the full 32–4096 range before
calling setInputSize.

In `@packages/processing/package.json`:
- Line 37: The `onnxruntime-web` dependency and the WASM CDN URL used by
`object-detection.ts` must stay in sync, since the package range currently
allows newer 1.x versions while the runtime still fetches artifacts from
`onnxruntime-web@1.27.0`. Update `packages/processing/package.json` to pin
`onnxruntime-web` to `1.27.0`, or refactor `object-detection.ts` and the package
metadata to read from a shared version constant so both references always match.

In `@packages/processing/src/object-detection.ts`:
- Around line 98-107: The tensor normalization in object-detection.ts is still
using raw sampled band values, so RasterData.nodata and other invalid pixels can
skew scaling and leak into the model. Update the normalization path around the
sampling logic and the helper that prepares r/g/b so it skips non-finite values
and any nodata samples when computing max/divisor, and replaces invalid pixels
with the existing padding value before returning the tensor inputs. Make sure
the same fix is applied anywhere the sampled bands are normalized, including the
code path referenced by the duplicated logic near the later lines.
- Around line 313-315: Validate the detection options before tensor allocation
in the object-detection flow by adding explicit guards near the inputSize,
confidenceThreshold, and iouThreshold defaults in object-detection.ts. Ensure
inputSize is a finite positive safe integer within a reasonable upper bound, and
clamp or reject confidenceThreshold and iouThreshold unless they are finite
values in the [0, 1] range. Apply these checks before any use in tensor
shape/allocation logic so the detection pipeline fails fast with a clear
validation error instead of exhausting memory or producing invalid filtering
behavior.

---

Outside diff comments:
In `@apps/geolibre-desktop/src/components/layout/toolbar/ProcessingMenu.tsx`:
- Around line 85-89: `showGeolibreActions` in `ProcessingMenu` is missing the
`processing.objectDetection` visibility check, which can hide the entire
GeoLibre submenu when object detection is the only enabled action. Update the
`showGeolibreActions` condition to include the object detection flag alongside
the existing geocode, modelBuilder, and segmentation checks, so `showGeolibre`
stays true whenever the new action is available.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6928ebb0-b455-4b9a-b490-39e629dc46d2

📥 Commits

Reviewing files that changed from the base of the PR and between 7436225 and 9ad9735.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
  • apps/geolibre-desktop/src/components/layout/TopToolbar.tsx
  • apps/geolibre-desktop/src/components/layout/toolbar/ProcessingMenu.tsx
  • apps/geolibre-desktop/src/components/processing/ObjectDetectionDialog.tsx
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/lib/ui-profile.ts
  • packages/core/src/store.ts
  • packages/processing/package.json
  • packages/processing/src/index.ts
  • packages/processing/src/object-detection.ts

Comment thread apps/geolibre-desktop/src/components/layout/toolbar/ProcessingMenu.tsx Outdated
Comment thread packages/processing/package.json Outdated
Comment thread packages/processing/src/object-detection.ts Outdated
Comment thread packages/processing/src/object-detection.ts
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Reviewed packages/processing/src/object-detection.ts (355 lines, new), apps/geolibre-desktop/src/components/processing/ObjectDetectionDialog.tsx (454 lines, new), and the store/menu/i18n wiring. Checked surrounding context: raster-client.ts (geotransform sign convention), duckdb-vector-loader.ts (reprojection path), store.ts (persistence), and tauri.conf.json (CSP).


Bugs

Finding Confidence
B1 — inputSize onChange handler does not enforce the max={4096} cap. HTML max is advisory; a user can type 65536 and preprocess() will attempt to allocate ~52 GB of Float32 data, crashing the tab. Fix: add Math.min(4096, …) to the handler. → comment High
B2 — Dark-image divisor misidentification in rgbBands. The function samples at most 4096 pixels to decide 0–255 vs 0–1 range. A nearly-black 8-bit image can have all sampled values ≤ 1.5, setting divisor = 1 and feeding raw 0–255 values into the model. The dialog silently shows "No objects detected" with no hint that normalisation failed. → comment Medium
B3 — Layout auto-detection is ambiguous when d1 === d2. d1 < d2 is false on a tie, so the tensor is silently decoded as YOLOv5 instead of v8. This can't happen with standard Ultralytics exports at normal sizes, but d1 <= d2 and a comment would make the tie-break intent explicit. → comment Low

Performance

Finding Confidence
P1 — Preprocessing blocks the main thread. The bilinear-resize loop is pure JS running inputSize² iterations synchronously. At 640 (~50–100 ms) it is barely perceptible; at 1280 (~400 ms) the UI freezes noticeably; at the UI-allowed maximum of 4096 (~4 s) the tab is fully unresponsive. No yield or Worker is used. → comment High

Security

No issues found. The model is user-supplied and parsed entirely within the ORT WASM sandbox in the browser, with no server-side surface. The CDN wasmPaths URL is pinned to an exact ORT version, and cdn.jsdelivr.net/npm/ is already in the Tauri CSP script-src allowlist.

Quality

Finding Confidence
Q1 — InferenceSession created fresh every run, never released. ORT Web WASM memory is freed by JS GC non-deterministically; for repeated runs with large models this can spike heap pressure. Caching the session keyed on the model ArrayBuffer identity and calling session.release() on eviction would fix it. → comment Medium
Q2 — outputNames[0] assumed to be the detection head. Some YOLO exports (baked-in NMS, --simplify) emit multiple outputs. If the first is metadata, decodeYolo throws a generic shape error with no indication of what outputs the model actually produced. Including session.outputNames in the error would save debugging time. → comment Medium

CLAUDE.md

  • Geotransform math in detectionsToFeatureCollection is correct: resX/resY are stored as positive values (via Math.abs in readRasterData), and the north/south formula mirrors the same convention used by clipByExtent and other client raster tools.
  • New UI strings are in en.json as the source of truth, with t() used throughout — no hardcoded English strings.
  • New menu entry respects !mobile and show() profile guards, consistent with the adjacent AI Segmentation entry.
  • objectDetectionOpen is in ui state but excluded from partialize — correctly not persisted or tracked in undo history.
  • No direct MapLibre mutations; the dialog goes through addGeoJsonLayer + store state as required.

Overall: The architecture is clean and the implementation follows the established pattern for client-side tools well. B1 (inputSize crash) and P1 (main-thread freeze) are the most important to address before shipping to users; the rest are lower-priority follow-ups.

- object-detection: validate inputSize/confidence/IoU before allocating the
  inference tensor, fail fast instead of OOMing or returning nonsense.
- object-detection: release the ORT session in a finally block so repeated
  runs do not leak WASM heap memory.
- object-detection: throw with the available output names when the picked
  output tensor is missing (multi-output / NMS-baked exports).
- object-detection: ignore NoData/non-finite pixels when choosing the
  normalisation divisor, scale higher-bit-depth data by its own max, and
  substitute the padding value for invalid samples (clamped to 0-1).
- object-detection: prefer the v8 layout on a d1 === d2 tie (true v5 has
  d1 >> d2) and document the ambiguity.
- ObjectDetectionDialog: clamp inputSize to the 4096 upper bound, not just the
  lower bound.
- ProcessingMenu: drop the `!mobile` gate; detection runs client-side and
  should stay available on web/mobile clients.
- pin onnxruntime-web to 1.27.0 to match the hardcoded WASM CDN version.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/geolibre-desktop/src/components/processing/ObjectDetectionDialog.tsx (1)

161-168: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Don’t clear running while a previous detection may still be active.

If the user closes and reopens the dialog before handleRun finishes, Line 167 marks the dialog idle even though inference is still running. That enables a second run and can double the WASM/tensor memory pressure. Let the existing finally clear running.

Proposed fix
-    // Reset transient state so a re-opened dialog never shows a stale error,
-    // result, or a `running` spinner left over from a previous session.
+    // Reset transient messages so a re-opened dialog never shows stale feedback.
+    // Do not clear `running`: an async detection may still be in flight.
     setError(null);
     setResultMessage(null);
-    setRunning(false);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/components/processing/ObjectDetectionDialog.tsx`
around lines 161 - 168, The open-state reset in ObjectDetectionDialog’s
useEffect is clearing the running flag too early, which can make a still-active
detection look idle and allow a duplicate run. Update the dialog reset logic so
it only clears transient UI state like error and resultMessage when open
changes, and leave running to be managed exclusively by handleRun’s finally
block. Use the ObjectDetectionDialog component and its useEffect/handleRun flow
to locate the change.
♻️ Duplicate comments (1)
packages/processing/src/object-detection.ts (1)

109-119: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Compute the normalization divisor across all sampled channels.

Line 113 samples only r, but Line 203 divides R/G/B by the same divisor. RGB rasters with a dark/NoData-heavy red band and bright green/blue bands will choose divisor = 1, then clamp valid 8-bit green/blue values to 1, silently distorting model input. Sample r, g, and b when choosing max.

Proposed fix
-  const step = Math.max(1, Math.floor(r.length / 4096));
-  for (let i = 0; i < r.length; i += step) {
-    const v = r[i];
-    if (!Number.isFinite(v) || v === nodata) continue;
-    if (v > max) max = v;
+  const channels = [r, g, b];
+  const step = Math.max(1, Math.floor(r.length / 4096));
+  for (const band of channels) {
+    for (let i = 0; i < band.length; i += step) {
+      const v = band[i];
+      if (!Number.isFinite(v) || v === nodata) continue;
+      if (v > max) max = v;
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/processing/src/object-detection.ts` around lines 109 - 119, The
normalization divisor in object detection is currently derived only from the red
band, which can produce the wrong scale when green or blue carry higher valid
values. Update the sampling logic in the object-detection path to inspect all
three channels used later in the same function (the RGB arrays in
object-detection.ts) when computing max and divisor, so the chosen normalization
matches the full image data before the R/G/B division step.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/geolibre-desktop/src/components/processing/ObjectDetectionDialog.tsx`:
- Around line 161-168: The open-state reset in ObjectDetectionDialog’s useEffect
is clearing the running flag too early, which can make a still-active detection
look idle and allow a duplicate run. Update the dialog reset logic so it only
clears transient UI state like error and resultMessage when open changes, and
leave running to be managed exclusively by handleRun’s finally block. Use the
ObjectDetectionDialog component and its useEffect/handleRun flow to locate the
change.

---

Duplicate comments:
In `@packages/processing/src/object-detection.ts`:
- Around line 109-119: The normalization divisor in object detection is
currently derived only from the red band, which can produce the wrong scale when
green or blue carry higher valid values. Update the sampling logic in the
object-detection path to inspect all three channels used later in the same
function (the RGB arrays in object-detection.ts) when computing max and divisor,
so the chosen normalization matches the full image data before the R/G/B
division step.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 50d4925a-633a-46c9-a583-4d8fa953071a

📥 Commits

Reviewing files that changed from the base of the PR and between 9ad9735 and b077190.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • apps/geolibre-desktop/src/components/layout/toolbar/ProcessingMenu.tsx
  • apps/geolibre-desktop/src/components/processing/ObjectDetectionDialog.tsx
  • packages/processing/package.json
  • packages/processing/src/object-detection.ts

Comment thread packages/processing/src/object-detection.ts
Comment thread packages/processing/src/object-detection.ts Outdated
Comment thread packages/processing/src/object-detection.ts
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Reviewed object-detection.ts (417 lines), ObjectDetectionDialog.tsx (456 lines), and the supporting plumbing changes (store, menus, i18n, ui-profile, lock file). Context reads: raster-client.ts (confirmed resY = Math.abs(...) so the north/south georeferencing is correct), SegmentationDialog.tsx (established baseline for dialog patterns), tauri-io.ts (confirmed openLocalDataFileWithFallback returns plain ArrayBuffer for binary reads).


Bugs

# Location Finding Confidence
1 object-detection.ts:303-315 YOLOv5 objectness/class scores compared as raw logits. at(4, i) and at(classOffset + c, i) for the v5 layout may be raw logits (range −∞ to +∞). The threshold guard and the score = bestProb * objectness product are only meaningful for probability values. YOLOv8/v11 outputs are sigmoid-activated at export time; YOLOv5 models exported without --simplify (or outside Ultralytics) are not. The fix is sigmoid(logit) on the v5 path, or an explicit doc note that only simplified/activated v5 exports are supported. Medium-high
2 object-detection.ts:100-114 All-NoData sample leaves max = 0, causing divisor = 1. The sampler steps over floor(length / 4096) pixels. If valid data falls entirely between steps, max stays at 0 and the heuristic selects divisor = 1 (the pre-normalised branch). An 8-bit raster then feeds raw 0–255 values into the model tensor. See inline suggestion for a hasValid guard. Low

Quality

# Location Finding Confidence
3 object-detection.ts:62-81 loadOrt uses a boolean flag instead of a promise singleton. Concurrent calls are safe in JS's event loop, but the pattern is fragile and non-obvious. A let ortPromise: Promise<...> | null singleton is the idiomatic fix (also eliminates the async keyword from the function). Medium
4 ObjectDetectionDialog.tsx:235-248 Layers are added to the map even when the dialog is closed mid-run. Closing the dialog resets the spinner via useEffect, but the handleRun closure continues to completion and calls addGeoJsonLayer. Matches SegmentationDialog behaviour — not a regression, but worth a cancelled ref guard. Low
5 ObjectDetectionDialog.tsx:244-247 fitLayer targets only the first-class layer. When multiple classes are detected, the map fits only the first one added; later classes whose boxes fall outside it are not framed. Consider fitting over all detected features. Also a minor nit: useAppStore.getState() is called inside a React callback to find the freshly-added layer by ID — works, but bypasses the reactive cycle. Low (nit)

Security / Performance / CLAUDE.md

  • CDN dependency for WASM: onnxruntime-web fetches its .wasm from cdn.jsdelivr.net at runtime. The CSP already allows https://cdn.jsdelivr.net/npm/, so this is not a security issue. It is a soft availability concern for offline use — documented trade-off.
  • ONNX model held in state indefinitely: modelBytes: ArrayBuffer | null persists in useState for the lifetime of DesktopShell (always mounted). A multi-hundred-MB ONNX model occupies heap until page reload. Acceptable for the UX goal of keeping the model selected across runs, but worth noting for long sessions.
  • ProcessingMenu.tsx indentation: The file has pre-existing unconventional indentation around the GeoLibre submenu closing tags. The new insertion is placed correctly inside the <DropdownMenuSubContent> and is structurally sound — no functional issue.
  • CLAUDE.md: New i18n strings added correctly to en.json. UI profile entry follows the existing tier convention. Store action follows the setSegmentationOpen pattern exactly. No guidelines violated.

Overall the implementation is solid — the georeferencing math, NMS, letterbox inversion, class-per-layer split, and session lifecycle are all correct. Finding #1 is the only one that could silently break detection for a subset of real YOLOv5 models; the rest are quality improvements.

Import onnxruntime-web from the `/wasm` subpath instead of the default entry.
The default entry also bundles the ~26 MB WebGPU/jsep WASM, which exceeds
Cloudflare Pages' 25 MiB per-file limit and failed the preview deploy. We force
the WASM execution provider anyway, so the WebGPU artifact was dead weight; the
CPU WASM is ~13 MB (under the limit) and shrinks every build. Verified detection
still works end to end in the browser.
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

⚡ Cloudflare Pages preview

Item Value
Preview https://dfcc4bdd.geolibre-preview.pages.dev
Demo app https://dfcc4bdd.geolibre-preview.pages.dev/demo/
Commit 5e75e72

Comment thread packages/processing/src/object-detection.ts Outdated
Comment thread packages/processing/src/object-detection.ts Outdated
Comment thread packages/processing/src/object-detection.ts
Comment thread packages/processing/src/object-detection.ts Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code review

PR adds a fully client-side ONNX/YOLO Object Detection toolbox. The architecture is clean (lazy-loaded dialog, store flag, proper ORT session teardown via finally), the decoding logic is well-commented, and the georeferencing math is correct (resY is stored positive, winding order is CCW). Findings below, grouped by category.


Bugs

Finding Confidence
showGeolibreActions does not include objectDetection — if a user's UI profile disables geocode, modelBuilder, and segmentation, showGeolibre is false and the whole GeoLibre sub-menu (including Object Detection) is hidden. Fix: add `
Dialog re-open during inference resets running to false — the useEffect that resets transient state on open fires and calls setRunning(false) while the original ONNX session is still executing. The Run button re-enables, a second handleRun can start, and both sessions eventually call addGeoJsonLayer, producing duplicate layers. Medium

Performance

Finding Confidence
Temporary array in bilinear sampling hot loopfor (const v of [v00, v10, v01, v11]) allocates a 4-element array on every sample: ~1.2 M allocations at 640 × 640 input. Four separate if statements avoid the allocations. Low-medium
Synchronous preprocess() blocks the main thread — at 640 × 640 this is tolerable, but at the allowed max of 4096 it's ~50 M iterations. Wrapping in a setTimeout(() => …, 0) defers the work past the next repaint so the spinner actually shows before the freeze. Low-medium

Maintainability

Finding Confidence
ORT_VERSION is a separate constant from the npm pin — if package.json is bumped but this string is not, the WASM fetch fails silently at runtime. Importing onnxruntime-web/package.json or adding a CI consistency check would keep them in sync. Medium

Quality / nits

Finding Confidence
No warning when raster has no georeferencing info — when epsgFromGeoKeys returns null, reprojection silently assumes WGS84. A raster without GeoKeys lands detection boxes in the wrong location with no user feedback. Low
Dead early-exit for v8 modelsif (objectness < confidenceThreshold) is always false for v8/v11 (objectness = 1). A hasObjectness && guard would make the intent explicit. Low (nit)

What I checked and did not flag

  • Georeferencing mathoriginY - pixel * resY (both positive) correctly computes north/south edges; polygon winding is CCW (RFC 7946 compliant). ✓
  • YOLOv8 vs v5 layout auto-detection — the d1 <= d2 heuristic is correct for all practical model shapes; the tie-break towards v8 is well-justified in the comment. ✓
  • readRasterData / RasterData imports — already exported from @geolibre/processing index before this PR. ✓
  • ORT session cleanupfinally { await session.release() } correctly frees native WASM memory on both success and error paths. ✓
  • Security — WASM is pinned to a specific version and jsdelivr is already in the Tauri CSP allowlist. The wasm execution provider is more sandboxed than webgpu/cuda. No secrets, no injection vectors, no XSS paths from class-name strings. ✓
  • CLAUDE.md — new user-facing strings go through t(), en.json is the source of truth (updated), feature-gated via the ui-profile catalog, lazy-loaded with an error boundary consistent with other dialogs. ✓

Add a model-source toggle to the Object Detection dialog so users no longer
have to supply their own .onnx file. The default is a built-in COCO model that
downloads from jsDelivr (permissive CORS, pinned to immutable commit SHAs) and
is cached via the Cache API so it is fetched once and works offline thereafter.

- detection-models.ts: COCO class list, the built-in model catalog
  (YOLOv8n + YOLOv5n), and a cached fetchDetectionModel helper.
- ObjectDetectionDialog: Built-in/Local source toggle, a built-in model
  dropdown that auto-fills the COCO class names, download-on-run with a
  spinner, and the local-file picker kept for custom/aerial models.
- i18n strings for the new controls.

Verified end to end: with only a GeoTIFF chosen and the built-in YOLOv8n
selected, Detect downloads the model and adds named per-class layers
(bus, person).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/geolibre-desktop/src/components/processing/ObjectDetectionDialog.tsx (1)

174-181: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the in-flight guard independent from the dialog open state.

Line 180 clears running every time the dialog reopens. If the user closes and reopens while model download or inference is still awaiting, the Detect button becomes enabled again and a second run can start. Both runs then add layers independently.

Suggested fix
+  const inFlightRef = useRef(false);
+
   useEffect(() => {
     if (!open) return;
     // Reset transient state so a re-opened dialog never shows a stale error,
     // result, or a `running` spinner left over from a previous session.
     setError(null);
     setResultMessage(null);
-    setRunning(false);
   }, [open]);

   const handleRun = useCallback(async () => {
+    if (inFlightRef.current) return;
+    inFlightRef.current = true;
     setError(null);
     setResultMessage(null);
     if (!imageBytes) {
       setError(t("objectDetection.error.chooseImage"));
+      inFlightRef.current = false;
       return;
     }
     if (modelSource === "local" && !modelBytes) {
       setError(t("objectDetection.error.chooseModel"));
+      inFlightRef.current = false;
       return;
     }
     setRunning(true);
     try {
       // ...
     } finally {
+      inFlightRef.current = false;
       setRunning(false);
     }
   }, [/* ... */]);

Also applies to: 217-229, 305-306

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/components/processing/ObjectDetectionDialog.tsx`
around lines 174 - 181, The transient reset in ObjectDetectionDialog’s
open-state effect is clearing the in-flight guard too aggressively, which can
re-enable Detect while a model download or inference is still running. Keep the
running state independent from the dialog’s open/close lifecycle by only
resetting error and result state on reopen, and preserve the guard used by the
detect flow so the existing run in the handleDetect / model-loading / inference
path cannot be started twice. Also update the related disable logic around the
Detect button and any cleanup paths referenced by the same running state so they
continue to respect an active in-flight operation even after the dialog is
reopened.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/lib/detection-models.ts`:
- Around line 79-97: The fetch and cache write are coupled in
detection-models.ts, so a successful download can be lost if cache.put() rejects
and the code falls back to fetching again. Split the logic in the model-loading
path so the network response from the first successful fetch in the
`fetch`/`caches.open` flow is returned directly even when `cache.put()` fails,
and only treat Cache API failures as non-fatal for caching while preserving the
already-downloaded `url` response.

---

Duplicate comments:
In `@apps/geolibre-desktop/src/components/processing/ObjectDetectionDialog.tsx`:
- Around line 174-181: The transient reset in ObjectDetectionDialog’s open-state
effect is clearing the in-flight guard too aggressively, which can re-enable
Detect while a model download or inference is still running. Keep the running
state independent from the dialog’s open/close lifecycle by only resetting error
and result state on reopen, and preserve the guard used by the detect flow so
the existing run in the handleDetect / model-loading / inference path cannot be
started twice. Also update the related disable logic around the Detect button
and any cleanup paths referenced by the same running state so they continue to
respect an active in-flight operation even after the dialog is reopened.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 20c6aa83-16b2-4dc7-9e25-bd94894487cb

📥 Commits

Reviewing files that changed from the base of the PR and between 87ef180 and 2eea4d0.

📒 Files selected for processing (3)
  • apps/geolibre-desktop/src/components/processing/ObjectDetectionDialog.tsx
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/lib/detection-models.ts

Comment thread apps/geolibre-desktop/src/lib/detection-models.ts
Comment thread apps/geolibre-desktop/src/lib/detection-models.ts
Comment thread packages/processing/src/object-detection.ts
Comment thread apps/geolibre-desktop/src/components/processing/ObjectDetectionDialog.tsx Outdated
Comment thread packages/processing/src/object-detection.ts
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Reviewed the full diff for PR #905 (in-browser ONNX/YOLO object detection). The overall design is sound — lazy loading, store flag, per-class layer grouping, NMS, WASM single-thread path, and CDN SHA-pinning are all done correctly. The YOLO output decoder's v8/v5 heuristic is correct for any realistic class count. The fallback Suspense pattern for the lazy dialog matches the established codebase convention. Below are the findings grouped by category.


Bugs

# File Finding Confidence
1 detection-models.ts L82–96 Catch scope too broad in fetchDetectionModel — HTTP fetch errors (404, 5xx) thrown inside the try block are swallowed by the outer catch intended only for Cache API failures (quota, insecure context). Result: a 404 causes a double fetch, logs a misleading "cache unavailable, fetching directly" debug message, and only properly throws on the second attempt. Suggested fix posted inline (split cache-open from fetch into separate try/catch). Medium
2 ObjectDetectionDialog.tsx L193–194 South-to-north raster row order not handledresY = Math.abs(resolutionY) is always positive, and the formula originY - minPxY * resY is correct only when originY is the north edge. For the rare south-to-north GeoTIFF (resolutionY > 0), getOrigin() returns the south edge and north/south are inverted. Extremely uncommon in practice but silently wrong. Low

Performance

# File Finding Confidence
3 object-detection.ts L171–174 O(inputSize²) bilinear sampling loop runs on the main thread — 640² ≈ 410 k iterations is fine; the UI-allowed maximum of 4096² ≈ 16.7 M iterations would freeze the event loop for several seconds with no opportunity for the spinner to animate. A comment noting this limit (and a future Web Worker path) was suggested inline. Medium

Quality

# File Finding Confidence
4 ObjectDetectionDialog.tsx L496–505 inputSize not enforced as a multiple of 32step={32} is advisory; a typed value like 33 passes the detectObjects() integer guard and produces a non-standard tensor edge. Suggested rounding to nearest 32 in the onChange handler. Low
5 object-detection.ts L333–337 InferenceSession recreated on every call — model graph deserialization + WASM compilation happens fresh each time detectObjects() is called. Fine for the single-shot MVP but becomes noticeable if users sweep thresholds or re-run detection. Noted as a follow-up in the inline comment. Low (MVP acceptable)

Security

Nothing flagged. The CDN URLs are pinned to immutable commit SHAs (good), the CORS fetch is explicit, the Cache API key is a fixed string, and user-supplied model bytes are passed directly to the ONNX runtime without any re-serialization.

CLAUDE.md

No violations. New i18n strings are in en.json, the lazy-load pattern matches existing dialogs, @geolibre/ui primitives are used throughout, and no direct MapLibre mutations are introduced.

- object-detection: make loadOrt a promise singleton so concurrent calls
  cannot race on configuration.
- object-detection: fall back to an 8-bit divisor when sampling finds no valid
  pixels (mostly-NoData raster), avoiding a divisor of 1 that feeds raw 0-255.
- object-detection: unroll the bilinear NoData check to drop a per-sample array
  allocation in the hot loop.
- object-detection: only apply the objectness gate for v5 (it was a no-op for
  v8/v11), and document that decode assumes sigmoid-activated output (standard
  Ultralytics exports) rather than double-activating the common case.
- object-detection: note the O(inputSize^2) main-thread preprocessing cost.
- object-detection: export ORT_VERSION and add a guard test asserting it equals
  the onnxruntime-web pin in package.json.
- detection-models: separate Cache API ops from the network fetch so a 404/5xx
  surfaces instead of being swallowed and retried, and a successful download is
  never discarded when a cache write fails.
- ObjectDetectionDialog: round inputSize to a multiple of 32 (YOLO stride);
  guard concurrent runs with an inferringRef that survives close/re-open so a
  re-opened dialog cannot spawn a duplicate inference; document the
  north-to-south row-order assumption.
- ProcessingMenu: include objectDetection in showGeolibreActions so the GeoLibre
  submenu still renders when it is the only enabled action.
// in a mostly-NoData raster), assume 8-bit range so the tensor receives 0-1
// values rather than raw 0-255 counts.
if (!hasValid) max = 255;
const divisor = max <= 1.5 ? 1 : max <= 255 ? 255 : max;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug — wrong divisor for float rasters with sampled max in (1.5, 255)

The three-way branch is:

  • max ≤ 1.5 → divisor 1 (already-normalised float)
  • 1.5 < max ≤ 255 → divisor 255 (treated as 8-bit)
  • max > 255 → divisor max (high bit-depth auto-scale)

Any GeoTIFF whose sampled red-band maximum falls in (1.5, 255) — multi-spectral reflectance products scaled 0–2, vegetation indices, DEMs with Celsius temperatures, or any float raster whose values happen to peak in that range — is silently treated as 8-bit. A pixel value of 2.0 becomes 2/255 ≈ 0.008 in the tensor; the model receives a near-black image and returns no detections.

A minimal fix for the float-vs-8-bit ambiguity is to auto-scale any raster where max is non-integer (floats never hit exact 255):

Suggested change
const divisor = max <= 1.5 ? 1 : max <= 255 ? 255 : max;
const divisor = max <= 1.5 ? 1 : (max <= 255 && Number.isInteger(max)) ? 255 : max;

A more robust fix would expose the GeoTIFF bit-depth through RasterData and use that directly, but the Number.isInteger guard eliminates the false-positive for the most common failure case without changing existing 8-bit or 16-bit paths.

features,
};
if (epsg && epsg !== 4326) {
fc.crs = { type: "name", properties: { name: `EPSG:${epsg}` } };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug — missing EPSG silently skips reprojection, placing boxes at wrong coordinates

When epsgFromGeoKeys returns null (user-defined CRS sentinel 32767, GeoTIFF 1.1 GeodeticCRSGeoKey, or any raster whose CRS is stored only as WKT with no EPSG authority code), no crs member is attached to the FeatureCollection.

reprojectFeatureCollectionToWgs84 in duckdb-vector-loader.ts checks sourceCrsFromGeoJson first (line 494):

if (!sourceCrs) return stripped as FeatureCollection;

So the feature collection is returned unchanged, and its coordinates — which are in the raster's native projected CRS — are added to the map as if they were WGS84 lon/lat. For a UTM raster without a recognized EPSG code, detection boxes could be placed thousands of kilometres from the true location with no error shown to the user.

Consider surfacing a warning when epsg === null and the raster has non-trivial georeferencing (i.e., originX !== 0 || originY !== 0):

Suggested change
fc.crs = { type: "name", properties: { name: `EPSG:${epsg}` } };
if (epsg && epsg !== 4326) {
fc.crs = { type: "name", properties: { name: `EPSG:${epsg}` } };
} else if (!epsg) {
console.warn(
"objectDetection: no recognized EPSG code in GeoKeys — detections will not be reprojected and may appear at the wrong location.",
);
}

try {
session = await ort.InferenceSession.create(new Uint8Array(modelBytes), {
executionProviders: ["wasm"],
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance — InferenceSession re-created on every call; model graph is re-parsed each run

fetchDetectionModel caches model bytes via the Cache API so there is no repeated download. But ort.InferenceSession.create re-parses those bytes into the WASM heap on each detectObjects() call. For the default yolov8n.onnx (~6 MB) this adds ~200–500 ms of parsing overhead on every run. A user who adjusts the confidence threshold and clicks "Detect" again pays the full re-parse cost each time.

The ortPromise singleton already avoids re-importing the ORT module; a session cache keyed on the model URL (or a stable hash of the bytes) would similarly amortise the model-graph cost:

const sessionCache = new Map<string, ort.InferenceSession>();

// in detectObjects(), before session.create:
let session = sessionCache.get(cacheKey);
if (!session) {
  session = await ort.InferenceSession.create(...);
  sessionCache.set(cacheKey, session);
}
// then omit session.release() from the finally block for cached sessions

The same try/finally { session.release() } wrapper should be kept only for uncached / one-shot sessions. A page-unload handler (or WeakRef-based eviction) can clean up cached sessions to avoid holding WASM heap indefinitely.

id: "yolov8n-coco",
label: "YOLOv8n (COCO, 80 classes)",
url: "https://cdn.jsdelivr.net/gh/Hyuto/yolov8-onnxruntime-web@fc4a52c466d15ad4519873a0cef22fbc935b93b6/public/model/yolov8n.onnx",
classNames: COCO_CLASSES,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLAUDE.md — label strings bypass t() and can't be translated

The label fields are rendered directly in the dropdown ({model.label}) in ObjectDetectionDialog.tsx without going through t(). CLAUDE.md requires t() for new user-facing strings.

These are technical model identifiers (version + dataset + class count) that arguably don't need translation, but if you want to follow the convention strictly, the labels could be moved to en.json and referenced by key:

Suggested change
classNames: COCO_CLASSES,
label: "toolbar.command.objectDetectionModel.yolov8n",

with a corresponding en.json entry:

"objectDetectionModel": {
  "yolov8n": "YOLOv8n (COCO, 80 classes)",
  "yolov5n": "YOLOv5n (COCO, 80 classes)"
}

Alternatively, if the team considers model identifiers exempt from i18n, that exception should be documented.

@@ -0,0 +1,450 @@
/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLAUDE.md — multi-paragraph JSDoc blocks throughout this file (and detection-models.ts, ObjectDetectionDialog.tsx)

CLAUDE.md: "Never write multi-paragraph docstrings or multi-line comment blocks — one short line max."

All three new files contain multi-paragraph JSDoc blocks. The file-level module doc here is 21 lines; rgbBands, sampleBilinear, preprocess, nonMaxSuppression, decodeYolo, and detectObjects each have 6–14-line blocks. detection-models.ts opens with a 14-line module comment and a 12-line JSDoc on fetchDetectionModel. ObjectDetectionDialog.tsx has four multi-paragraph blocks on its helper functions.

The content of these comments often explains genuinely non-obvious constraints (YOLO layout heuristic, NoData NaN sentinel, single-threaded WASM rationale), which satisfies the "WHY is non-obvious" criterion — but the format rule is unconditional. Each of these blocks should collapse to at most one line, with the non-obvious constraint preserved and the rest removed.

if (model) setClassNames(model.classNames.join(", "));
}, []);

const handleRun = useCallback(async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quality — in-flight inference cannot be cancelled; dialog close produces a ghost run

inferringRef.current correctly prevents a second run from starting while one is in progress. But there is no AbortController passed to fetch() inside fetchDetectionModel, and no cancellation path exists for ort.InferenceSession.run(). If the user starts detection, closes the dialog while the model is downloading or inference is running, and then re-opens it:

  • The ghost run continues to completion on the stale closure.
  • addGeoJsonLayer is called silently — a layer appears on the map with no result message visible.
  • inferringRef.current stays true until the ghost run finishes, making the re-opened dialog appear locked (the button is greyed out via the running state, but running was reset on close... wait — running is NOT reset on close, so the spinner shows). This is consistent, but a user who did not see the original spinner may be confused.

For a future iteration, consider passing an AbortSignal to fetchDetectionModel and polling a cancel flag between inference steps. At minimum, noting the limitation in a comment near inferringRef would help future contributors understand why no cancel UI is offered.

@github-actions

Copy link
Copy Markdown
Contributor

Code review

This PR adds a fully client-side YOLO/ONNX object detection toolbox: a new `@geolibre/processing` engine handles raster preprocessing, layout-auto-detected YOLOv8/v11 and YOLOv5 decoding, NMS, and back-projection; the `ObjectDetectionDialog` georeferences detected boxes, splits them by class into separate GeoJSON layers, and supports both downloadable built-in COCO models and user-supplied ONNX files. The store, toolbar, Processing menu, UI-profile catalog, i18n, and lazy-mount shell wiring are all extended consistently.


Bugs

# Location Finding Confidence
1 object-detection.ts:139 Wrong divisor for float rasters with sampled max in (1.5, 255). The branch max <= 255 then divisor = 255 treats any float raster whose peak value exceeds 1.5 as 8-bit imagery. A reflectance product scaled 0-2 (or Celsius temperatures, or any multi-spectral index) gets divided by 255, collapsing the entire tensor to near-zero; the model sees a black image and returns no detections. Fix: add Number.isInteger(max) guard so only genuine integer-valued bands get the 255 divisor. High
2 ObjectDetectionDialog.tsx:141 Null EPSG silently skips reprojection. When epsgFromGeoKeys returns null (user-defined CRS sentinel 32767, GeoTIFF 1.1 keys, or a WKT-only CRS), no crs member is attached. reprojectFeatureCollectionToWgs84 then returns the collection unchanged; detection boxes are added in the raster native projected CRS as if they were WGS84, appearing in the wrong location with no error. High

Performance

# Location Finding Confidence
3 object-detection.ts:404 InferenceSession rebuilt from scratch on every run. fetchDetectionModel caches model bytes via the Cache API, but ort.InferenceSession.create re-parses those bytes into the WASM heap on each detectObjects() call (~200-500 ms for yolov8n). A session cache keyed on model URL (parallel to ortPromise for the ORT module) would make repeated runs nearly instant after the first. High

Quality

# Location Finding Confidence
4 ObjectDetectionDialog.tsx:231 No cancellation path for in-flight inference. inferringRef prevents a second concurrent run but does not cancel the current one. Closing the dialog mid-inference lets the ghost run complete, silently adding a layer to the map; the dialog appears locked (spinner shown) if re-opened before the ghost run finishes. Medium

CLAUDE.md

# Location Finding Confidence
5 detection-models.ts:51 Built-in model label strings bypass t(). "YOLOv8n (COCO, 80 classes)" and "YOLOv5n (COCO, 80 classes)" are rendered directly as dropdown option text. CLAUDE.md: "Use t() for new user-facing strings." High
6 object-detection.ts:1, detection-models.ts:1, ObjectDetectionDialog.tsx Pervasive multi-paragraph JSDoc blocks. All three new files contain multi-line block comments (6-21 lines each). CLAUDE.md: "Never write multi-paragraph docstrings or multi-line comment blocks — one short line max." The content often explains genuine non-obvious constraints (YOLO layout heuristic, NoData NaN sentinel, single-threaded WASM requirement), but the format rule is unconditional. High

What I checked and did not flag: the inferringRef early-return guard is correct (all returns after the ref is set are inside the outer try, so finally always resets it); the crs format {type:"name",properties:{name:"EPSG:…"}} matches what sourceCrsFromGeoJson expects; reprojectFeatureCollectionToWgs84 is concurrent-safe (monotonic sequence counter, per-call DuckDB connection); addGeoJsonLayer return value is synchronously available; the v8/v11 layout heuristic (d1 <= d2) is correct for standard Ultralytics ONNX exports (outputs are in input-pixel coordinates, not normalised, so the un-letterbox math is right); the ORT_VERSION test guard against version skew is a nice touch.

@Diemsgit

Copy link
Copy Markdown

Greatly thankful for taking action on this request.

FYI
Tried Deploy Preview 905.
The GeoTiff was too large. See below error message.

This raster is too large for the in-browser engine. Use the sidecar (rasterio/GDAL) engine instead.

@giswqs

giswqs commented Jun 27, 2026

Copy link
Copy Markdown
Member Author

Yeah, it is not ready yet. Need more testing.

@Diemsgit

Copy link
Copy Markdown

If it makes things easier(?) Apply detection on layer or visible (just as QGIS plugins)
Dropping tif file on map also crashes the app .. Works via add data raster layer however

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for onnx/yolo detection models to complement build in AI Segmentation

2 participants