drawdown-arcore-depth 0.1.4

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.
@@ -0,0 +1,509 @@
1
+ package org.drawdown.arcoredepth;
2
+
3
+ import android.media.Image;
4
+
5
+ import com.google.ar.core.Camera;
6
+ import com.google.ar.core.CameraIntrinsics;
7
+ import com.google.ar.core.Coordinates2d;
8
+ import com.google.ar.core.Frame;
9
+ import com.google.ar.core.TrackingState;
10
+ import com.google.ar.core.exceptions.NotYetAvailableException;
11
+
12
+ import java.nio.ByteBuffer;
13
+ import java.nio.ByteOrder;
14
+ import java.util.ArrayList;
15
+ import java.util.Collections;
16
+ import java.util.List;
17
+
18
+ /**
19
+ * DBH sampler anchored by ARCore Raw Depth.
20
+ *
21
+ * Raw Depth is deliberately used for the front-surface range because it is the
22
+ * ARCore API intended for measurement/shape work and supplies a per-pixel
23
+ * confidence image. The smoothed Depth image is used only to fill the trunk
24
+ * surface enough to estimate left/right boundaries, and only after Raw Depth
25
+ * has established a nearby, high-confidence foreground range.
26
+ */
27
+ final class DepthObservationSampler {
28
+ private static final int RAW_CONFIDENCE_THRESHOLD = 128;
29
+ private static final int RAW_ANCHOR_SEARCH_RADIUS = 10;
30
+ private static final int RAW_PATCH_RADIUS = 5;
31
+ private static final int MIN_RAW_PATCH_SAMPLES = 3;
32
+ private static final int ROW_RADIUS = 2;
33
+ private static final int MAX_GAP_PIXELS = 1;
34
+ private static final int[] BOUNDARY_ROW_OFFSETS = new int[] { -4, -2, 0, 2, 4 };
35
+ private static final int MIN_CONSENSUS_ROWS = 3;
36
+ private static final int MAX_BOUNDARY_SPREAD_PX = 4;
37
+ private static final double MAX_TRUNK_WIDTH_FRACTION = 0.68;
38
+ private static final int EDGE_MARGIN_PX = 2;
39
+ private static final double MIN_TARGET_DEPTH_MM = 500.0;
40
+ private static final double MAX_TARGET_DEPTH_MM = 5000.0;
41
+
42
+ DepthObservation sample(Frame frame, Camera camera, int viewWidth, int viewHeight) {
43
+ if (camera.getTrackingState() != TrackingState.TRACKING) {
44
+ return null;
45
+ }
46
+
47
+ try (
48
+ Image rawDepth = frame.acquireRawDepthImage16Bits();
49
+ Image rawConfidence = frame.acquireRawDepthConfidenceImage();
50
+ Image smoothDepth = frame.acquireDepthImage16Bits()) {
51
+
52
+ CameraIntrinsics intrinsics = camera.getImageIntrinsics();
53
+ float[] focal = intrinsics.getFocalLength();
54
+ int[] imageDimensions = intrinsics.getImageDimensions();
55
+ if (focal == null || focal.length < 2 || imageDimensions == null || imageDimensions.length < 2) {
56
+ return null;
57
+ }
58
+
59
+ // ARCore documents raw depth, confidence and smoothed depth as the same size.
60
+ if (rawDepth.getWidth() != rawConfidence.getWidth()
61
+ || rawDepth.getHeight() != rawConfidence.getHeight()
62
+ || rawDepth.getWidth() != smoothDepth.getWidth()
63
+ || rawDepth.getHeight() != smoothDepth.getHeight()) {
64
+ return null;
65
+ }
66
+
67
+ float[] viewCenter = new float[] { viewWidth / 2f, viewHeight / 2f };
68
+ float[] imageCenter = new float[2];
69
+ frame.transformCoordinates2d(
70
+ Coordinates2d.VIEW,
71
+ viewCenter,
72
+ Coordinates2d.IMAGE_PIXELS,
73
+ imageCenter);
74
+
75
+ int[] mappedCenter = imageToDepth(frame, rawDepth, imageCenter[0], imageCenter[1]);
76
+ if (mappedCenter == null) {
77
+ return null;
78
+ }
79
+
80
+ RawAnchor anchor = findRawAnchor(rawDepth, rawConfidence, mappedCenter[0], mappedCenter[1]);
81
+ if (anchor == null) {
82
+ return null;
83
+ }
84
+
85
+ double rawToleranceMm = clamp(anchor.depthMm * 0.10, 100.0, 250.0);
86
+ List<Integer> rawPatch = collectConfidentRawPatch(
87
+ rawDepth,
88
+ rawConfidence,
89
+ anchor.x,
90
+ anchor.y,
91
+ RAW_PATCH_RADIUS,
92
+ rawToleranceMm,
93
+ anchor.depthMm);
94
+ if (rawPatch.size() < MIN_RAW_PATCH_SAMPLES) {
95
+ return null;
96
+ }
97
+
98
+ double centerDepthMm = median(rawPatch);
99
+ double centerMadMm = mad(rawPatch, centerDepthMm);
100
+ if (centerDepthMm < MIN_TARGET_DEPTH_MM || centerDepthMm > MAX_TARGET_DEPTH_MM) {
101
+ return null;
102
+ }
103
+
104
+ // Locate the foreground surface in the smoothed depth image, but only
105
+ // near the high-confidence raw-depth anchor. This prevents the 20-30 m
106
+ // outdoor background from becoming the trunk range.
107
+ int centerX = findNearestSmoothPixelAtDepth(
108
+ smoothDepth,
109
+ anchor.x,
110
+ anchor.y,
111
+ centerDepthMm,
112
+ clamp(centerDepthMm * 0.10, 100.0, 250.0));
113
+ if (centerX < 0) {
114
+ return null;
115
+ }
116
+
117
+ double toleranceMm = clamp(centerDepthMm * 0.08, 80.0, 220.0);
118
+ Boundary boundary = findConsensusBoundary(
119
+ smoothDepth,
120
+ centerX,
121
+ anchor.y,
122
+ centerDepthMm,
123
+ toleranceMm);
124
+ if (boundary == null || boundary.rightX - boundary.leftX < 3) {
125
+ return null;
126
+ }
127
+
128
+ int depthWidth = smoothDepth.getWidth();
129
+ int boundaryWidth = boundary.rightX - boundary.leftX;
130
+ if (boundary.leftX <= EDGE_MARGIN_PX
131
+ || boundary.rightX >= depthWidth - 1 - EDGE_MARGIN_PX
132
+ || boundaryWidth > depthWidth * MAX_TRUNK_WIDTH_FRACTION) {
133
+ return null;
134
+ }
135
+
136
+ float[] leftImage = depthToImage(frame, smoothDepth, boundary.leftX, anchor.y);
137
+ float[] rightImage = depthToImage(frame, smoothDepth, boundary.rightX, anchor.y);
138
+ if (leftImage == null || rightImage == null) {
139
+ return null;
140
+ }
141
+
142
+ double dx = rightImage[0] - leftImage[0];
143
+ double dy = rightImage[1] - leftImage[1];
144
+ double imageWidthPx = Math.hypot(dx, dy);
145
+ if (!Double.isFinite(imageWidthPx) || imageWidthPx < 8.0) {
146
+ return null;
147
+ }
148
+
149
+ double normalizedAngularWidth = Math.sqrt(
150
+ Math.pow(dx / focal[0], 2.0)
151
+ + Math.pow(dy / focal[1], 2.0));
152
+ if (!Double.isFinite(normalizedAngularWidth)
153
+ || normalizedAngularWidth <= 0.0
154
+ || normalizedAngularWidth > 0.95) {
155
+ return null;
156
+ }
157
+
158
+ double effectiveFocalPx = imageWidthPx / normalizedAngularWidth;
159
+ double confidenceMedian = medianConfidence(rawConfidence, anchor.x, anchor.y, RAW_PATCH_RADIUS);
160
+ double trackingConfidence = clamp(confidenceMedian / 255.0, 0.0, 1.0);
161
+
162
+ String boundaryConfidence;
163
+ if (boundary.consensusRows >= 4
164
+ && boundary.maxSpreadPx <= 2
165
+ && boundary.leftJumpMm >= 120.0
166
+ && boundary.rightJumpMm >= 120.0
167
+ && boundaryWidth >= 6
168
+ && centerMadMm <= 50.0
169
+ && trackingConfidence >= 0.65) {
170
+ boundaryConfidence = "high";
171
+ } else if (boundary.consensusRows >= MIN_CONSENSUS_ROWS
172
+ && boundary.maxSpreadPx <= MAX_BOUNDARY_SPREAD_PX
173
+ && boundaryWidth >= 4
174
+ && centerMadMm <= 90.0
175
+ && trackingConfidence >= 0.50) {
176
+ boundaryConfidence = "medium";
177
+ } else {
178
+ boundaryConfidence = "low";
179
+ }
180
+
181
+ return new DepthObservation(
182
+ centerDepthMm / 1000.0,
183
+ effectiveFocalPx,
184
+ imageWidthPx,
185
+ "arcore_raw_depth",
186
+ trackingConfidence,
187
+ boundaryConfidence,
188
+ centerMadMm / 1000.0,
189
+ System.currentTimeMillis());
190
+ } catch (NotYetAvailableException e) {
191
+ return null;
192
+ } catch (Exception e) {
193
+ return null;
194
+ }
195
+ }
196
+
197
+ private static RawAnchor findRawAnchor(Image rawDepth, Image confidence, int centerX, int centerY) {
198
+ RawAnchor best = null;
199
+ double bestScore = Double.POSITIVE_INFINITY;
200
+
201
+ for (int y = Math.max(0, centerY - RAW_ANCHOR_SEARCH_RADIUS);
202
+ y <= Math.min(rawDepth.getHeight() - 1, centerY + RAW_ANCHOR_SEARCH_RADIUS);
203
+ y++) {
204
+ for (int x = Math.max(0, centerX - RAW_ANCHOR_SEARCH_RADIUS);
205
+ x <= Math.min(rawDepth.getWidth() - 1, centerX + RAW_ANCHOR_SEARCH_RADIUS);
206
+ x++) {
207
+ int conf = getConfidence(confidence, x, y);
208
+ if (conf < RAW_CONFIDENCE_THRESHOLD) continue;
209
+ int depthMm = getDepthMm(rawDepth, x, y);
210
+ if (depthMm < MIN_TARGET_DEPTH_MM || depthMm > MAX_TARGET_DEPTH_MM) continue;
211
+
212
+ double spatial = Math.hypot(x - centerX, y - centerY);
213
+ // Prefer the pixel nearest the crosshair, breaking near-ties in
214
+ // favour of higher raw-depth confidence.
215
+ double score = spatial - (conf / 255.0) * 0.35;
216
+ if (score < bestScore) {
217
+ bestScore = score;
218
+ best = new RawAnchor(x, y, depthMm, conf);
219
+ }
220
+ }
221
+ }
222
+ return best;
223
+ }
224
+
225
+ private static List<Integer> collectConfidentRawPatch(
226
+ Image rawDepth,
227
+ Image confidence,
228
+ int centerX,
229
+ int centerY,
230
+ int radius,
231
+ double toleranceMm,
232
+ double expectedDepthMm) {
233
+ List<Integer> values = new ArrayList<>();
234
+ for (int y = Math.max(0, centerY - radius); y <= Math.min(rawDepth.getHeight() - 1, centerY + radius); y++) {
235
+ for (int x = Math.max(0, centerX - radius); x <= Math.min(rawDepth.getWidth() - 1, centerX + radius); x++) {
236
+ if (getConfidence(confidence, x, y) < RAW_CONFIDENCE_THRESHOLD) continue;
237
+ int value = getDepthMm(rawDepth, x, y);
238
+ if (value > 0 && Math.abs(value - expectedDepthMm) <= toleranceMm) {
239
+ values.add(value);
240
+ }
241
+ }
242
+ }
243
+ return values;
244
+ }
245
+
246
+ private static int findNearestSmoothPixelAtDepth(
247
+ Image smoothDepth,
248
+ int rawX,
249
+ int rawY,
250
+ double targetDepthMm,
251
+ double toleranceMm) {
252
+ int bestX = -1;
253
+ double bestDistance = Double.POSITIVE_INFINITY;
254
+ for (int offset = 0; offset <= 6; offset++) {
255
+ int[] xs = offset == 0 ? new int[] { rawX } : new int[] { rawX - offset, rawX + offset };
256
+ for (int x : xs) {
257
+ if (x < 0 || x >= smoothDepth.getWidth()) continue;
258
+ double value = rowMedian(smoothDepth, x, rawY, ROW_RADIUS);
259
+ if (value <= 0 || Math.abs(value - targetDepthMm) > toleranceMm) continue;
260
+ double distance = Math.abs(x - rawX);
261
+ if (distance < bestDistance) {
262
+ bestDistance = distance;
263
+ bestX = x;
264
+ }
265
+ }
266
+ if (bestX >= 0) return bestX;
267
+ }
268
+ return -1;
269
+ }
270
+
271
+ private static int[] imageToDepth(Frame frame, Image depthImage, float imageX, float imageY) {
272
+ float[] cpu = new float[] { imageX, imageY };
273
+ float[] texture = new float[2];
274
+ frame.transformCoordinates2d(
275
+ Coordinates2d.IMAGE_PIXELS,
276
+ cpu,
277
+ Coordinates2d.TEXTURE_NORMALIZED,
278
+ texture);
279
+ if (!isTextureCoordinateValid(texture)) return null;
280
+ int x = clampInt((int) Math.floor(texture[0] * depthImage.getWidth()), 0, depthImage.getWidth() - 1);
281
+ int y = clampInt((int) Math.floor(texture[1] * depthImage.getHeight()), 0, depthImage.getHeight() - 1);
282
+ return new int[] { x, y };
283
+ }
284
+
285
+ private static float[] depthToImage(Frame frame, Image depthImage, int depthX, int depthY) {
286
+ float[] texture = new float[] {
287
+ depthX / (float) depthImage.getWidth(),
288
+ depthY / (float) depthImage.getHeight()
289
+ };
290
+ float[] cpu = new float[2];
291
+ frame.transformCoordinates2d(
292
+ Coordinates2d.TEXTURE_NORMALIZED,
293
+ texture,
294
+ Coordinates2d.IMAGE_PIXELS,
295
+ cpu);
296
+ if (!Float.isFinite(cpu[0]) || !Float.isFinite(cpu[1])) return null;
297
+ return cpu;
298
+ }
299
+
300
+ private static boolean isTextureCoordinateValid(float[] texture) {
301
+ return texture != null && texture.length >= 2
302
+ && Float.isFinite(texture[0]) && Float.isFinite(texture[1])
303
+ && texture[0] >= 0f && texture[0] <= 1f
304
+ && texture[1] >= 0f && texture[1] <= 1f;
305
+ }
306
+
307
+ private static Boundary findConsensusBoundary(
308
+ Image depthImage,
309
+ int centerX,
310
+ int centerY,
311
+ double centerDepthMm,
312
+ double toleranceMm) {
313
+ List<Boundary> candidates = new ArrayList<>();
314
+ for (int offset : BOUNDARY_ROW_OFFSETS) {
315
+ int y = centerY + offset;
316
+ if (y < ROW_RADIUS || y >= depthImage.getHeight() - ROW_RADIUS) continue;
317
+ double rowCenterDepth = rowMedian(depthImage, centerX, y, ROW_RADIUS);
318
+ if (rowCenterDepth <= 0 || Math.abs(rowCenterDepth - centerDepthMm) > toleranceMm) continue;
319
+ double rowTolerance = clamp(rowCenterDepth * 0.08, 80.0, 220.0);
320
+ Boundary candidate = findDepthBoundary(depthImage, centerX, y, rowCenterDepth, rowTolerance);
321
+ if (candidate == null || candidate.rightX - candidate.leftX < 3) continue;
322
+ if (candidate.leftX <= EDGE_MARGIN_PX
323
+ || candidate.rightX >= depthImage.getWidth() - 1 - EDGE_MARGIN_PX
324
+ || candidate.rightX - candidate.leftX > depthImage.getWidth() * MAX_TRUNK_WIDTH_FRACTION) continue;
325
+ candidates.add(candidate);
326
+ }
327
+ if (candidates.size() < MIN_CONSENSUS_ROWS) return null;
328
+
329
+ List<Integer> lefts = new ArrayList<>();
330
+ List<Integer> rights = new ArrayList<>();
331
+ List<Integer> widths = new ArrayList<>();
332
+ List<Double> leftJumps = new ArrayList<>();
333
+ List<Double> rightJumps = new ArrayList<>();
334
+ for (Boundary candidate : candidates) {
335
+ lefts.add(candidate.leftX);
336
+ rights.add(candidate.rightX);
337
+ widths.add(candidate.rightX - candidate.leftX);
338
+ leftJumps.add(candidate.leftJumpMm);
339
+ rightJumps.add(candidate.rightJumpMm);
340
+ }
341
+ int left = (int) Math.round(medianInts(lefts));
342
+ int right = (int) Math.round(medianInts(rights));
343
+ int medianWidth = (int) Math.round(medianInts(widths));
344
+ int spread = Math.max(maxAbsoluteDeviation(lefts, left), maxAbsoluteDeviation(rights, right));
345
+ if (left >= right || medianWidth < 3 || spread > MAX_BOUNDARY_SPREAD_PX) return null;
346
+ return new Boundary(left, right, medianDoubles(leftJumps), medianDoubles(rightJumps), candidates.size(), spread);
347
+ }
348
+
349
+ private static Boundary findDepthBoundary(
350
+ Image depthImage,
351
+ int centerX,
352
+ int y,
353
+ double centerDepthMm,
354
+ double toleranceMm) {
355
+ int left = walkBoundary(depthImage, centerX, y, centerDepthMm, toleranceMm, -1);
356
+ int right = walkBoundary(depthImage, centerX, y, centerDepthMm, toleranceMm, 1);
357
+ if (left >= right) return null;
358
+ double leftOutside = outsideMedian(depthImage, left, y, -1);
359
+ double rightOutside = outsideMedian(depthImage, right, y, 1);
360
+ double leftJump = leftOutside > 0 ? Math.abs(leftOutside - centerDepthMm) : toleranceMm * 2.0;
361
+ double rightJump = rightOutside > 0 ? Math.abs(rightOutside - centerDepthMm) : toleranceMm * 2.0;
362
+ return new Boundary(left, right, leftJump, rightJump, 1, 0);
363
+ }
364
+
365
+ private static int walkBoundary(
366
+ Image depthImage,
367
+ int startX,
368
+ int y,
369
+ double centerDepthMm,
370
+ double toleranceMm,
371
+ int direction) {
372
+ int x = startX;
373
+ int lastGood = startX;
374
+ int gap = 0;
375
+ while (true) {
376
+ x += direction;
377
+ if (x < 0 || x >= depthImage.getWidth()) break;
378
+ double value = rowMedian(depthImage, x, y, ROW_RADIUS);
379
+ boolean matches = value > 0 && Math.abs(value - centerDepthMm) <= toleranceMm;
380
+ if (matches) {
381
+ lastGood = x;
382
+ gap = 0;
383
+ } else if (++gap > MAX_GAP_PIXELS) {
384
+ break;
385
+ }
386
+ }
387
+ return lastGood;
388
+ }
389
+
390
+ private static double outsideMedian(Image depthImage, int boundaryX, int y, int direction) {
391
+ List<Integer> values = new ArrayList<>();
392
+ for (int step = 2; step <= 4; step++) {
393
+ int x = boundaryX + direction * step;
394
+ if (x < 0 || x >= depthImage.getWidth()) continue;
395
+ int value = getDepthMm(depthImage, x, y);
396
+ if (value > 0) values.add(value);
397
+ }
398
+ return values.isEmpty() ? 0.0 : median(values);
399
+ }
400
+
401
+ private static double rowMedian(Image depthImage, int x, int y, int radiusY) {
402
+ List<Integer> values = new ArrayList<>();
403
+ for (int yy = Math.max(0, y - radiusY); yy <= Math.min(depthImage.getHeight() - 1, y + radiusY); yy++) {
404
+ int value = getDepthMm(depthImage, x, yy);
405
+ if (value > 0) values.add(value);
406
+ }
407
+ return values.size() < 2 ? 0.0 : median(values);
408
+ }
409
+
410
+ private static int getDepthMm(Image depthImage, int x, int y) {
411
+ if (x < 0 || y < 0 || x >= depthImage.getWidth() || y >= depthImage.getHeight()) return 0;
412
+ Image.Plane plane = depthImage.getPlanes()[0];
413
+ int byteIndex = x * plane.getPixelStride() + y * plane.getRowStride();
414
+ ByteBuffer buffer = plane.getBuffer().order(ByteOrder.nativeOrder());
415
+ if (byteIndex < 0 || byteIndex + 1 >= buffer.capacity()) return 0;
416
+ return Short.toUnsignedInt(buffer.getShort(byteIndex));
417
+ }
418
+
419
+ private static int getConfidence(Image confidence, int x, int y) {
420
+ if (x < 0 || y < 0 || x >= confidence.getWidth() || y >= confidence.getHeight()) return 0;
421
+ Image.Plane plane = confidence.getPlanes()[0];
422
+ int byteIndex = x * plane.getPixelStride() + y * plane.getRowStride();
423
+ ByteBuffer buffer = plane.getBuffer();
424
+ if (byteIndex < 0 || byteIndex >= buffer.capacity()) return 0;
425
+ return Byte.toUnsignedInt(buffer.get(byteIndex));
426
+ }
427
+
428
+ private static double medianConfidence(Image confidence, int centerX, int centerY, int radius) {
429
+ List<Integer> values = new ArrayList<>();
430
+ for (int y = Math.max(0, centerY - radius); y <= Math.min(confidence.getHeight() - 1, centerY + radius); y++) {
431
+ for (int x = Math.max(0, centerX - radius); x <= Math.min(confidence.getWidth() - 1, centerX + radius); x++) {
432
+ int value = getConfidence(confidence, x, y);
433
+ if (value > 0) values.add(value);
434
+ }
435
+ }
436
+ return values.isEmpty() ? 0.0 : median(values);
437
+ }
438
+
439
+ private static double median(List<Integer> values) {
440
+ if (values.isEmpty()) return 0.0;
441
+ List<Integer> sorted = new ArrayList<>(values);
442
+ Collections.sort(sorted);
443
+ int mid = sorted.size() / 2;
444
+ return sorted.size() % 2 == 1 ? sorted.get(mid) : (sorted.get(mid - 1) + sorted.get(mid)) / 2.0;
445
+ }
446
+
447
+ private static double medianInts(List<Integer> values) { return median(values); }
448
+
449
+ private static double medianDoubles(List<Double> values) {
450
+ if (values.isEmpty()) return 0.0;
451
+ List<Double> sorted = new ArrayList<>(values);
452
+ Collections.sort(sorted);
453
+ int mid = sorted.size() / 2;
454
+ return sorted.size() % 2 == 1 ? sorted.get(mid) : (sorted.get(mid - 1) + sorted.get(mid)) / 2.0;
455
+ }
456
+
457
+ private static double mad(List<Integer> values, double center) {
458
+ if (values.isEmpty()) return 0.0;
459
+ List<Integer> deviations = new ArrayList<>(values.size());
460
+ for (int value : values) deviations.add((int) Math.round(Math.abs(value - center)));
461
+ return median(deviations);
462
+ }
463
+
464
+ private static int maxAbsoluteDeviation(List<Integer> values, int center) {
465
+ int max = 0;
466
+ for (int value : values) max = Math.max(max, Math.abs(value - center));
467
+ return max;
468
+ }
469
+
470
+ private static double clamp(double value, double min, double max) {
471
+ return Math.max(min, Math.min(max, value));
472
+ }
473
+
474
+ private static int clampInt(int value, int min, int max) {
475
+ return Math.max(min, Math.min(max, value));
476
+ }
477
+
478
+ private static final class RawAnchor {
479
+ final int x;
480
+ final int y;
481
+ final double depthMm;
482
+ final int confidence;
483
+
484
+ RawAnchor(int x, int y, double depthMm, int confidence) {
485
+ this.x = x;
486
+ this.y = y;
487
+ this.depthMm = depthMm;
488
+ this.confidence = confidence;
489
+ }
490
+ }
491
+
492
+ private static final class Boundary {
493
+ final int leftX;
494
+ final int rightX;
495
+ final double leftJumpMm;
496
+ final double rightJumpMm;
497
+ final int consensusRows;
498
+ final int maxSpreadPx;
499
+
500
+ Boundary(int leftX, int rightX, double leftJumpMm, double rightJumpMm, int consensusRows, int maxSpreadPx) {
501
+ this.leftX = leftX;
502
+ this.rightX = rightX;
503
+ this.leftJumpMm = leftJumpMm;
504
+ this.rightJumpMm = rightJumpMm;
505
+ this.consensusRows = consensusRows;
506
+ this.maxSpreadPx = maxSpreadPx;
507
+ }
508
+ }
509
+ }