drawdown-arcore-depth 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -188,3 +188,13 @@ No ArUco marker is required by the native ARCore workflow. The existing RFID/NFC
188
188
  ## v0.1.4 Raw Depth field fix
189
189
 
190
190
  The DBH sampler now anchors front-surface range with ARCore Raw Depth plus the matching confidence image (confidence >= 128). Smoothed Depth is used only after a high-confidence foreground tree range has been established, to fill the trunk surface for multi-row boundary estimation. This prevents distant outdoor background depth from being mistaken for the trunk.
191
+
192
+
193
+ ## 0.1.6 diagnostic sweep
194
+
195
+ Version 0.1.6 keeps the 0.1.4 Raw Depth DBH acceptance geometry unchanged and adds compact native sweep diagnostics. `captureDbhSweep()` now reports per-stage frame counts and `nativeRejectionCounts` so zero-observation field failures can be diagnosed without retaining camera or depth frames.
196
+
197
+
198
+ ## 0.1.6 Raw Depth anchor diagnostics
199
+
200
+ 0.1.6 keeps the 0.1.5 DBH acceptance algorithm unchanged and adds compact numeric diagnostics around the mapped crosshair at 10, 20 and 30 pixel radii. It reports non-zero/in-range Raw Depth counts, confidence threshold counts (64/96/128/160), the highest confidence in the eligible 0.5–5 m range and the associated depth. These diagnostics do not influence the accepted DBH result.
@@ -39,29 +39,70 @@ final class DepthObservationSampler {
39
39
  private static final double MIN_TARGET_DEPTH_MM = 500.0;
40
40
  private static final double MAX_TARGET_DEPTH_MM = 5000.0;
41
41
 
42
- DepthObservation sample(Frame frame, Camera camera, int viewWidth, int viewHeight) {
42
+ DepthSampleResult sample(Frame frame, Camera camera, int viewWidth, int viewHeight) {
43
+ boolean trackingFrame = false;
44
+ boolean rawDepthFrame = false;
45
+ boolean mappedCenterReached = false;
46
+ boolean rawAnchorReached = false;
47
+ boolean rawPatchReached = false;
48
+ boolean smoothDepthMatchReached = false;
49
+ boolean boundaryConsensusReached = false;
50
+ RawAnchorDiagnostics rawAnchorDiagnostics = null;
51
+
43
52
  if (camera.getTrackingState() != TrackingState.TRACKING) {
44
- return null;
53
+ return DepthSampleResult.failure(
54
+ "tracking_not_tracking",
55
+ false, false, false, false, false, false, false, rawAnchorDiagnostics);
45
56
  }
57
+ trackingFrame = true;
58
+
59
+ Image rawDepth = null;
60
+ Image rawConfidence = null;
61
+ Image smoothDepth = null;
62
+ try {
63
+ try {
64
+ rawDepth = frame.acquireRawDepthImage16Bits();
65
+ rawDepthFrame = true;
66
+ } catch (NotYetAvailableException e) {
67
+ return DepthSampleResult.failure(
68
+ "raw_depth_not_yet_available",
69
+ trackingFrame, false, false, false, false, false, false, rawAnchorDiagnostics);
70
+ }
46
71
 
47
- try (
48
- Image rawDepth = frame.acquireRawDepthImage16Bits();
49
- Image rawConfidence = frame.acquireRawDepthConfidenceImage();
50
- Image smoothDepth = frame.acquireDepthImage16Bits()) {
72
+ try {
73
+ rawConfidence = frame.acquireRawDepthConfidenceImage();
74
+ } catch (NotYetAvailableException e) {
75
+ return DepthSampleResult.failure(
76
+ "raw_confidence_not_available",
77
+ trackingFrame, rawDepthFrame, false, false, false, false, false, rawAnchorDiagnostics);
78
+ }
79
+
80
+ try {
81
+ smoothDepth = frame.acquireDepthImage16Bits();
82
+ } catch (NotYetAvailableException e) {
83
+ return DepthSampleResult.failure(
84
+ "smooth_depth_not_available",
85
+ trackingFrame, rawDepthFrame, false, false, false, false, false, rawAnchorDiagnostics);
86
+ }
51
87
 
52
88
  CameraIntrinsics intrinsics = camera.getImageIntrinsics();
53
89
  float[] focal = intrinsics.getFocalLength();
54
90
  int[] imageDimensions = intrinsics.getImageDimensions();
55
91
  if (focal == null || focal.length < 2 || imageDimensions == null || imageDimensions.length < 2) {
56
- return null;
92
+ return DepthSampleResult.failure(
93
+ "image_intrinsics_missing",
94
+ trackingFrame, rawDepthFrame, false, false, false, false, false, rawAnchorDiagnostics);
57
95
  }
58
96
 
59
- // ARCore documents raw depth, confidence and smoothed depth as the same size.
97
+ // Keep the 0.1.5 measurement geometry/thresholds unchanged; 0.1.6 only adds
98
+ // Raw Depth anchor diagnostics around the mapped crosshair.
60
99
  if (rawDepth.getWidth() != rawConfidence.getWidth()
61
100
  || rawDepth.getHeight() != rawConfidence.getHeight()
62
101
  || rawDepth.getWidth() != smoothDepth.getWidth()
63
102
  || rawDepth.getHeight() != smoothDepth.getHeight()) {
64
- return null;
103
+ return DepthSampleResult.failure(
104
+ "image_size_mismatch",
105
+ trackingFrame, rawDepthFrame, false, false, false, false, false, rawAnchorDiagnostics);
65
106
  }
66
107
 
67
108
  float[] viewCenter = new float[] { viewWidth / 2f, viewHeight / 2f };
@@ -74,13 +115,21 @@ final class DepthObservationSampler {
74
115
 
75
116
  int[] mappedCenter = imageToDepth(frame, rawDepth, imageCenter[0], imageCenter[1]);
76
117
  if (mappedCenter == null) {
77
- return null;
118
+ return DepthSampleResult.failure(
119
+ "image_to_depth_mapping_failed",
120
+ trackingFrame, rawDepthFrame, false, false, false, false, false, rawAnchorDiagnostics);
78
121
  }
122
+ mappedCenterReached = true;
123
+ rawAnchorDiagnostics = collectRawAnchorDiagnostics(
124
+ rawDepth, rawConfidence, mappedCenter[0], mappedCenter[1]);
79
125
 
80
126
  RawAnchor anchor = findRawAnchor(rawDepth, rawConfidence, mappedCenter[0], mappedCenter[1]);
81
127
  if (anchor == null) {
82
- return null;
128
+ return DepthSampleResult.failure(
129
+ "no_confident_raw_anchor",
130
+ trackingFrame, rawDepthFrame, mappedCenterReached, false, false, false, false, rawAnchorDiagnostics);
83
131
  }
132
+ rawAnchorReached = true;
84
133
 
85
134
  double rawToleranceMm = clamp(anchor.depthMm * 0.10, 100.0, 250.0);
86
135
  List<Integer> rawPatch = collectConfidentRawPatch(
@@ -92,13 +141,18 @@ final class DepthObservationSampler {
92
141
  rawToleranceMm,
93
142
  anchor.depthMm);
94
143
  if (rawPatch.size() < MIN_RAW_PATCH_SAMPLES) {
95
- return null;
144
+ return DepthSampleResult.failure(
145
+ "insufficient_raw_patch_samples",
146
+ trackingFrame, rawDepthFrame, mappedCenterReached, rawAnchorReached, false, false, false, rawAnchorDiagnostics);
96
147
  }
148
+ rawPatchReached = true;
97
149
 
98
150
  double centerDepthMm = median(rawPatch);
99
151
  double centerMadMm = mad(rawPatch, centerDepthMm);
100
152
  if (centerDepthMm < MIN_TARGET_DEPTH_MM || centerDepthMm > MAX_TARGET_DEPTH_MM) {
101
- return null;
153
+ return DepthSampleResult.failure(
154
+ "target_depth_out_of_range",
155
+ trackingFrame, rawDepthFrame, mappedCenterReached, rawAnchorReached, rawPatchReached, false, false, rawAnchorDiagnostics);
102
156
  }
103
157
 
104
158
  // Locate the foreground surface in the smoothed depth image, but only
@@ -111,8 +165,11 @@ final class DepthObservationSampler {
111
165
  centerDepthMm,
112
166
  clamp(centerDepthMm * 0.10, 100.0, 250.0));
113
167
  if (centerX < 0) {
114
- return null;
168
+ return DepthSampleResult.failure(
169
+ "no_smooth_depth_match",
170
+ trackingFrame, rawDepthFrame, mappedCenterReached, rawAnchorReached, rawPatchReached, false, false, rawAnchorDiagnostics);
115
171
  }
172
+ smoothDepthMatchReached = true;
116
173
 
117
174
  double toleranceMm = clamp(centerDepthMm * 0.08, 80.0, 220.0);
118
175
  Boundary boundary = findConsensusBoundary(
@@ -122,28 +179,41 @@ final class DepthObservationSampler {
122
179
  centerDepthMm,
123
180
  toleranceMm);
124
181
  if (boundary == null || boundary.rightX - boundary.leftX < 3) {
125
- return null;
182
+ return DepthSampleResult.failure(
183
+ "boundary_no_consensus",
184
+ trackingFrame, rawDepthFrame, mappedCenterReached, rawAnchorReached, rawPatchReached, smoothDepthMatchReached, false, rawAnchorDiagnostics);
126
185
  }
186
+ boundaryConsensusReached = true;
127
187
 
128
188
  int depthWidth = smoothDepth.getWidth();
129
189
  int boundaryWidth = boundary.rightX - boundary.leftX;
130
190
  if (boundary.leftX <= EDGE_MARGIN_PX
131
- || boundary.rightX >= depthWidth - 1 - EDGE_MARGIN_PX
132
- || boundaryWidth > depthWidth * MAX_TRUNK_WIDTH_FRACTION) {
133
- return null;
191
+ || boundary.rightX >= depthWidth - 1 - EDGE_MARGIN_PX) {
192
+ return DepthSampleResult.failure(
193
+ "boundary_touches_edge",
194
+ trackingFrame, rawDepthFrame, mappedCenterReached, rawAnchorReached, rawPatchReached, smoothDepthMatchReached, boundaryConsensusReached, rawAnchorDiagnostics);
195
+ }
196
+ if (boundaryWidth > depthWidth * MAX_TRUNK_WIDTH_FRACTION) {
197
+ return DepthSampleResult.failure(
198
+ "boundary_too_wide",
199
+ trackingFrame, rawDepthFrame, mappedCenterReached, rawAnchorReached, rawPatchReached, smoothDepthMatchReached, boundaryConsensusReached, rawAnchorDiagnostics);
134
200
  }
135
201
 
136
202
  float[] leftImage = depthToImage(frame, smoothDepth, boundary.leftX, anchor.y);
137
203
  float[] rightImage = depthToImage(frame, smoothDepth, boundary.rightX, anchor.y);
138
204
  if (leftImage == null || rightImage == null) {
139
- return null;
205
+ return DepthSampleResult.failure(
206
+ "depth_to_image_mapping_failed",
207
+ trackingFrame, rawDepthFrame, mappedCenterReached, rawAnchorReached, rawPatchReached, smoothDepthMatchReached, boundaryConsensusReached, rawAnchorDiagnostics);
140
208
  }
141
209
 
142
210
  double dx = rightImage[0] - leftImage[0];
143
211
  double dy = rightImage[1] - leftImage[1];
144
212
  double imageWidthPx = Math.hypot(dx, dy);
145
213
  if (!Double.isFinite(imageWidthPx) || imageWidthPx < 8.0) {
146
- return null;
214
+ return DepthSampleResult.failure(
215
+ "image_width_too_small",
216
+ trackingFrame, rawDepthFrame, mappedCenterReached, rawAnchorReached, rawPatchReached, smoothDepthMatchReached, boundaryConsensusReached, rawAnchorDiagnostics);
147
217
  }
148
218
 
149
219
  double normalizedAngularWidth = Math.sqrt(
@@ -152,7 +222,9 @@ final class DepthObservationSampler {
152
222
  if (!Double.isFinite(normalizedAngularWidth)
153
223
  || normalizedAngularWidth <= 0.0
154
224
  || normalizedAngularWidth > 0.95) {
155
- return null;
225
+ return DepthSampleResult.failure(
226
+ "angular_width_invalid",
227
+ trackingFrame, rawDepthFrame, mappedCenterReached, rawAnchorReached, rawPatchReached, smoothDepthMatchReached, boundaryConsensusReached, rawAnchorDiagnostics);
156
228
  }
157
229
 
158
230
  double effectiveFocalPx = imageWidthPx / normalizedAngularWidth;
@@ -178,7 +250,7 @@ final class DepthObservationSampler {
178
250
  boundaryConfidence = "low";
179
251
  }
180
252
 
181
- return new DepthObservation(
253
+ DepthObservation observation = new DepthObservation(
182
254
  centerDepthMm / 1000.0,
183
255
  effectiveFocalPx,
184
256
  imageWidthPx,
@@ -187,11 +259,94 @@ final class DepthObservationSampler {
187
259
  boundaryConfidence,
188
260
  centerMadMm / 1000.0,
189
261
  System.currentTimeMillis());
190
- } catch (NotYetAvailableException e) {
191
- return null;
262
+ return DepthSampleResult.success(observation, rawAnchorDiagnostics);
192
263
  } catch (Exception e) {
193
- return null;
264
+ return DepthSampleResult.failure(
265
+ "unexpected_exception",
266
+ trackingFrame,
267
+ rawDepthFrame,
268
+ mappedCenterReached,
269
+ rawAnchorReached,
270
+ rawPatchReached,
271
+ smoothDepthMatchReached,
272
+ boundaryConsensusReached,
273
+ rawAnchorDiagnostics);
274
+ } finally {
275
+ if (smoothDepth != null) smoothDepth.close();
276
+ if (rawConfidence != null) rawConfidence.close();
277
+ if (rawDepth != null) rawDepth.close();
278
+ }
279
+ }
280
+
281
+ private static RawAnchorDiagnostics collectRawAnchorDiagnostics(
282
+ Image rawDepth,
283
+ Image confidence,
284
+ int centerX,
285
+ int centerY) {
286
+ RawAnchorDiagnostics.RadiusDiagnostics radius10 = scanRawRadius(rawDepth, confidence, centerX, centerY, 10);
287
+ RawAnchorDiagnostics.RadiusDiagnostics radius20 = scanRawRadius(rawDepth, confidence, centerX, centerY, 20);
288
+ RawAnchorDiagnostics.RadiusDiagnostics radius30 = scanRawRadius(rawDepth, confidence, centerX, centerY, 30);
289
+ return new RawAnchorDiagnostics(
290
+ centerX,
291
+ centerY,
292
+ rawDepth.getWidth(),
293
+ rawDepth.getHeight(),
294
+ radius10,
295
+ radius20,
296
+ radius30);
297
+ }
298
+
299
+ private static RawAnchorDiagnostics.RadiusDiagnostics scanRawRadius(
300
+ Image rawDepth,
301
+ Image confidence,
302
+ int centerX,
303
+ int centerY,
304
+ int radius) {
305
+ int sampled = 0;
306
+ int nonZero = 0;
307
+ int inRange = 0;
308
+ int atLeast64 = 0;
309
+ int atLeast96 = 0;
310
+ int atLeast128 = 0;
311
+ int atLeast160 = 0;
312
+ int maxConfidenceInRange = 0;
313
+ int depthAtMaxConfidence = 0;
314
+
315
+ for (int y = Math.max(0, centerY - radius);
316
+ y <= Math.min(rawDepth.getHeight() - 1, centerY + radius);
317
+ y++) {
318
+ for (int x = Math.max(0, centerX - radius);
319
+ x <= Math.min(rawDepth.getWidth() - 1, centerX + radius);
320
+ x++) {
321
+ sampled += 1;
322
+ int depthMm = getDepthMm(rawDepth, x, y);
323
+ if (depthMm <= 0) continue;
324
+ nonZero += 1;
325
+ if (depthMm < MIN_TARGET_DEPTH_MM || depthMm > MAX_TARGET_DEPTH_MM) continue;
326
+ inRange += 1;
327
+ int conf = getConfidence(confidence, x, y);
328
+ if (conf >= 64) atLeast64 += 1;
329
+ if (conf >= 96) atLeast96 += 1;
330
+ if (conf >= 128) atLeast128 += 1;
331
+ if (conf >= 160) atLeast160 += 1;
332
+ if (conf > maxConfidenceInRange) {
333
+ maxConfidenceInRange = conf;
334
+ depthAtMaxConfidence = depthMm;
335
+ }
336
+ }
194
337
  }
338
+
339
+ return new RawAnchorDiagnostics.RadiusDiagnostics(
340
+ radius,
341
+ sampled,
342
+ nonZero,
343
+ inRange,
344
+ atLeast64,
345
+ atLeast96,
346
+ atLeast128,
347
+ atLeast160,
348
+ maxConfidenceInRange,
349
+ depthAtMaxConfidence);
195
350
  }
196
351
 
197
352
  private static RawAnchor findRawAnchor(Image rawDepth, Image confidence, int centerX, int centerY) {
@@ -0,0 +1,69 @@
1
+ package org.drawdown.arcoredepth;
2
+
3
+ /**
4
+ * One attempted native ARCore sample. A failed sample carries a stable reason
5
+ * plus milestone flags so the sweep can explain where frames are being lost
6
+ * without persisting camera/depth frames.
7
+ */
8
+ final class DepthSampleResult {
9
+ final DepthObservation observation;
10
+ final String rejectionReason;
11
+ final boolean trackingFrame;
12
+ final boolean rawDepthFrame;
13
+ final boolean mappedCenter;
14
+ final boolean rawAnchor;
15
+ final boolean rawPatch;
16
+ final boolean smoothDepthMatch;
17
+ final boolean boundaryConsensus;
18
+ final RawAnchorDiagnostics rawAnchorDiagnostics;
19
+
20
+ DepthSampleResult(
21
+ DepthObservation observation,
22
+ String rejectionReason,
23
+ boolean trackingFrame,
24
+ boolean rawDepthFrame,
25
+ boolean mappedCenter,
26
+ boolean rawAnchor,
27
+ boolean rawPatch,
28
+ boolean smoothDepthMatch,
29
+ boolean boundaryConsensus,
30
+ RawAnchorDiagnostics rawAnchorDiagnostics) {
31
+ this.observation = observation;
32
+ this.rejectionReason = rejectionReason;
33
+ this.trackingFrame = trackingFrame;
34
+ this.rawDepthFrame = rawDepthFrame;
35
+ this.mappedCenter = mappedCenter;
36
+ this.rawAnchor = rawAnchor;
37
+ this.rawPatch = rawPatch;
38
+ this.smoothDepthMatch = smoothDepthMatch;
39
+ this.boundaryConsensus = boundaryConsensus;
40
+ this.rawAnchorDiagnostics = rawAnchorDiagnostics;
41
+ }
42
+
43
+ static DepthSampleResult failure(
44
+ String reason,
45
+ boolean trackingFrame,
46
+ boolean rawDepthFrame,
47
+ boolean mappedCenter,
48
+ boolean rawAnchor,
49
+ boolean rawPatch,
50
+ boolean smoothDepthMatch,
51
+ boolean boundaryConsensus,
52
+ RawAnchorDiagnostics rawAnchorDiagnostics) {
53
+ return new DepthSampleResult(
54
+ null,
55
+ reason,
56
+ trackingFrame,
57
+ rawDepthFrame,
58
+ mappedCenter,
59
+ rawAnchor,
60
+ rawPatch,
61
+ smoothDepthMatch,
62
+ boundaryConsensus,
63
+ rawAnchorDiagnostics);
64
+ }
65
+
66
+ static DepthSampleResult success(DepthObservation observation, RawAnchorDiagnostics rawAnchorDiagnostics) {
67
+ return new DepthSampleResult(observation, null, true, true, true, true, true, true, true, rawAnchorDiagnostics);
68
+ }
69
+ }
@@ -36,7 +36,9 @@ import com.google.ar.core.exceptions.UnavailableUserDeclinedInstallationExceptio
36
36
  import android.opengl.GLSurfaceView;
37
37
 
38
38
  import java.util.ArrayList;
39
+ import java.util.LinkedHashMap;
39
40
  import java.util.List;
41
+ import java.util.Map;
40
42
 
41
43
  @CapacitorPlugin(
42
44
  name = "DrawDownArCoreDepth",
@@ -44,7 +46,8 @@ import java.util.List;
44
46
  @Permission(alias = "camera", strings = { Manifest.permission.CAMERA })
45
47
  })
46
48
  public class DrawDownArCoreDepthPlugin extends Plugin {
47
- private static final String PLUGIN_VERSION = "0.1.4";
49
+ private static final String PLUGIN_VERSION = "0.1.6";
50
+ private static final String METHOD_VERSION = "arcore-raw-depth-cylinder-v5-anchor-diagnostics";
48
51
  private static final long WARMUP_MS = 1200L;
49
52
 
50
53
  private final Handler mainHandler = new Handler(Looper.getMainLooper());
@@ -68,6 +71,16 @@ public class DrawDownArCoreDepthPlugin extends Plugin {
68
71
  private int maxObservations = 10;
69
72
  private int minObservations = 5;
70
73
  private final List<DepthObservation> sweepObservations = new ArrayList<>();
74
+ private final Map<String, Integer> sweepRejectionCounts = new LinkedHashMap<>();
75
+ private final List<RawAnchorDiagnostics> sweepRawAnchorDiagnostics = new ArrayList<>();
76
+ private int totalFrameCount = 0;
77
+ private int trackingFrameCount = 0;
78
+ private int rawDepthFrameCount = 0;
79
+ private int mappedCenterCount = 0;
80
+ private int rawAnchorCount = 0;
81
+ private int rawPatchCount = 0;
82
+ private int smoothDepthMatchCount = 0;
83
+ private int boundaryConsensusCount = 0;
71
84
 
72
85
  @PluginMethod
73
86
  public void checkSupport(PluginCall call) {
@@ -251,6 +264,16 @@ public class DrawDownArCoreDepthPlugin extends Plugin {
251
264
 
252
265
  pendingSweepCall = call;
253
266
  sweepObservations.clear();
267
+ sweepRejectionCounts.clear();
268
+ sweepRawAnchorDiagnostics.clear();
269
+ totalFrameCount = 0;
270
+ trackingFrameCount = 0;
271
+ rawDepthFrameCount = 0;
272
+ mappedCenterCount = 0;
273
+ rawAnchorCount = 0;
274
+ rawPatchCount = 0;
275
+ smoothDepthMatchCount = 0;
276
+ boundaryConsensusCount = 0;
254
277
  minObservations = requestedMin;
255
278
  maxObservations = requestedMax;
256
279
  sampleIntervalMs = Math.max(100L, durationMs / Math.max(1, requestedMax * 2));
@@ -372,32 +395,65 @@ public class DrawDownArCoreDepthPlugin extends Plugin {
372
395
 
373
396
  try {
374
397
  Camera camera = frame.getCamera();
375
- DepthObservation observation = sampler.sample(frame, camera, viewWidth, viewHeight);
376
- if (observation == null) {
377
- return;
378
- }
398
+ DepthSampleResult sampleResult = sampler.sample(frame, camera, viewWidth, viewHeight);
379
399
 
380
400
  boolean reachedMax = false;
381
401
  synchronized (sweepLock) {
382
402
  if (!sweepActive || pendingSweepCall == null) {
383
403
  return;
384
404
  }
385
- sweepObservations.add(observation);
386
- reachedMax = sweepObservations.size() >= maxObservations;
405
+ totalFrameCount += 1;
406
+ if (sampleResult.trackingFrame) trackingFrameCount += 1;
407
+ if (sampleResult.rawDepthFrame) rawDepthFrameCount += 1;
408
+ if (sampleResult.mappedCenter) mappedCenterCount += 1;
409
+ if (sampleResult.rawAnchor) rawAnchorCount += 1;
410
+ if (sampleResult.rawPatch) rawPatchCount += 1;
411
+ if (sampleResult.smoothDepthMatch) smoothDepthMatchCount += 1;
412
+ if (sampleResult.boundaryConsensus) boundaryConsensusCount += 1;
413
+ if (sampleResult.rawAnchorDiagnostics != null) {
414
+ sweepRawAnchorDiagnostics.add(sampleResult.rawAnchorDiagnostics);
415
+ }
416
+
417
+ if (sampleResult.rejectionReason != null) {
418
+ int current = sweepRejectionCounts.containsKey(sampleResult.rejectionReason)
419
+ ? sweepRejectionCounts.get(sampleResult.rejectionReason)
420
+ : 0;
421
+ sweepRejectionCounts.put(sampleResult.rejectionReason, current + 1);
422
+ }
423
+
424
+ if (sampleResult.observation != null) {
425
+ sweepObservations.add(sampleResult.observation);
426
+ reachedMax = sweepObservations.size() >= maxObservations;
427
+ }
387
428
  }
388
429
 
389
430
  if (reachedMax) {
390
431
  mainHandler.post(() -> finishSweep(false));
391
432
  }
392
433
  } catch (Exception ignored) {
393
- // Invalid frames are simply skipped; the app's aggregate QA rejects
394
- // a sweep with too few valid observations.
434
+ synchronized (sweepLock) {
435
+ totalFrameCount += 1;
436
+ int current = sweepRejectionCounts.containsKey("unexpected_exception")
437
+ ? sweepRejectionCounts.get("unexpected_exception")
438
+ : 0;
439
+ sweepRejectionCounts.put("unexpected_exception", current + 1);
440
+ }
395
441
  }
396
442
  }
397
443
 
398
444
  private void finishSweep(boolean stoppedByCaller) {
399
445
  PluginCall sweepCall;
400
446
  List<DepthObservation> observations;
447
+ Map<String, Integer> rejectionCounts;
448
+ List<RawAnchorDiagnostics> rawAnchorDiagnostics;
449
+ int framesTotal;
450
+ int framesTracking;
451
+ int framesRawDepth;
452
+ int centersMapped;
453
+ int anchorsRaw;
454
+ int patchesRaw;
455
+ int matchesSmooth;
456
+ int boundariesConsensus;
401
457
  synchronized (sweepLock) {
402
458
  sweepCall = pendingSweepCall;
403
459
  if (sweepCall == null) {
@@ -407,7 +463,19 @@ public class DrawDownArCoreDepthPlugin extends Plugin {
407
463
  pendingSweepCall = null;
408
464
  sweepActive = false;
409
465
  observations = new ArrayList<>(sweepObservations);
466
+ rejectionCounts = new LinkedHashMap<>(sweepRejectionCounts);
467
+ rawAnchorDiagnostics = new ArrayList<>(sweepRawAnchorDiagnostics);
468
+ framesTotal = totalFrameCount;
469
+ framesTracking = trackingFrameCount;
470
+ framesRawDepth = rawDepthFrameCount;
471
+ centersMapped = mappedCenterCount;
472
+ anchorsRaw = rawAnchorCount;
473
+ patchesRaw = rawPatchCount;
474
+ matchesSmooth = smoothDepthMatchCount;
475
+ boundariesConsensus = boundaryConsensusCount;
410
476
  sweepObservations.clear();
477
+ sweepRejectionCounts.clear();
478
+ sweepRawAnchorDiagnostics.clear();
411
479
  }
412
480
 
413
481
  JSArray observationArray = new JSArray();
@@ -415,12 +483,37 @@ public class DrawDownArCoreDepthPlugin extends Plugin {
415
483
  observationArray.put(observation.toJsObject());
416
484
  }
417
485
 
486
+ JSObject rejectionObject = new JSObject();
487
+ for (Map.Entry<String, Integer> entry : rejectionCounts.entrySet()) {
488
+ rejectionObject.put(entry.getKey(), entry.getValue());
489
+ }
490
+
491
+ JSArray rawDiagnosticArray = new JSArray();
492
+ for (RawAnchorDiagnostics diagnostics : rawAnchorDiagnostics) {
493
+ rawDiagnosticArray.put(diagnostics.toJsObject());
494
+ }
495
+ JSObject rawDiagnosticSummary = buildRawAnchorDiagnosticSummary(rawAnchorDiagnostics);
496
+
418
497
  JSObject result = new JSObject();
419
498
  result.put("observations", observationArray);
420
499
  result.put("deviceModel", Build.MANUFACTURER + " " + Build.MODEL);
421
500
  result.put("arcoreVersion", installedArcoreVersion());
501
+ result.put("pluginVersion", PLUGIN_VERSION);
502
+ result.put("methodVersion", METHOD_VERSION);
422
503
  result.put("minimumRequested", minObservations);
423
504
  result.put("stoppedByCaller", stoppedByCaller);
505
+ result.put("totalFrameCount", framesTotal);
506
+ result.put("trackingFrameCount", framesTracking);
507
+ result.put("rawDepthFrameCount", framesRawDepth);
508
+ result.put("mappedCenterCount", centersMapped);
509
+ result.put("rawAnchorCount", anchorsRaw);
510
+ result.put("rawPatchCount", patchesRaw);
511
+ result.put("smoothDepthMatchCount", matchesSmooth);
512
+ result.put("boundaryConsensusCount", boundariesConsensus);
513
+ result.put("nativeValidObservationCount", observations.size());
514
+ result.put("nativeRejectionCounts", rejectionObject);
515
+ result.put("rawAnchorDiagnostics", rawDiagnosticArray);
516
+ result.put("rawAnchorDiagnosticSummary", rawDiagnosticSummary);
424
517
  sweepCall.resolve(result);
425
518
 
426
519
  if (!stoppedByCaller) {
@@ -430,6 +523,93 @@ public class DrawDownArCoreDepthPlugin extends Plugin {
430
523
  }
431
524
  }
432
525
 
526
+ private static JSObject buildRawAnchorDiagnosticSummary(List<RawAnchorDiagnostics> diagnostics) {
527
+ JSObject summary = new JSObject();
528
+ summary.put("frameCount", diagnostics.size());
529
+ if (diagnostics.isEmpty()) {
530
+ return summary;
531
+ }
532
+
533
+ int minX = Integer.MAX_VALUE;
534
+ int maxX = Integer.MIN_VALUE;
535
+ int minY = Integer.MAX_VALUE;
536
+ int maxY = Integer.MIN_VALUE;
537
+ int rawWidth = 0;
538
+ int rawHeight = 0;
539
+
540
+ for (RawAnchorDiagnostics item : diagnostics) {
541
+ minX = Math.min(minX, item.centerX);
542
+ maxX = Math.max(maxX, item.centerX);
543
+ minY = Math.min(minY, item.centerY);
544
+ maxY = Math.max(maxY, item.centerY);
545
+ rawWidth = item.imageWidth;
546
+ rawHeight = item.imageHeight;
547
+ }
548
+
549
+ summary.put("mappedCenterMinX", minX);
550
+ summary.put("mappedCenterMaxX", maxX);
551
+ summary.put("mappedCenterMinY", minY);
552
+ summary.put("mappedCenterMaxY", maxY);
553
+ summary.put("rawDepthWidth", rawWidth);
554
+ summary.put("rawDepthHeight", rawHeight);
555
+ summary.put("radius10", summarizeRawRadius(diagnostics, 10));
556
+ summary.put("radius20", summarizeRawRadius(diagnostics, 20));
557
+ summary.put("radius30", summarizeRawRadius(diagnostics, 30));
558
+ return summary;
559
+ }
560
+
561
+ private static JSObject summarizeRawRadius(List<RawAnchorDiagnostics> diagnostics, int radius) {
562
+ long totalSampled = 0L;
563
+ long totalNonZero = 0L;
564
+ long totalInRange = 0L;
565
+ long total64 = 0L;
566
+ long total96 = 0L;
567
+ long total128 = 0L;
568
+ long total160 = 0L;
569
+ int frames64 = 0;
570
+ int frames96 = 0;
571
+ int frames128 = 0;
572
+ int frames160 = 0;
573
+ int maxConfidence = 0;
574
+ int depthAtMaxConfidence = 0;
575
+
576
+ for (RawAnchorDiagnostics item : diagnostics) {
577
+ RawAnchorDiagnostics.RadiusDiagnostics r = item.forRadius(radius);
578
+ totalSampled += r.sampledPixelCount;
579
+ totalNonZero += r.nonZeroDepthCount;
580
+ totalInRange += r.inRangeDepthCount;
581
+ total64 += r.confidenceAtLeast64Count;
582
+ total96 += r.confidenceAtLeast96Count;
583
+ total128 += r.confidenceAtLeast128Count;
584
+ total160 += r.confidenceAtLeast160Count;
585
+ if (r.confidenceAtLeast64Count > 0) frames64 += 1;
586
+ if (r.confidenceAtLeast96Count > 0) frames96 += 1;
587
+ if (r.confidenceAtLeast128Count > 0) frames128 += 1;
588
+ if (r.confidenceAtLeast160Count > 0) frames160 += 1;
589
+ if (r.maxConfidenceInRange > maxConfidence) {
590
+ maxConfidence = r.maxConfidenceInRange;
591
+ depthAtMaxConfidence = r.depthMmAtMaxConfidenceInRange;
592
+ }
593
+ }
594
+
595
+ JSObject obj = new JSObject();
596
+ obj.put("radiusPx", radius);
597
+ obj.put("totalSampledPixelCount", totalSampled);
598
+ obj.put("totalNonZeroDepthCount", totalNonZero);
599
+ obj.put("totalInRangeDepthCount", totalInRange);
600
+ obj.put("totalConfidenceAtLeast64Count", total64);
601
+ obj.put("totalConfidenceAtLeast96Count", total96);
602
+ obj.put("totalConfidenceAtLeast128Count", total128);
603
+ obj.put("totalConfidenceAtLeast160Count", total160);
604
+ obj.put("framesWithConfidenceAtLeast64", frames64);
605
+ obj.put("framesWithConfidenceAtLeast96", frames96);
606
+ obj.put("framesWithConfidenceAtLeast128", frames128);
607
+ obj.put("framesWithConfidenceAtLeast160", frames160);
608
+ obj.put("maxConfidenceInRange", maxConfidence);
609
+ obj.put("depthMmAtMaxConfidenceInRange", depthAtMaxConfidence);
610
+ return obj;
611
+ }
612
+
433
613
  private void cleanupSession() {
434
614
  sweepActive = false;
435
615
 
@@ -0,0 +1,106 @@
1
+ package org.drawdown.arcoredepth;
2
+
3
+ import com.getcapacitor.JSObject;
4
+
5
+ /**
6
+ * Compact numeric diagnostics describing Raw Depth around the mapped crosshair.
7
+ * These values never influence the accepted DBH measurement; they exist only
8
+ * to distinguish confidence-threshold failures from coordinate/search failures.
9
+ */
10
+ final class RawAnchorDiagnostics {
11
+ static final int[] RADII = new int[] { 10, 20, 30 };
12
+
13
+ final int centerX;
14
+ final int centerY;
15
+ final int imageWidth;
16
+ final int imageHeight;
17
+ final RadiusDiagnostics radius10;
18
+ final RadiusDiagnostics radius20;
19
+ final RadiusDiagnostics radius30;
20
+
21
+ RawAnchorDiagnostics(
22
+ int centerX,
23
+ int centerY,
24
+ int imageWidth,
25
+ int imageHeight,
26
+ RadiusDiagnostics radius10,
27
+ RadiusDiagnostics radius20,
28
+ RadiusDiagnostics radius30) {
29
+ this.centerX = centerX;
30
+ this.centerY = centerY;
31
+ this.imageWidth = imageWidth;
32
+ this.imageHeight = imageHeight;
33
+ this.radius10 = radius10;
34
+ this.radius20 = radius20;
35
+ this.radius30 = radius30;
36
+ }
37
+
38
+ RadiusDiagnostics forRadius(int radius) {
39
+ if (radius == 10) return radius10;
40
+ if (radius == 20) return radius20;
41
+ return radius30;
42
+ }
43
+
44
+ JSObject toJsObject() {
45
+ JSObject obj = new JSObject();
46
+ obj.put("mappedCenterX", centerX);
47
+ obj.put("mappedCenterY", centerY);
48
+ obj.put("rawDepthWidth", imageWidth);
49
+ obj.put("rawDepthHeight", imageHeight);
50
+ obj.put("radius10", radius10.toJsObject());
51
+ obj.put("radius20", radius20.toJsObject());
52
+ obj.put("radius30", radius30.toJsObject());
53
+ return obj;
54
+ }
55
+
56
+ static final class RadiusDiagnostics {
57
+ final int radiusPx;
58
+ final int sampledPixelCount;
59
+ final int nonZeroDepthCount;
60
+ final int inRangeDepthCount;
61
+ final int confidenceAtLeast64Count;
62
+ final int confidenceAtLeast96Count;
63
+ final int confidenceAtLeast128Count;
64
+ final int confidenceAtLeast160Count;
65
+ final int maxConfidenceInRange;
66
+ final int depthMmAtMaxConfidenceInRange;
67
+
68
+ RadiusDiagnostics(
69
+ int radiusPx,
70
+ int sampledPixelCount,
71
+ int nonZeroDepthCount,
72
+ int inRangeDepthCount,
73
+ int confidenceAtLeast64Count,
74
+ int confidenceAtLeast96Count,
75
+ int confidenceAtLeast128Count,
76
+ int confidenceAtLeast160Count,
77
+ int maxConfidenceInRange,
78
+ int depthMmAtMaxConfidenceInRange) {
79
+ this.radiusPx = radiusPx;
80
+ this.sampledPixelCount = sampledPixelCount;
81
+ this.nonZeroDepthCount = nonZeroDepthCount;
82
+ this.inRangeDepthCount = inRangeDepthCount;
83
+ this.confidenceAtLeast64Count = confidenceAtLeast64Count;
84
+ this.confidenceAtLeast96Count = confidenceAtLeast96Count;
85
+ this.confidenceAtLeast128Count = confidenceAtLeast128Count;
86
+ this.confidenceAtLeast160Count = confidenceAtLeast160Count;
87
+ this.maxConfidenceInRange = maxConfidenceInRange;
88
+ this.depthMmAtMaxConfidenceInRange = depthMmAtMaxConfidenceInRange;
89
+ }
90
+
91
+ JSObject toJsObject() {
92
+ JSObject obj = new JSObject();
93
+ obj.put("radiusPx", radiusPx);
94
+ obj.put("sampledPixelCount", sampledPixelCount);
95
+ obj.put("nonZeroDepthCount", nonZeroDepthCount);
96
+ obj.put("inRangeDepthCount", inRangeDepthCount);
97
+ obj.put("confidenceAtLeast64Count", confidenceAtLeast64Count);
98
+ obj.put("confidenceAtLeast96Count", confidenceAtLeast96Count);
99
+ obj.put("confidenceAtLeast128Count", confidenceAtLeast128Count);
100
+ obj.put("confidenceAtLeast160Count", confidenceAtLeast160Count);
101
+ obj.put("maxConfidenceInRange", maxConfidenceInRange);
102
+ obj.put("depthMmAtMaxConfidenceInRange", depthMmAtMaxConfidenceInRange);
103
+ return obj;
104
+ }
105
+ }
106
+ }
@@ -1,54 +1,129 @@
1
1
  export type ArcoreRangeSource = 'arcore_depth16' | 'arcore_raw_depth' | 'arcore_surface_hit';
2
2
  export type BoundaryConfidence = 'high' | 'medium' | 'low';
3
+
3
4
  export interface SupportResult {
4
- isSupported: boolean;
5
- hasDepthApi: boolean;
6
- pluginVersion?: string;
7
- message?: string;
5
+ isSupported: boolean;
6
+ hasDepthApi: boolean;
7
+ pluginVersion?: string;
8
+ message?: string;
8
9
  }
10
+
9
11
  export interface StartDbhSessionOptions {
10
- /** RFID/NFC tree tag is the field anchor that identifies the 1.3 m DBH plane. */
11
- anchorType?: 'rfid_tag';
12
- targetHeightM: 1.3;
13
- /** @deprecated Ignored from v0.1.2; retained only for backward compatibility. */
14
- markerDictionary?: 'DICT_6X6_250';
15
- /** @deprecated Ignored from v0.1.2; retained only for backward compatibility. */
16
- markerId?: 0;
17
- /** @deprecated Ignored from v0.1.2; retained only for backward compatibility. */
18
- markerSizeCm?: 10;
12
+ /** RFID/NFC tree tag is the field anchor that identifies the 1.3 m DBH plane. */
13
+ anchorType?: 'rfid_tag';
14
+ targetHeightM: 1.3;
15
+ /** @deprecated Ignored from v0.1.2; retained only for backward compatibility. */
16
+ markerDictionary?: 'DICT_6X6_250';
17
+ /** @deprecated Ignored from v0.1.2; retained only for backward compatibility. */
18
+ markerId?: 0;
19
+ /** @deprecated Ignored from v0.1.2; retained only for backward compatibility. */
20
+ markerSizeCm?: 10;
19
21
  }
22
+
20
23
  export interface StartDbhSessionResult {
21
- started: boolean;
22
- installationRequested?: boolean;
23
- message?: string;
24
+ started: boolean;
25
+ installationRequested?: boolean;
26
+ message?: string;
24
27
  }
28
+
25
29
  export interface CaptureDbhSweepOptions {
26
- durationMs: number;
27
- minObservations: number;
28
- maxObservations: number;
29
- retainFrames: false;
30
+ durationMs: number;
31
+ minObservations: number;
32
+ maxObservations: number;
33
+ retainFrames: false;
30
34
  }
35
+
31
36
  export interface DbhObservation {
32
- frontSurfaceDepthM: number;
33
- /** Effective focal length in pixels along the measured trunk-width axis. */
34
- focalLengthXPx: number;
35
- /** Trunk tangent-to-tangent apparent width in corresponding image pixels. */
36
- trunkWidthPx: number;
37
- rangeSource: ArcoreRangeSource;
38
- trackingConfidence?: number;
39
- boundaryConfidence?: BoundaryConfidence;
40
- depthMadM?: number;
41
- timestampMs?: number;
37
+ frontSurfaceDepthM: number;
38
+ /** Effective focal length in pixels along the measured trunk-width axis. */
39
+ focalLengthXPx: number;
40
+ /** Trunk tangent-to-tangent apparent width in corresponding image pixels. */
41
+ trunkWidthPx: number;
42
+ rangeSource: ArcoreRangeSource;
43
+ trackingConfidence?: number;
44
+ boundaryConfidence?: BoundaryConfidence;
45
+ depthMadM?: number;
46
+ timestampMs?: number;
42
47
  }
48
+
49
+ export interface RawAnchorRadiusDiagnostics {
50
+ radiusPx: number;
51
+ sampledPixelCount: number;
52
+ nonZeroDepthCount: number;
53
+ inRangeDepthCount: number;
54
+ confidenceAtLeast64Count: number;
55
+ confidenceAtLeast96Count: number;
56
+ confidenceAtLeast128Count: number;
57
+ confidenceAtLeast160Count: number;
58
+ maxConfidenceInRange: number;
59
+ depthMmAtMaxConfidenceInRange: number;
60
+ }
61
+
62
+ export interface RawAnchorFrameDiagnostics {
63
+ mappedCenterX: number;
64
+ mappedCenterY: number;
65
+ rawDepthWidth: number;
66
+ rawDepthHeight: number;
67
+ radius10: RawAnchorRadiusDiagnostics;
68
+ radius20: RawAnchorRadiusDiagnostics;
69
+ radius30: RawAnchorRadiusDiagnostics;
70
+ }
71
+
72
+ export interface RawAnchorRadiusSummary {
73
+ radiusPx: number;
74
+ totalSampledPixelCount: number;
75
+ totalNonZeroDepthCount: number;
76
+ totalInRangeDepthCount: number;
77
+ totalConfidenceAtLeast64Count: number;
78
+ totalConfidenceAtLeast96Count: number;
79
+ totalConfidenceAtLeast128Count: number;
80
+ totalConfidenceAtLeast160Count: number;
81
+ framesWithConfidenceAtLeast64: number;
82
+ framesWithConfidenceAtLeast96: number;
83
+ framesWithConfidenceAtLeast128: number;
84
+ framesWithConfidenceAtLeast160: number;
85
+ maxConfidenceInRange: number;
86
+ depthMmAtMaxConfidenceInRange: number;
87
+ }
88
+
89
+ export interface RawAnchorDiagnosticSummary {
90
+ frameCount: number;
91
+ mappedCenterMinX?: number;
92
+ mappedCenterMaxX?: number;
93
+ mappedCenterMinY?: number;
94
+ mappedCenterMaxY?: number;
95
+ rawDepthWidth?: number;
96
+ rawDepthHeight?: number;
97
+ radius10?: RawAnchorRadiusSummary;
98
+ radius20?: RawAnchorRadiusSummary;
99
+ radius30?: RawAnchorRadiusSummary;
100
+ }
101
+
43
102
  export interface CaptureDbhSweepResult {
44
- observations: DbhObservation[];
45
- deviceModel?: string;
46
- arcoreVersion?: string;
103
+ observations: DbhObservation[];
104
+ deviceModel?: string;
105
+ arcoreVersion?: string;
106
+ pluginVersion?: string;
107
+ methodVersion?: string;
108
+ totalFrameCount?: number;
109
+ trackingFrameCount?: number;
110
+ rawDepthFrameCount?: number;
111
+ mappedCenterCount?: number;
112
+ rawAnchorCount?: number;
113
+ rawPatchCount?: number;
114
+ smoothDepthMatchCount?: number;
115
+ boundaryConsensusCount?: number;
116
+ nativeValidObservationCount?: number;
117
+ nativeRejectionCounts?: Record<string, number>;
118
+ rawAnchorDiagnostics?: RawAnchorFrameDiagnostics[];
119
+ rawAnchorDiagnosticSummary?: RawAnchorDiagnosticSummary;
47
120
  }
121
+
48
122
  export interface DrawDownArCoreDepthPlugin {
49
- checkSupport(): Promise<SupportResult>;
50
- startDbhSession(options: StartDbhSessionOptions): Promise<StartDbhSessionResult>;
51
- captureDbhSweep(options: CaptureDbhSweepOptions): Promise<CaptureDbhSweepResult>;
52
- stopDbhSession(): Promise<void>;
123
+ checkSupport(): Promise<SupportResult>;
124
+ startDbhSession(options: StartDbhSessionOptions): Promise<StartDbhSessionResult>;
125
+ captureDbhSweep(options: CaptureDbhSweepOptions): Promise<CaptureDbhSweepResult>;
126
+ stopDbhSession(): Promise<void>;
53
127
  }
54
- //# sourceMappingURL=definitions.d.ts.map
128
+
129
+ //# sourceMappingURL=definitions.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "drawdown-arcore-depth",
3
- "version": "0.1.4",
4
- "description": "Small Android-only Capacitor 8 bridge for ARCore Depth DBH observations.",
3
+ "version": "0.1.6",
4
+ "description": "Small Android-only Capacitor 8 bridge for ARCore Raw Depth DBH observations with native Raw Depth anchor diagnostics.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
7
7
  "types": "dist/esm/index.d.ts",