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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DrawDown
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE.md ADDED
@@ -0,0 +1,7 @@
1
+ # Notices
2
+
3
+ This plugin depends on Google ARCore (`com.google.ar:core`). Use of ARCore is subject to Google's ARCore Additional Terms of Service and user-privacy requirements.
4
+
5
+ The plugin does not use Cloud Anchors, Geospatial APIs, VPS, or any network-based AR service. Its DBH sweep uses the local ARCore Depth API and transient in-memory processing.
6
+
7
+ ARCore depth is an experimental QC input for DrawDown. It must not replace the authoritative forestry diameter-tape measurement unless a future validation and methodology decision explicitly changes that policy.
package/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # drawdown-arcore-depth
2
+
3
+ Android-only Capacitor 8 bridge for the DrawDown experimental ARCore DBH quality-control workflow.
4
+
5
+ ## Purpose
6
+
7
+ The plugin exposes local ARCore Depth measurements to a Capacitor app without requiring cloud services, video storage, LiDAR, Cloud Anchors, Geospatial APIs, or server-side inference.
8
+
9
+ It is deliberately narrow. During a short DBH sweep it:
10
+
11
+ 1. opens a native ARCore camera preview;
12
+ 2. enables `Config.DepthMode.AUTOMATIC`;
13
+ 3. asks the inspector to align the 1.3 m DrawDown tag with a centre crosshair;
14
+ 4. acquires ARCore 16-bit depth frames locally;
15
+ 5. finds the contiguous depth surface around the crosshair and estimates left/right trunk boundaries from the depth discontinuity;
16
+ 6. converts those boundaries back into CPU image coordinates using ARCore coordinate transforms;
17
+ 7. combines camera intrinsics with the apparent width to return compact DBH observations;
18
+ 8. discards all intermediate frames.
19
+
20
+ The plugin does **not** calculate carbon and does **not** replace the forestry diameter tape. In DrawDown, `manualDbhCm` remains authoritative; the ARCore result is experimental QC evidence only.
21
+
22
+ ## Capacitor contract
23
+
24
+ The native plugin registers as:
25
+
26
+ ```ts
27
+ DrawDownArCoreDepth
28
+ ```
29
+
30
+ Methods:
31
+
32
+ ```ts
33
+ checkSupport(): Promise<{
34
+ isSupported: boolean;
35
+ hasDepthApi: boolean;
36
+ pluginVersion?: string;
37
+ message?: string;
38
+ }>
39
+ ```
40
+
41
+ ```ts
42
+ startDbhSession({
43
+ tagDictionary: 'DICT_6X6_250',
44
+ tagId: 0,
45
+ tagSizeCm: 10,
46
+ targetHeightM: 1.3,
47
+ }): Promise<{ started: boolean }>
48
+ ```
49
+
50
+ ```ts
51
+ captureDbhSweep({
52
+ durationMs: 3000,
53
+ minObservations: 5,
54
+ maxObservations: 10,
55
+ retainFrames: false,
56
+ }): Promise<{
57
+ observations: Array<{
58
+ frontSurfaceDepthM: number;
59
+ focalLengthXPx: number;
60
+ trunkWidthPx: number;
61
+ rangeSource: 'arcore_depth16';
62
+ trackingConfidence?: number;
63
+ boundaryConfidence?: 'high' | 'medium' | 'low';
64
+ depthMadM?: number;
65
+ timestampMs?: number;
66
+ }>;
67
+ deviceModel?: string;
68
+ arcoreVersion?: string;
69
+ }>
70
+ ```
71
+
72
+ ```ts
73
+ stopDbhSession(): Promise<void>
74
+ ```
75
+
76
+ This matches the `DrawDownArCoreDepth` adapter already prepared in DrawDown.
77
+
78
+ ## Installation
79
+
80
+ From a local tarball:
81
+
82
+ ```bash
83
+ npm install ./drawdown-arcore-depth-0.1.1.tgz
84
+ npx cap sync android
85
+ ```
86
+
87
+ Or, after publishing the package to a registry:
88
+
89
+ ```bash
90
+ npm install drawdown-arcore-depth
91
+ npx cap sync android
92
+ ```
93
+
94
+ Then import it if direct typed access is wanted:
95
+
96
+ ```ts
97
+ import { DrawDownArCoreDepth } from 'drawdown-arcore-depth';
98
+ ```
99
+
100
+ The current DrawDown adapter can also resolve the registered native plugin by its Capacitor name.
101
+
102
+ ## Android requirements
103
+
104
+ - Capacitor 8
105
+ - Android min SDK 24+
106
+ - ARCore-supported Android device
107
+ - device support for the ARCore Depth API
108
+ - Google Play Services for AR installed and current
109
+ - camera permission
110
+
111
+ The plugin declares ARCore and ARCore Depth as optional features so the wider DrawDown app can still install on unsupported devices. The app must runtime-gate the experimental DBH feature with `checkSupport()`.
112
+
113
+ ## Offline behaviour
114
+
115
+ The measurement path is local:
116
+
117
+ ```text
118
+ ARCore camera
119
+ -> local depth map
120
+ -> local boundary sampling
121
+ -> compact numeric observations
122
+ -> DrawDown on-device DBH aggregation
123
+ -> offline sync queue
124
+ ```
125
+
126
+ No internet connection is required for the sweep itself.
127
+
128
+ ## Storage behaviour
129
+
130
+ `retainFrames: true` is rejected by design.
131
+
132
+ The plugin retains no sweep video and no routine RGB/depth frames. Only compact numeric observations are returned to DrawDown. DrawDown can continue to retain its normal single inspection photograph separately.
133
+
134
+ ## Measurement geometry
135
+
136
+ The plugin returns:
137
+
138
+ - front-surface z-depth to the trunk around the crosshair;
139
+ - apparent trunk width in CPU-image pixels;
140
+ - an effective focal length along the actual measured width axis.
141
+
142
+ The DrawDown application then applies the existing GreenLens-style tangent/cylinder correction rather than treating the trunk as a flat plane.
143
+
144
+ ## V0.1 boundary method
145
+
146
+ This first field-test version intentionally avoids a heavy native OpenCV or neural-network dependency.
147
+
148
+ The inspector centres the tag on the crosshair. The plugin samples the local dense depth surface and walks left/right until the depth leaves the trunk-depth band. It scores boundary confidence from edge depth jumps, patch completeness, and depth MAD.
149
+
150
+ This is suitable for a controlled validation pilot against diameter tape. It is **not yet an audit-grade claim**. The purpose of the pilot is to quantify bias, MAE/RMSE, failure rates, device effects, and performance by DBH class before deciding whether to add RGB segmentation or a multi-view 3-D fit.
151
+
152
+ ## Field validation recommendation
153
+
154
+ For each test tree, retain:
155
+
156
+ - authoritative tape DBH;
157
+ - ARCore QC DBH;
158
+ - disagreement percentage;
159
+ - phone model and ARCore version;
160
+ - observation count and MAD;
161
+ - existing DrawDown inspection photo.
162
+
163
+ Test across tree size classes, bark types, light conditions, species, and several ARCore Depth-capable Android devices.
164
+
165
+ ## ARCore version
166
+
167
+ The Android library is currently pinned to:
168
+
169
+ ```gradle
170
+ implementation 'com.google.ar:core:1.54.0'
171
+ ```
172
+
173
+ Review this pin deliberately rather than silently floating to a newer version, because measurement software should record and validate changes in the sensing stack.
174
+
175
+ ## Privacy / ARCore notice
176
+
177
+ Apps using ARCore need to comply with Google's ARCore user-privacy requirements. See `NOTICE.md` and Google's current ARCore documentation before production distribution.
178
+
179
+ ## License
180
+
181
+ MIT for this plugin source. Google ARCore itself is subject to Google's ARCore terms.
182
+
183
+ ## DrawDown field anchor (v0.1.2)
184
+
185
+ No ArUco marker is required by the native ARCore workflow. The existing RFID/NFC tree tag identifies the 1.3 m DBH plane. For the depth sweep, place the crosshair on exposed bark immediately beside the tag at the same height so the tag itself does not influence the depth boundary. Manual diameter tape remains authoritative.
186
+
187
+
188
+ ## v0.1.4 Raw Depth field fix
189
+
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.
@@ -0,0 +1,42 @@
1
+ ext {
2
+ junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
3
+ androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
4
+ }
5
+
6
+ buildscript {
7
+ repositories {
8
+ google()
9
+ mavenCentral()
10
+ }
11
+ dependencies {
12
+ classpath 'com.android.tools.build:gradle:8.13.0'
13
+ }
14
+ }
15
+
16
+ apply plugin: 'com.android.library'
17
+
18
+ android {
19
+ namespace 'org.drawdown.arcoredepth'
20
+ compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
21
+
22
+ defaultConfig {
23
+ minSdk project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24
24
+ consumerProguardFiles 'consumer-rules.pro'
25
+ }
26
+
27
+ compileOptions {
28
+ sourceCompatibility JavaVersion.VERSION_21
29
+ targetCompatibility JavaVersion.VERSION_21
30
+ }
31
+ }
32
+
33
+ repositories {
34
+ google()
35
+ mavenCentral()
36
+ }
37
+
38
+ dependencies {
39
+ implementation project(':capacitor-android')
40
+ implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
41
+ implementation 'com.google.ar:core:1.54.0'
42
+ }
@@ -0,0 +1 @@
1
+ # No reflection-based plugin classes beyond Capacitor's normal discovery.
@@ -0,0 +1,11 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.CAMERA" />
3
+
4
+ <!-- AR is optional for the overall DrawDown app; the plugin checks at runtime. -->
5
+ <uses-feature android:name="android.hardware.camera.ar" android:required="false" />
6
+ <uses-feature android:name="com.google.ar.core.depth" android:required="false" />
7
+
8
+ <application>
9
+ <meta-data android:name="com.google.ar.core" android:value="optional" />
10
+ </application>
11
+ </manifest>
@@ -0,0 +1,202 @@
1
+ package org.drawdown.arcoredepth;
2
+
3
+ import android.opengl.GLES11Ext;
4
+ import android.opengl.GLES20;
5
+ import android.opengl.GLSurfaceView;
6
+ import android.view.Display;
7
+
8
+ import com.google.ar.core.Coordinates2d;
9
+ import com.google.ar.core.Frame;
10
+ import com.google.ar.core.Session;
11
+
12
+ import java.nio.ByteBuffer;
13
+ import java.nio.ByteOrder;
14
+ import java.nio.FloatBuffer;
15
+
16
+ import javax.microedition.khronos.egl.EGLConfig;
17
+ import javax.microedition.khronos.opengles.GL10;
18
+
19
+ final class ArCameraRenderer implements GLSurfaceView.Renderer {
20
+ interface Callback {
21
+ void onRendererReady(int textureId);
22
+ void onFrame(Frame frame, int viewWidth, int viewHeight);
23
+ int getDisplayRotation();
24
+ }
25
+
26
+ private static final String VERTEX_SHADER =
27
+ "attribute vec4 aPosition;\n" +
28
+ "attribute vec2 aTexCoord;\n" +
29
+ "varying vec2 vTexCoord;\n" +
30
+ "void main() { gl_Position = aPosition; vTexCoord = aTexCoord; }";
31
+
32
+ private static final String FRAGMENT_SHADER =
33
+ "#extension GL_OES_EGL_image_external : require\n" +
34
+ "precision mediump float;\n" +
35
+ "varying vec2 vTexCoord;\n" +
36
+ "uniform samplerExternalOES uTexture;\n" +
37
+ "void main() { gl_FragColor = texture2D(uTexture, vTexCoord); }";
38
+
39
+ private final Session session;
40
+ private final Callback callback;
41
+
42
+ private final FloatBuffer positions;
43
+ private final FloatBuffer inputUv;
44
+ private final FloatBuffer transformedUv;
45
+
46
+ private int textureId = -1;
47
+ private int program = -1;
48
+ private int viewWidth = 1;
49
+ private int viewHeight = 1;
50
+
51
+ ArCameraRenderer(Session session, Callback callback) {
52
+ this.session = session;
53
+ this.callback = callback;
54
+
55
+ positions = directFloatBuffer(new float[] {
56
+ -1f, -1f,
57
+ 1f, -1f,
58
+ -1f, 1f,
59
+ 1f, 1f
60
+ });
61
+ inputUv = directFloatBuffer(new float[] {
62
+ 0f, 1f,
63
+ 1f, 1f,
64
+ 0f, 0f,
65
+ 1f, 0f
66
+ });
67
+ transformedUv = ByteBuffer.allocateDirect(8 * 4)
68
+ .order(ByteOrder.nativeOrder())
69
+ .asFloatBuffer();
70
+ }
71
+
72
+ @Override
73
+ public void onSurfaceCreated(GL10 gl, EGLConfig config) {
74
+ int[] textures = new int[1];
75
+ GLES20.glGenTextures(1, textures, 0);
76
+ textureId = textures[0];
77
+ GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
78
+ GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
79
+ GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
80
+ GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
81
+ GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
82
+
83
+ program = createProgram(VERTEX_SHADER, FRAGMENT_SHADER);
84
+ session.setCameraTextureName(textureId);
85
+ callback.onRendererReady(textureId);
86
+ }
87
+
88
+ @Override
89
+ public void onSurfaceChanged(GL10 gl, int width, int height) {
90
+ viewWidth = Math.max(1, width);
91
+ viewHeight = Math.max(1, height);
92
+ GLES20.glViewport(0, 0, viewWidth, viewHeight);
93
+ session.setDisplayGeometry(callback.getDisplayRotation(), viewWidth, viewHeight);
94
+ }
95
+
96
+ @Override
97
+ public void onDrawFrame(GL10 gl) {
98
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
99
+ try {
100
+ session.setCameraTextureName(textureId);
101
+ session.setDisplayGeometry(callback.getDisplayRotation(), viewWidth, viewHeight);
102
+ Frame frame = session.update();
103
+ updateUv(frame);
104
+ drawCamera();
105
+ callback.onFrame(frame, viewWidth, viewHeight);
106
+ } catch (Exception ignored) {
107
+ // Session lifecycle transitions can briefly make update unavailable.
108
+ }
109
+ }
110
+
111
+ private void updateUv(Frame frame) {
112
+ try {
113
+ inputUv.position(0);
114
+ transformedUv.position(0);
115
+ frame.transformCoordinates2d(
116
+ Coordinates2d.VIEW_NORMALIZED,
117
+ inputUv,
118
+ Coordinates2d.TEXTURE_NORMALIZED,
119
+ transformedUv);
120
+ transformedUv.position(0);
121
+ } catch (Exception e) {
122
+ transformedUv.position(0);
123
+ transformedUv.put(new float[] {
124
+ 0f, 1f,
125
+ 1f, 1f,
126
+ 0f, 0f,
127
+ 1f, 0f
128
+ });
129
+ transformedUv.position(0);
130
+ }
131
+ }
132
+
133
+ private void drawCamera() {
134
+ if (program <= 0 || textureId <= 0) {
135
+ return;
136
+ }
137
+ GLES20.glDisable(GLES20.GL_DEPTH_TEST);
138
+ GLES20.glUseProgram(program);
139
+
140
+ int posLocation = GLES20.glGetAttribLocation(program, "aPosition");
141
+ int uvLocation = GLES20.glGetAttribLocation(program, "aTexCoord");
142
+ int textureLocation = GLES20.glGetUniformLocation(program, "uTexture");
143
+
144
+ positions.position(0);
145
+ transformedUv.position(0);
146
+ GLES20.glEnableVertexAttribArray(posLocation);
147
+ GLES20.glVertexAttribPointer(posLocation, 2, GLES20.GL_FLOAT, false, 0, positions);
148
+ GLES20.glEnableVertexAttribArray(uvLocation);
149
+ GLES20.glVertexAttribPointer(uvLocation, 2, GLES20.GL_FLOAT, false, 0, transformedUv);
150
+
151
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
152
+ GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
153
+ GLES20.glUniform1i(textureLocation, 0);
154
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
155
+
156
+ GLES20.glDisableVertexAttribArray(posLocation);
157
+ GLES20.glDisableVertexAttribArray(uvLocation);
158
+ }
159
+
160
+ private static FloatBuffer directFloatBuffer(float[] values) {
161
+ FloatBuffer buffer = ByteBuffer.allocateDirect(values.length * 4)
162
+ .order(ByteOrder.nativeOrder())
163
+ .asFloatBuffer();
164
+ buffer.put(values);
165
+ buffer.position(0);
166
+ return buffer;
167
+ }
168
+
169
+ private static int createProgram(String vertexSource, String fragmentSource) {
170
+ int vertex = compileShader(GLES20.GL_VERTEX_SHADER, vertexSource);
171
+ int fragment = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
172
+ if (vertex == 0 || fragment == 0) {
173
+ return 0;
174
+ }
175
+ int program = GLES20.glCreateProgram();
176
+ GLES20.glAttachShader(program, vertex);
177
+ GLES20.glAttachShader(program, fragment);
178
+ GLES20.glLinkProgram(program);
179
+ int[] linked = new int[1];
180
+ GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linked, 0);
181
+ GLES20.glDeleteShader(vertex);
182
+ GLES20.glDeleteShader(fragment);
183
+ if (linked[0] == 0) {
184
+ GLES20.glDeleteProgram(program);
185
+ return 0;
186
+ }
187
+ return program;
188
+ }
189
+
190
+ private static int compileShader(int type, String source) {
191
+ int shader = GLES20.glCreateShader(type);
192
+ GLES20.glShaderSource(shader, source);
193
+ GLES20.glCompileShader(shader);
194
+ int[] compiled = new int[1];
195
+ GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
196
+ if (compiled[0] == 0) {
197
+ GLES20.glDeleteShader(shader);
198
+ return 0;
199
+ }
200
+ return shader;
201
+ }
202
+ }
@@ -0,0 +1,46 @@
1
+ package org.drawdown.arcoredepth;
2
+
3
+ import com.getcapacitor.JSObject;
4
+
5
+ final class DepthObservation {
6
+ final double frontSurfaceDepthM;
7
+ final double effectiveFocalLengthPx;
8
+ final double trunkWidthPx;
9
+ final String rangeSource;
10
+ final double trackingConfidence;
11
+ final String boundaryConfidence;
12
+ final double depthMadM;
13
+ final long timestampMs;
14
+
15
+ DepthObservation(
16
+ double frontSurfaceDepthM,
17
+ double effectiveFocalLengthPx,
18
+ double trunkWidthPx,
19
+ String rangeSource,
20
+ double trackingConfidence,
21
+ String boundaryConfidence,
22
+ double depthMadM,
23
+ long timestampMs) {
24
+ this.frontSurfaceDepthM = frontSurfaceDepthM;
25
+ this.effectiveFocalLengthPx = effectiveFocalLengthPx;
26
+ this.trunkWidthPx = trunkWidthPx;
27
+ this.rangeSource = rangeSource;
28
+ this.trackingConfidence = trackingConfidence;
29
+ this.boundaryConfidence = boundaryConfidence;
30
+ this.depthMadM = depthMadM;
31
+ this.timestampMs = timestampMs;
32
+ }
33
+
34
+ JSObject toJsObject() {
35
+ JSObject out = new JSObject();
36
+ out.put("frontSurfaceDepthM", frontSurfaceDepthM);
37
+ out.put("focalLengthXPx", effectiveFocalLengthPx);
38
+ out.put("trunkWidthPx", trunkWidthPx);
39
+ out.put("rangeSource", rangeSource);
40
+ out.put("trackingConfidence", trackingConfidence);
41
+ out.put("boundaryConfidence", boundaryConfidence);
42
+ out.put("depthMadM", depthMadM);
43
+ out.put("timestampMs", timestampMs);
44
+ return out;
45
+ }
46
+ }