diff --git a/.cspell-wordlist.txt b/.cspell-wordlist.txt
index f701ebd35d..2e3e47e2a5 100644
--- a/.cspell-wordlist.txt
+++ b/.cspell-wordlist.txt
@@ -423,3 +423,4 @@ xcrun
denoiser
imageembeddings
unparseable
+zipdepth
diff --git a/apps/computer-vision/app/_layout.tsx b/apps/computer-vision/app/_layout.tsx
index 2ee132c0fb..db3837fe98 100644
--- a/apps/computer-vision/app/_layout.tsx
+++ b/apps/computer-vision/app/_layout.tsx
@@ -69,6 +69,13 @@ export default function Layout() {
title: 'Keypoint Detection',
}}
/>
+
= {
+ xnnpack: [
+ { label: 'Depth Anything V2 Small (INT8)', family: 'v2', value: DEPTH_MODELS.XNNPACK_INT8 },
+ { label: 'Depth Anything V2 Small (FP32)', family: 'v2', value: DEPTH_MODELS.XNNPACK_FP32 },
+ { label: 'Depth Anything V3 Small (INT8)', family: 'v3', value: DEPTH_V3_MODELS.XNNPACK_INT8 },
+ { label: 'Depth Anything V3 Small (FP32)', family: 'v3', value: DEPTH_V3_MODELS.XNNPACK_FP32 },
+ { label: 'ZipDepth Base (FP32)', family: 'zipdepth', value: ZIPDEPTH_MODELS.XNNPACK_FP32 },
+ ],
+ coreml: [
+ { label: 'Depth Anything V2 Small (FP16)', family: 'v2', value: DEPTH_MODELS.COREML_FP16 },
+ { label: 'Depth Anything V3 Small (FP16)', family: 'v3', value: DEPTH_V3_MODELS.COREML_FP16 },
+ { label: 'ZipDepth Base (FP32)', family: 'zipdepth', value: ZIPDEPTH_MODELS.COREML_FP32 },
+ ],
+ vulkan: [
+ { label: 'Depth Anything V2 Small (FP16)', family: 'v2', value: DEPTH_MODELS.VULKAN_FP16 },
+ { label: 'Depth Anything V3 Small (FP16)', family: 'v3', value: DEPTH_V3_MODELS.VULKAN_FP16 },
+ { label: 'ZipDepth Base (FP16)', family: 'zipdepth', value: ZIPDEPTH_MODELS.VULKAN_FP16 },
+ ],
+};
+
+type DepthPreview = { image: SkImage; min: number; max: number };
+
+// Display conversion belongs to the demo; the library retains the raw float map.
+function createDepthPreview(depth: DepthMap, isDepth: boolean): DepthPreview {
+ // Effects use inverse depth so near surfaces retain contrast across model families.
+ const values = isDepth
+ ? depth.data.map((value) => (Number.isFinite(value) && value > 0 ? 1 / value : NaN))
+ : depth.data;
+ let rawMin = Infinity;
+ let rawMax = -Infinity;
+ let min = Infinity;
+ let max = -Infinity;
+ for (let i = 0; i < values.length; i++) {
+ const raw = depth.data[i]!;
+ if (Number.isFinite(raw)) {
+ rawMin = Math.min(rawMin, raw);
+ rawMax = Math.max(rawMax, raw);
+ }
+ const value = values[i]!;
+ if (!Number.isFinite(value)) continue;
+ min = Math.min(min, value);
+ max = Math.max(max, value);
+ }
+ if (!Number.isFinite(min))
+ throw new Error('The model returned no valid depth values for the preview.');
+ const range = max - min;
+ const rgba = new Uint8Array(depth.width * depth.height * 4);
+ for (let i = 0; i < depth.data.length; i++) {
+ const value = values[i]!;
+ const normalized = range > 0 && Number.isFinite(value) ? (value - min) / range : 0;
+ const gray = Math.round(normalized * 255);
+ rgba[i * 4] = gray;
+ rgba[i * 4 + 1] = gray;
+ rgba[i * 4 + 2] = gray;
+ rgba[i * 4 + 3] = 255;
+ }
+ const image = Skia.Image.MakeImage(
+ {
+ width: depth.width,
+ height: depth.height,
+ colorType: ColorType.RGBA_8888,
+ alphaType: AlphaType.Opaque,
+ },
+ Skia.Data.fromBytes(rgba),
+ depth.width * 4
+ );
+ if (!image) throw new Error('Failed to render the depth preview.');
+ return { image, min: rawMin, max: rawMax };
+}
+
+function DepthEstimationContent() {
+ const insets = useSafeAreaInsets();
+ const [selectedBackend, setSelectedBackend] = useState('xnnpack');
+ const [selectedModel, setSelectedModel] = useState(
+ MODEL_OPTIONS.xnnpack[0]!.value
+ );
+ const [busy, setBusy] = useState(false);
+ const [imageUri, setImageUri] = useState(null);
+ const [preview, setPreview] = useState(null);
+ const [latency, setLatency] = useState(null);
+ const [error, setError] = useState(null);
+ const mounted = useRef(true);
+ useEffect(() => {
+ mounted.current = true;
+ return () => {
+ mounted.current = false;
+ };
+ }, []);
+
+ const selectedOption = MODEL_OPTIONS[selectedBackend].find(
+ (option) => option.value === selectedModel
+ )!;
+ const image = useImage(imageUri, (failure) => setError(failure.message));
+ const estimator = useDepthEstimator(selectedModel);
+
+ const selectModel = (model: DepthEstimatorModel) => {
+ if (busy || model === selectedModel) return;
+ setSelectedModel(model);
+ setPreview(null);
+ setLatency(null);
+ setError(null);
+ };
+
+ const selectBackend = (backend: Backend) => {
+ if (busy || backend === selectedBackend) return;
+ const nextModel =
+ MODEL_OPTIONS[backend].find((option) => option.family === selectedOption.family) ??
+ MODEL_OPTIONS[backend][0]!;
+ setSelectedBackend(backend);
+ selectModel(nextModel.value);
+ };
+
+ const pickImage = async (camera: boolean) => {
+ setBusy(true);
+ setError(null);
+ try {
+ const uri = await getImage(camera);
+ if (uri && mounted.current) {
+ setImageUri(uri);
+ setPreview(null);
+ setLatency(null);
+ }
+ } catch (failure) {
+ if (mounted.current) setError(String(failure));
+ } finally {
+ if (mounted.current) setBusy(false);
+ }
+ };
+
+ const runDepthEstimation = async (sync: boolean) => {
+ if (!image || !estimator.estimateDepth || !estimator.estimateDepthWorklet || busy) return;
+ setBusy(true);
+ setError(null);
+ try {
+ const input = skImageToBuffer(image);
+ const start = Date.now();
+ const depth = sync
+ ? estimator.estimateDepthWorklet(input)
+ : await estimator.estimateDepth(input);
+ const elapsed = Date.now() - start;
+ if (!mounted.current) return;
+ setPreview(createDepthPreview(depth, selectedOption.family === 'v3'));
+ setLatency(elapsed);
+ } catch (failure) {
+ if (mounted.current) setError(String(failure));
+ } finally {
+ if (mounted.current) setBusy(false);
+ }
+ };
+
+ return (
+
+
+ Turn a photo into a moving scene. Estimate depth once, then explore perspective, focus, and
+ the space between subjects.
+
+
+
+
+ {preview && image ? (
+
+ ) : (
+ {
+ if (!busy) pickImage(false);
+ }}
+ />
+ )}
+
+
+
+ runDepthEstimation(false)}
+ disabled={!image || !estimator.isReady || busy}
+ />
+ runDepthEstimation(true)}
+ disabled={!image || !estimator.isReady || busy}
+ variant="accent"
+ />
+
+ {preview && (
+
+ Raw relative depth: {preview.min.toFixed(3)}–{preview.max.toFixed(3)}. Values are not
+ meters.
+
+ )}
+
+
+ );
+}
+
+export default function DepthEstimationScreen() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/computer-vision/app/index.tsx b/apps/computer-vision/app/index.tsx
index 96428df502..958bea2528 100644
--- a/apps/computer-vision/app/index.tsx
+++ b/apps/computer-vision/app/index.tsx
@@ -36,6 +36,9 @@ export default function Home() {
router.navigate('segmentation/')}>
Semantic Segmentation
+ router.navigate('depthEstimation/')}>
+ Depth Estimation
+
router.navigate('instanceSegmentation/')}
diff --git a/apps/computer-vision/components/DepthPhoto.tsx b/apps/computer-vision/components/DepthPhoto.tsx
new file mode 100644
index 0000000000..64131423d9
--- /dev/null
+++ b/apps/computer-vision/components/DepthPhoto.tsx
@@ -0,0 +1,278 @@
+import React, { useEffect, useState } from 'react';
+import { Pressable, StyleSheet, Text, View, useWindowDimensions } from 'react-native';
+import {
+ Canvas,
+ Circle,
+ FilterMode,
+ ImageShader,
+ Rect,
+ Shader,
+ Skia,
+ type SkImage,
+} from '@shopify/react-native-skia';
+import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
+import {
+ cancelAnimation,
+ Easing,
+ useDerivedValue,
+ useReducedMotion,
+ useSharedValue,
+ withRepeat,
+ withTiming,
+} from 'react-native-reanimated';
+import { theme } from '../theme';
+import { DEPTH_PHOTO_SHADER } from './depthPhotoShader';
+
+const EFFECT = Skia.RuntimeEffect.Make(DEPTH_PHOTO_SHADER);
+const MODES = ['3D Photo', 'Focus', 'Depth', 'Original'] as const;
+const AMOUNTS = [
+ { label: 'Subtle', value: 0.45 },
+ { label: 'Medium', value: 0.85 },
+ { label: 'Strong', value: 1.3 },
+];
+
+// Applies interactive depth effects to an existing photograph without rerunning inference.
+export function DepthPhoto({ image, depthTexture }: { image: SkImage; depthTexture: SkImage }) {
+ const { width: windowWidth } = useWindowDimensions();
+ const reducedMotion = useReducedMotion();
+ const [mode, setMode] = useState(0);
+ const [amount, setAmount] = useState(0.85);
+ const [playing, setPlaying] = useState(!reducedMotion);
+ const phase = useSharedValue(0);
+ const interaction = useSharedValue(0);
+ const tiltX = useSharedValue(0);
+ const tiltY = useSharedValue(0);
+ const focusX = useSharedValue(0.5);
+ const focusY = useSharedValue(0.5);
+
+ const width = Math.max(1, windowWidth - 32);
+ const height = Math.min(
+ width * 1.25,
+ Math.max(width * 0.7, (width * image.height()) / image.width())
+ );
+ const scale = Math.min(width / image.width(), height / image.height());
+ const imageWidth = image.width() * scale;
+ const imageHeight = image.height() * scale;
+ const x = (width - imageWidth) / 2;
+ const y = (height - imageHeight) / 2;
+ const imageRect = { x, y, width: imageWidth, height: imageHeight };
+
+ useEffect(() => {
+ if (playing && mode === 0) {
+ phase.value = withRepeat(
+ withTiming(phase.value + Math.PI * 2, {
+ duration: 12000,
+ easing: Easing.linear,
+ }),
+ -1,
+ false
+ );
+ } else {
+ cancelAnimation(phase);
+ }
+ return () => cancelAnimation(phase);
+ }, [playing, mode, phase]);
+
+ const uniforms = useDerivedValue(() => {
+ const autoX = Math.sin(phase.value) * 0.7;
+ const autoY = Math.cos(phase.value) * 0.35;
+ return {
+ origin: [x, y],
+ size: [imageWidth, imageHeight],
+ tilt: [
+ autoX * (1 - interaction.value) + tiltX.value * interaction.value,
+ autoY * (1 - interaction.value) + tiltY.value * interaction.value,
+ ],
+ focus: [focusX.value, focusY.value],
+ amount,
+ mode,
+ };
+ });
+ const focusCx = useDerivedValue(() => x + focusX.value * imageWidth);
+ const focusCy = useDerivedValue(() => y + focusY.value * imageHeight);
+
+ const pan = Gesture.Pan()
+ .enabled(mode === 0)
+ .activeOffsetX([-4, 4])
+ .failOffsetY([-20, 20])
+ .onStart((event) => {
+ interaction.value = withTiming(1, { duration: 100 });
+ tiltX.value = Math.max(-1, Math.min(1, ((event.x - x) / imageWidth) * 2 - 1));
+ tiltY.value = Math.max(-1, Math.min(1, ((event.y - y) / imageHeight) * 2 - 1));
+ })
+ .onUpdate((event) => {
+ tiltX.value = Math.max(-1, Math.min(1, ((event.x - x) / imageWidth) * 2 - 1));
+ tiltY.value = Math.max(-1, Math.min(1, ((event.y - y) / imageHeight) * 2 - 1));
+ })
+ .onFinalize(() => {
+ interaction.value = withTiming(0, { duration: 500 });
+ });
+ const tap = Gesture.Tap()
+ .enabled(mode === 1)
+ .onEnd((event, success) => {
+ if (
+ !success ||
+ event.x < x ||
+ event.x > x + imageWidth ||
+ event.y < y ||
+ event.y > y + imageHeight
+ )
+ return;
+ focusX.value = (event.x - x) / imageWidth;
+ focusY.value = (event.y - y) / imageHeight;
+ });
+
+ if (!EFFECT) {
+ return Depth effects are unavailable on this renderer.;
+ }
+
+ return (
+
+
+ DEPTH PLAYGROUND
+
+ {mode === 0
+ ? 'A little perspective.'
+ : mode === 1
+ ? 'Find your focus.'
+ : mode === 2
+ ? 'Beneath the surface.'
+ : 'The starting point.'}
+
+
+
+ {MODES.map((label, index) => (
+ setMode(index)}
+ style={[styles.tab, mode === index && styles.activeTab]}
+ accessibilityRole="button"
+ accessibilityState={{ selected: mode === index }}
+ >
+ {label}
+
+ ))}
+
+
+
+
+
+
+
+ {mode === 0
+ ? 'Drag sideways to shift your perspective. Nearer surfaces move more.'
+ : mode === 1
+ ? 'Tap a subject to keep its depth in focus and soften the rest.'
+ : mode === 2
+ ? 'Cool colors are farther away. Warm colors are closer.'
+ : 'Your original photo, without any depth effects.'}
+
+ {mode < 2 && (
+
+
+ {AMOUNTS.map((item) => (
+ setAmount(item.value)}
+ accessibilityRole="button"
+ accessibilityState={{ selected: amount === item.value }}
+ style={[styles.amount, amount === item.value && styles.selectedAmount]}
+ >
+ {item.label}
+
+ ))}
+
+ {mode === 0 && (
+ setPlaying(!playing)}
+ accessibilityRole="button"
+ style={styles.playButton}
+ >
+ {playing ? 'Pause' : 'Animate'}
+
+ )}
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { width: '100%', marginBottom: 20 },
+ heading: { marginBottom: 14 },
+ eyebrow: {
+ color: theme.colors.primary,
+ fontSize: 10,
+ fontWeight: '700',
+ letterSpacing: 2,
+ marginBottom: 5,
+ },
+ title: {
+ fontSize: 25,
+ color: theme.colors.strongPrimary,
+ fontWeight: '700',
+ letterSpacing: -0.7,
+ },
+ tabs: {
+ flexDirection: 'row',
+ padding: 4,
+ borderRadius: 12,
+ backgroundColor: '#E6E9EF',
+ marginBottom: 12,
+ },
+ tab: { flex: 1, paddingVertical: 10, alignItems: 'center', borderRadius: 9 },
+ activeTab: { backgroundColor: theme.colors.strongPrimary },
+ tabText: { color: '#596579', fontWeight: '600', fontSize: 12 },
+ activeTabText: { color: '#FFFFFF' },
+ frame: { borderRadius: 16, overflow: 'hidden', backgroundColor: '#080F22' },
+ hint: {
+ color: theme.colors.textMuted,
+ fontSize: 12,
+ lineHeight: 18,
+ marginTop: 10,
+ marginBottom: 12,
+ },
+ controls: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 8 },
+ amounts: { flexDirection: 'row', gap: 4 },
+ amount: { paddingHorizontal: 10, paddingVertical: 9, borderRadius: 8 },
+ selectedAmount: { backgroundColor: '#DDE6FA' },
+ amountText: { fontSize: 12, fontWeight: '600', color: theme.colors.primary },
+ playButton: {
+ paddingHorizontal: 14,
+ paddingVertical: 9,
+ borderRadius: 8,
+ backgroundColor: theme.colors.strongPrimary,
+ },
+ playText: { color: '#FFFFFF', fontSize: 12, fontWeight: '600' },
+});
diff --git a/apps/computer-vision/components/depthPhotoShader.ts b/apps/computer-vision/components/depthPhotoShader.ts
new file mode 100644
index 0000000000..35bc7c7c43
--- /dev/null
+++ b/apps/computer-vision/components/depthPhotoShader.ts
@@ -0,0 +1,64 @@
+/** Two image textures: the original photograph and its normalized relative depth. */
+export const DEPTH_PHOTO_SHADER = `
+uniform shader photo;
+uniform shader depth;
+uniform float2 origin;
+uniform float2 size;
+uniform float2 tilt;
+uniform float2 focus;
+uniform float amount;
+uniform float mode;
+
+float depthAt(float2 uv) {
+ return depth.eval(origin + clamp(uv, 0.0, 1.0) * size).r;
+}
+
+half4 photoAt(float2 uv) {
+ return photo.eval(origin + clamp(uv, 0.0, 1.0) * size);
+}
+
+half4 main(float2 position) {
+ float2 uv = (position - origin) / size;
+
+ if (mode > 2.5) {
+ return photoAt(uv);
+ }
+
+ if (mode > 1.5) {
+ float d = depthAt(uv);
+ half3 farColor = half3(0.035, 0.07, 0.22);
+ half3 middleColor = half3(0.06, 0.76, 0.80);
+ half3 nearColor = half3(1.0, 0.83, 0.40);
+ half3 color = mix(farColor, middleColor, smoothstep(0.0, 0.6, d));
+ color = mix(color, nearColor, smoothstep(0.5, 1.0, d));
+ return half4(color, 1.0);
+ }
+
+ if (mode > 0.5) {
+ float difference = abs(depthAt(uv) - depthAt(focus));
+ float radius = smoothstep(0.02, 0.55, difference) * amount * 18.0;
+ half4 color = half4(0.0);
+ float totalWeight = 0.0;
+ // A weighted spiral avoids the distinct repeated edges of a sparse blur ring.
+ for (int i = 0; i < 32; i++) {
+ float r = sqrt((float(i) + 0.5) / 32.0);
+ float angle = float(i) * 2.399963;
+ float weight = exp(-2.0 * r * r);
+ float2 offset = float2(cos(angle), sin(angle)) * r * radius / size;
+ color += photoAt(uv + offset) * weight;
+ totalWeight += weight;
+ }
+ return color / totalWeight;
+ }
+
+ // Slight overscan hides exposed edges. Iterate the inverse depth warp so
+ // the depth lookup follows the displaced source pixel rather than the screen.
+ float2 base = (uv - 0.5) / (1.0 + amount * 0.10) + 0.5;
+ float2 ray = tilt * amount * 0.045;
+ float2 displaced = base;
+ for (int i = 0; i < 4; i++) {
+ displaced = mix(displaced, base + ray * (depthAt(displaced) - 0.35), 0.75);
+ }
+ return photoAt(displaced);
+}
+`;
diff --git a/apps/computer-vision/package.json b/apps/computer-vision/package.json
index 7e69c03519..31dd656d8d 100644
--- a/apps/computer-vision/package.json
+++ b/apps/computer-vision/package.json
@@ -5,6 +5,7 @@
"react-native-executorch": {
"features": [
"classification",
+ "depthEstimation",
"imageEmbeddings",
"textEmbeddings",
"instanceSegmentation",
diff --git a/docs/docs/02-extensions/computer-vision/12-depth-estimation.md b/docs/docs/02-extensions/computer-vision/12-depth-estimation.md
new file mode 100644
index 0000000000..5c306c3df3
--- /dev/null
+++ b/docs/docs/02-extensions/computer-vision/12-depth-estimation.md
@@ -0,0 +1,180 @@
+---
+title: Depth Estimation
+slug: /extensions/depth-estimation
+description: 'Estimate pixel-level relative depth from a single image for portrait effects, scene understanding, and photo animation.'
+keywords:
+ [
+ react native,
+ depth estimation,
+ depth anything,
+ depth map,
+ portrait effects,
+ mobile ml,
+ on-device ai,
+ ]
+---
+
+# Depth Estimation
+
+Depth estimation predicts a depth value for every pixel in an image. The pipeline returns a floating-point depth map matching the original image dimensions, which can be used for portrait blur, photo animation, and scene understanding.
+
+## Quick Start
+
+The [`useDepthEstimator`](../../06-api-reference/functions/useDepthEstimator.md) hook manages model downloading, initialization, and lifecycle:
+
+```tsx
+import { models, useDepthEstimator } from 'react-native-executorch';
+import type { ImageBuffer } from 'react-native-executorch/cv';
+
+function MyComponent() {
+ const estimator = useDepthEstimator(models.depthEstimation.DEPTH_ANYTHING_V2_SMALL.DEFAULT);
+
+ // Hook state:
+ // estimator.isReady — true once model is downloaded and loaded in memory
+ // estimator.downloadProgress — 0 to 100 download progress
+ // estimator.error — Error instance if download or load failed
+ // estimator.resource — resolved config with all URLs replaced by local file paths
+
+ const handleEstimate = async (imageBuffer: ImageBuffer) => {
+ if (!estimator.isReady || !estimator.estimateDepth) return;
+
+ // Run inference on a background thread
+ const depth = await estimator.estimateDepth(imageBuffer);
+ const x = Math.floor(depth.width / 2);
+ const y = Math.floor(depth.height / 2);
+ console.log('Center depth:', depth.data[y * depth.width + x]);
+ };
+
+ // Trigger handleEstimate from an image picker, button press, or camera frame
+}
+```
+
+:::tip Full Interactive Example
+See [`apps/computer-vision/app/depthEstimation/index.tsx`](https://github.com/software-mansion/react-native-executorch/blob/main/apps/computer-vision/app/depthEstimation/index.tsx) for a complete screen with model selection, photo capture, depth visualization, interactive photo effects, and latency tracking.
+:::
+
+## Output Format
+
+[`estimateDepth()`](../../06-api-reference/type-aliases/DepthEstimator.md#estimatedepth) returns a [`DepthMap`](../../06-api-reference/type-aliases/DepthMap.md) object:
+
+```typescript
+type DepthMap = {
+ /** Raw depth values in row-major order: data[y * width + x] */
+ readonly data: Float32Array;
+ /** Width of the depth map in pixels */
+ readonly width: number;
+ /** Height of the depth map in pixels */
+ readonly height: number;
+};
+```
+
+The map contains one float per source image pixel. Each result owns its buffer and remains valid after subsequent inference or pipeline disposal.
+
+Values retain the model's original scale. For visualization, normalize a copy of the data and apply a color map in your application.
+
+## Configuration & Options
+
+The [`DepthEstimatorOptions`](../../06-api-reference/type-aliases/DepthEstimatorOptions.md) in `modelOpts` configure input preprocessing and output resizing. Hosted presets include these settings:
+
+| Option | Description |
+| :----------------- | :-------------------------------------------------------------------------------------------- |
+| `resizeMode` | Resize the full image to the model input dimensions. Currently only `'stretch'` is supported. |
+| `interpolation` | Interpolation used to resize the input image. |
+| `normalizeOpts` | Input channel scaling and normalization. |
+| `outInterpolation` | Interpolation used to resize the float depth map to the source image dimensions. |
+
+ZipDepth applies ImageNet normalization inside the model, so its preset only scales input pixel values to `[0, 1]`.
+
+There are no per-inference options. To change preprocessing for a different model, supply a custom model configuration.
+
+## Imperative API
+
+For background processing or manual lifecycle management outside React components, create the estimator using [`createDepthEstimator`](../../06-api-reference/functions/createDepthEstimator.md):
+
+```typescript
+import { createDepthEstimator, download, models } from 'react-native-executorch';
+
+// Download and cache model assets before creating the pipeline
+const model = await download(models.depthEstimation.DEPTH_ANYTHING_V2_SMALL.DEFAULT);
+const estimator = await createDepthEstimator(model);
+
+try {
+ const depth = await estimator.estimateDepth(imageBuffer);
+ console.log('Depth map dimensions:', depth.width, depth.height);
+} finally {
+ // Always release native resources when finished
+ estimator.dispose();
+}
+```
+
+## Synchronous Execution
+
+For worklet-based processing, [`createDepthEstimator`](../../06-api-reference/functions/createDepthEstimator.md) exposes a synchronous [`estimateDepthWorklet`](../../06-api-reference/type-aliases/DepthEstimator.md#estimatedepthworklet) function:
+
+```typescript
+// Called synchronously on the worklet thread
+const depth = estimator.estimateDepthWorklet(frameBuffer);
+```
+
+See [Worklets & Threading](../../03-core-and-advanced/06-worklets-and-threading.md) for details on worklet execution contexts.
+
+## Available Models
+
+Ready-to-use models are available in [`models.depthEstimation`](../../06-api-reference/variables/models.md#depthestimation):
+
+| Model Family | Variants | Size Range | Supported Backends | Notes |
+| :-------------------------- | :--------------------------------------------------------------------------------------- | :----------------- | :----------------------------------------------- | :-------------------------------------------------------------------------- |
+| **Depth Anything V2 Small** | [See](../../06-api-reference/variables/models.md#depthestimationdepth_anything_v2_small) | 35.5 MB – 99.0 MB | XNNPACK (CPU), Core ML (Apple), Vulkan (Android) | ViT-S/14 backbone with a 518 × 518 input and relative inverse depth output. |
+| **Depth Anything 3 Small** | [See](../../06-api-reference/variables/models.md#depthestimationdepth_anything_v3_small) | 40.7 MB – 106.5 MB | XNNPACK (CPU), Core ML (Apple), Vulkan (Android) | Single-image relative depth with a 518 × 518 input. |
+| **ZipDepth Base** | [See](../../06-api-reference/variables/models.md#depthestimationzipdepth_base) | 12.8 MB – 24.9 MB | XNNPACK (CPU), Core ML (Apple), Vulkan (Android) | Fast preset with a 384 × 384 input and relative inverse depth output. |
+
+Depth Anything V2 and ZipDepth return relative inverse depth (larger means nearer), while Depth Anything 3 returns relative depth (larger means farther). All presets return raw float values through the same `DepthMap` API.
+
+:::tip Using Custom Models
+Pass a [`DepthEstimatorModel`](../../06-api-reference/type-aliases/DepthEstimatorModel.md) configuration to [`useDepthEstimator`](../../06-api-reference/functions/useDepthEstimator.md) or download it before calling [`createDepthEstimator`](../../06-api-reference/functions/createDepthEstimator.md):
+
+```typescript
+import { createDepthEstimator, download, IMAGENET_NORM } from 'react-native-executorch';
+
+const model = await download({
+ modelPath: 'https://example.com/my-depth-model.pte',
+ modelOpts: {
+ resizeMode: 'stretch' as const,
+ interpolation: 'cubic' as const,
+ normalizeOpts: IMAGENET_NORM,
+ outInterpolation: 'linear' as const,
+ },
+});
+const estimator = await createDepthEstimator(model);
+```
+
+Choose normalization and interpolation settings for your model. The pipeline accepts static float32 RGB inputs `[1, 3, H, W]` with outputs `[1, OH, OW]`, or unbatched inputs `[3, H, W]` with outputs `[OH, OW]`. Input and output spatial dimensions may differ. Output values retain the custom model's meaning and units.
+
+The pipeline verifies the exported input and output shapes automatically. See [Exporting Custom Models](../../03-core-and-advanced/07-exporting-custom-models.md#using-a-built-in-pipeline) for export requirements.
+:::
+
+## API Reference
+
+### Hooks & Pipelines
+
+- [`useDepthEstimator()`](../../06-api-reference/functions/useDepthEstimator.md) — React hook for model downloading, state, and lifecycle.
+- [`createDepthEstimator()`](../../06-api-reference/functions/createDepthEstimator.md) — Imperative factory for depth estimation pipelines.
+
+### Types & Options
+
+- [`DepthEstimator`](../../06-api-reference/type-aliases/DepthEstimator.md) — Runner interface with `estimateDepth` and `estimateDepthWorklet`.
+- [`DepthMap`](../../06-api-reference/type-aliases/DepthMap.md) — Floating-point depth data with width and height.
+- [`DepthEstimatorModel`](../../06-api-reference/type-aliases/DepthEstimatorModel.md) — Model configuration spec.
+- [`DepthEstimatorOptions`](../../06-api-reference/type-aliases/DepthEstimatorOptions.md) — Input preprocessing and output interpolation options.
+- [`ImageBuffer`](../../06-api-reference/react-native-executorch/namespaces/cv/type-aliases/ImageBuffer.md) — Input image buffer structure.
+
+### Model Presets & Constants
+
+- [`models.depthEstimation`](../../06-api-reference/variables/models.md#depthestimation) — Pre-configured depth estimation models registry.
+- [`IMAGENET_NORM`](../../06-api-reference/variables/IMAGENET_NORM.md) — Input normalization used by Depth Anything V2 and V3 presets.
+
+:::info Source Code
+View the implementation on GitHub:
+
+- [`src/extensions/cv/tasks/depthEstimation.ts` ↗](https://github.com/software-mansion/react-native-executorch/blob/main/packages/react-native-executorch/src/extensions/cv/tasks/depthEstimation.ts)
+ :::
diff --git a/docs/docs/03-core-and-advanced/08-native-libraries.md b/docs/docs/03-core-and-advanced/08-native-libraries.md
index 0dd37bba93..258e0e38a3 100644
--- a/docs/docs/03-core-and-advanced/08-native-libraries.md
+++ b/docs/docs/03-core-and-advanced/08-native-libraries.md
@@ -93,6 +93,7 @@ Specifying a task under `features` is shorthand: it automatically expands to the
| `textEmbeddings` | xnnpack, coreml, mlx, vulkan | — |
| `imageEmbeddings` | xnnpack, coreml, mlx, vulkan | opencv |
| `classification` | xnnpack, coreml | opencv |
+| `depthEstimation` | xnnpack, coreml, vulkan | opencv |
| `objectDetection` | xnnpack, coreml, vulkan | opencv |
| `keypointDetection` | xnnpack, coreml, mlx, vulkan | opencv |
| `semanticSegmentation` | xnnpack, coreml | opencv |
diff --git a/packages/react-native-executorch/__tests__/api/__snapshots__/apiSurface.test.ts.snap b/packages/react-native-executorch/__tests__/api/__snapshots__/apiSurface.test.ts.snap
index 99804c6b0f..a6c6fa9291 100644
--- a/packages/react-native-executorch/__tests__/api/__snapshots__/apiSurface.test.ts.snap
+++ b/packages/react-native-executorch/__tests__/api/__snapshots__/apiSurface.test.ts.snap
@@ -4,6 +4,7 @@ exports[`public API surface exports one hook per user-facing task 1`] = `
[
"useClassifier",
"useColbertEmbedder",
+ "useDepthEstimator",
"useImageEmbedder",
"useInstanceSegmenter",
"useKeypointDetector",
@@ -133,6 +134,7 @@ exports[`public API surface matches the recorded export list 1`] = `
"WHISPER_SAMPLE_RATE_HZ",
"createClassifier",
"createColbertEmbedder",
+ "createDepthEstimator",
"createFsmnVoiceActivityDetector",
"createImageEmbedder",
"createInstanceSegmenter",
@@ -169,6 +171,7 @@ exports[`public API surface matches the recorded export list 1`] = `
"tensor",
"useClassifier",
"useColbertEmbedder",
+ "useDepthEstimator",
"useImageEmbedder",
"useInstanceSegmenter",
"useKeypointDetector",
diff --git a/packages/react-native-executorch/__tests__/api/modelVariants.test.ts b/packages/react-native-executorch/__tests__/api/modelVariants.test.ts
index ef303485c6..0279433e71 100644
--- a/packages/react-native-executorch/__tests__/api/modelVariants.test.ts
+++ b/packages/react-native-executorch/__tests__/api/modelVariants.test.ts
@@ -336,6 +336,7 @@ describe('feature map', () => {
// added without an entry here fails the coverage case below.
const FEATURE_OF_CATEGORY: Record = {
classification: 'classification',
+ depthEstimation: 'depthEstimation',
styleTransfer: 'styleTransfer',
semanticSegmentation: 'semanticSegmentation',
objectDetection: 'objectDetection',
diff --git a/packages/react-native-executorch/__tests__/tasks/constructionFailure.test.ts b/packages/react-native-executorch/__tests__/tasks/constructionFailure.test.ts
index d2edbf4358..f4520e350a 100644
--- a/packages/react-native-executorch/__tests__/tasks/constructionFailure.test.ts
+++ b/packages/react-native-executorch/__tests__/tasks/constructionFailure.test.ts
@@ -28,6 +28,7 @@ import { createPaddleOcr } from '../../src/extensions/cv/tasks/paddleOcr';
import { createSdxsTextToImage } from '../../src/extensions/cv/tasks/sdxsTextToImage';
import { createSemanticSegmenter } from '../../src/extensions/cv/tasks/semanticSegmentation';
import { createStyleTransfer } from '../../src/extensions/cv/tasks/styleTransfer';
+import { createDepthEstimator } from '../../src/extensions/cv/tasks/depthEstimation';
import { createPrivacyFilter } from '../../src/extensions/nlp/tasks/privacyFilter';
import { createTextEmbedder } from '../../src/extensions/nlp/tasks/textEmbedding';
import { createColbertEmbedder } from '../../src/extensions/nlp/tasks/colbertEmbedding';
@@ -86,6 +87,7 @@ const factories: Record Promise> = {
objectDetector: () => createObjectDetector(cv),
semanticSegmenter: () => createSemanticSegmenter(cv),
styleTransfer: () => createStyleTransfer(cv),
+ depthEstimator: () => createDepthEstimator(cv),
imageEmbedder: () => createImageEmbedder(cv),
instanceSegmenter: () => createInstanceSegmenter(cv),
keypointDetector: () => createKeypointDetector(cv),
diff --git a/packages/react-native-executorch/__tests__/tasks/depthEstimation.test.ts b/packages/react-native-executorch/__tests__/tasks/depthEstimation.test.ts
new file mode 100644
index 0000000000..ccb7609f5d
--- /dev/null
+++ b/packages/react-native-executorch/__tests__/tasks/depthEstimation.test.ts
@@ -0,0 +1,187 @@
+import { IMAGENET_NORM } from '../../src/constants';
+import { f32, i32, method } from '../../src/core/schema';
+import { createDepthEstimator } from '../../src/extensions/cv/tasks/depthEstimation';
+import { fakeJsi } from '../support/fakeJsi';
+import { tracked } from '../support/lifetime';
+import { exported, imageBuffer, writesOutputs } from '../support/fixtures';
+import type { FakeTensor } from '../support/fakeTensor';
+
+const MODEL_PATH = '/models/depth.pte';
+const config = {
+ modelPath: MODEL_PATH,
+ modelOpts: {
+ resizeMode: 'stretch',
+ interpolation: 'cubic',
+ normalizeOpts: IMAGENET_NORM,
+ outInterpolation: 'linear',
+ },
+} as const;
+const schema = exported(method('forward', [f32(1, 3, 2, 3)], [f32(1, 2, 3)]));
+
+describe('createDepthEstimator', () => {
+ beforeEach(() => {
+ fakeJsi.registerModel(MODEL_PATH, {
+ schema,
+ execute: writesOutputs([-1.25, 0.5, 2.75, 4, 10.125, 512.5]),
+ });
+ });
+
+ it('preserves float values and row-major ordering without normalizing or clipping', async () => {
+ const estimator = tracked(await createDepthEstimator(config));
+ const result = await estimator.estimateDepth(imageBuffer(3, 2));
+
+ expect(result).toEqual({
+ width: 3,
+ height: 2,
+ data: Float32Array.from([-1.25, 0.5, 2.75, 4, 10.125, 512.5]),
+ });
+ });
+
+ it('restores a constant depth field to portrait and landscape source dimensions', async () => {
+ fakeJsi.registerModel(MODEL_PATH, { schema, execute: writesOutputs(Array(6).fill(12.25)) });
+ const estimator = tracked(await createDepthEstimator(config));
+ for (const [width, height] of [
+ [8, 4],
+ [4, 8],
+ ]) {
+ const result = await estimator.estimateDepth(imageBuffer(width!, height!));
+ expect(result.width).toBe(width);
+ expect(result.height).toBe(height);
+ expect([...result.data]).toEqual(Array(width! * height!).fill(12.25));
+ }
+ });
+
+ it('converts RGBA to RGB channels-first with ImageNet normalization', async () => {
+ fakeJsi.registerModel(MODEL_PATH, {
+ schema,
+ execute: (_method, inputs) => {
+ const input = inputs[0] as FakeTensor;
+ const means = [0.485, 0.456, 0.406];
+ const deviations = [0.229, 0.224, 0.225];
+ const pixels = [255, 128, 0];
+ for (let channel = 0; channel < 3; channel++) {
+ for (let pixel = 0; pixel < 6; pixel++) {
+ expect(input.getElement(channel * 6 + pixel)).toBeCloseTo(
+ (pixels[channel]! / 255 - means[channel]!) / deviations[channel]!,
+ 5
+ );
+ }
+ }
+ },
+ });
+ const estimator = tracked(await createDepthEstimator(config));
+ await estimator.estimateDepth({
+ data: Uint8Array.from(Array(6).fill([255, 128, 0, 64]).flat()),
+ width: 3,
+ height: 2,
+ format: 'rgba',
+ layout: 'hwc',
+ });
+ });
+
+ it('owns each result buffer across subsequent calls and disposal', async () => {
+ let call = 0;
+ fakeJsi.registerModel(MODEL_PATH, {
+ schema,
+ execute: (_method, _inputs, outputs) => {
+ outputs[0]!.setElement(0, ++call);
+ },
+ });
+ const estimator = tracked(await createDepthEstimator(config));
+ const first = await estimator.estimateDepth(imageBuffer(3, 2));
+ const second = estimator.estimateDepthWorklet(imageBuffer(3, 2));
+ expect(first.data).not.toBe(second.data);
+ second.data[0] = 99;
+ estimator.dispose();
+ expect(first.data[0]).toBe(1);
+ });
+
+ it('agrees between asynchronous and synchronous entry points', async () => {
+ const estimator = tracked(await createDepthEstimator(config));
+ const input = imageBuffer(3, 2);
+ expect(await estimator.estimateDepth(input)).toEqual(estimator.estimateDepthWorklet(input));
+ });
+
+ it.each([
+ { input: [1, 3, 4, 6], output: [1, 2, 3] },
+ { input: [3, 4, 6], output: [2, 3] },
+ ])('accepts input $input and lower-resolution output $output', async ({ input, output }) => {
+ fakeJsi.registerModel(MODEL_PATH, {
+ schema: exported(method('forward', [f32(...input)], [f32(...output)])),
+ execute: writesOutputs(Array(6).fill(7.25)),
+ });
+ const estimator = tracked(await createDepthEstimator(config));
+ const result = await estimator.estimateDepth(imageBuffer(12, 8));
+ expect(result.width).toBe(12);
+ expect(result.height).toBe(8);
+ expect([...result.data]).toEqual(Array(96).fill(7.25));
+ });
+
+ it('uses model-specific normalization instead of assuming ImageNet inputs', async () => {
+ fakeJsi.registerModel(MODEL_PATH, {
+ schema,
+ execute: (_method, inputs) => {
+ const input = inputs[0] as FakeTensor;
+ expect(input.getElement(0)).toBeCloseTo(255 / 127.5 - 1);
+ expect(input.getElement(6)).toBeCloseTo(128 / 127.5 - 1);
+ expect(input.getElement(12)).toBeCloseTo(-1);
+ },
+ });
+ const estimator = tracked(
+ await createDepthEstimator({
+ ...config,
+ modelOpts: {
+ resizeMode: 'stretch',
+ interpolation: 'nearest',
+ normalizeOpts: { alpha: 1 / 127.5, beta: -1 },
+ outInterpolation: 'nearest',
+ },
+ })
+ );
+ await estimator.estimateDepth({
+ data: Uint8Array.from(Array(6).fill([255, 128, 0]).flat()),
+ width: 3,
+ height: 2,
+ format: 'rgb',
+ layout: 'hwc',
+ });
+ });
+
+ it.each([[f32(1, 1, 2, 3)], [f32(1, 2, 2, 3)], [i32(1, 2, 3)], [f32(2, 2, 3)]])(
+ 'rejects incompatible depth output %j and releases the model',
+ async (output) => {
+ fakeJsi.registerModel(MODEL_PATH, {
+ schema: exported(method('forward', [f32(1, 3, 2, 3)], [output])),
+ });
+ await expect(createDepthEstimator(config)).rejects.toMatchObject({ code: 'SCHEMA_MISMATCH' });
+ expect(fakeJsi.liveModels()).toEqual([]);
+ expect(fakeJsi.liveTensors()).toBe(0);
+ }
+ );
+
+ it('releases per-call tensors and all resources on repeated disposal', async () => {
+ const estimator = tracked(await createDepthEstimator(config));
+ const initialTensors = fakeJsi.liveTensors();
+ for (const size of [4, 8, 16]) {
+ await estimator.estimateDepth(imageBuffer(size, size));
+ expect(fakeJsi.liveTensors()).toBe(initialTensors);
+ }
+ estimator.dispose();
+ estimator.dispose();
+ expect(fakeJsi.liveTensors()).toBe(0);
+ expect(fakeJsi.liveModels()).toEqual([]);
+ });
+
+ it('releases transient preprocessing tensors when inference fails', async () => {
+ fakeJsi.registerModel(MODEL_PATH, {
+ schema,
+ execute: () => {
+ throw new Error('Inference failed');
+ },
+ });
+ const estimator = tracked(await createDepthEstimator(config));
+ const initialTensors = fakeJsi.liveTensors();
+ await expect(estimator.estimateDepth(imageBuffer(8, 4))).rejects.toThrow('Inference failed');
+ expect(fakeJsi.liveTensors()).toBe(initialTensors);
+ });
+});
diff --git a/packages/react-native-executorch/scripts/download-libs.js b/packages/react-native-executorch/scripts/download-libs.js
index 45732133b8..0b0b215f04 100644
--- a/packages/react-native-executorch/scripts/download-libs.js
+++ b/packages/react-native-executorch/scripts/download-libs.js
@@ -44,7 +44,7 @@
* libs: opencv, phonemis
* features: llm, multimodalLLM, speechToText, textToSpeech, vad, privacyFilter,
* textEmbeddings, imageEmbeddings,
- * classification, objectDetection, semanticSegmentation, instanceSegmentation,
+ * classification, objectDetection, depthEstimation, semanticSegmentation, instanceSegmentation,
* ocr, verticalOCR, keypointDetection, styleTransfer, textToImage, segmentAnything,
* tokenizer
*
@@ -164,6 +164,8 @@ const FEATURE_MAP = {
backends: ['xnnpack', 'coreml', 'mlx', 'vulkan'],
libs: ['opencv'],
},
+ // Depth Anything V2, V3 and ZipDepth ship xnnpack, coreml and vulkan.
+ depthEstimation: { backends: ['xnnpack', 'coreml', 'vulkan'], libs: ['opencv'] },
// DeepLab/FCN/LR-ASPP/selfie all ship xnnpack + coreml.
semanticSegmentation: { backends: ['xnnpack', 'coreml'], libs: ['opencv'] },
// YOLO-seg xnnpack-only, rf_detr-seg/fastsam add coreml, and all three ship
diff --git a/packages/react-native-executorch/src/extensions/cv/tasks/depthEstimation.ts b/packages/react-native-executorch/src/extensions/cv/tasks/depthEstimation.ts
new file mode 100644
index 0000000000..02c2099655
--- /dev/null
+++ b/packages/react-native-executorch/src/extensions/cv/tasks/depthEstimation.ts
@@ -0,0 +1,174 @@
+/**
+ * Depth estimation task pipeline with image preprocessing and floating-point
+ * depth map resizing.
+ */
+import type { WorkletRuntime } from 'react-native-worklets';
+
+import { createResourceScope } from '../../../core/lifetime';
+import { loadModel } from '../../../core/model';
+import { wrapAsync } from '../../../core/runtime';
+import { validateSpec, method, f32 } from '../../../core/schema';
+import { tensor } from '../../../core/tensor';
+import type { ImageBuffer } from '../image';
+import { resize, type InterpolationMethod } from '../ops/image';
+import { createImagePreprocessor, type ImagePreprocessorOptions } from '../utils/imagePreprocessor';
+
+/**
+ * Result structure representing the output of a depth estimation task.
+ * @category CV / Types
+ */
+export type DepthMap = {
+ /**
+ * Raw model values, indexed as `data[y * width + x]`. Each result owns its
+ * buffer. Values are not normalized or converted to display pixels.
+ * Interpretation and units depend on the loaded model.
+ */
+ readonly data: Float32Array;
+ /** Width of the depth map in pixels. */
+ readonly width: number;
+ /** Height of the depth map in pixels. */
+ readonly height: number;
+};
+
+/**
+ * Options for configuring a depth estimator preprocessor and output
+ * interpolation.
+ * @category CV / Types
+ */
+export type DepthEstimatorOptions = Omit & {
+ /** Resize mode for input images. Must be `'stretch'`. */
+ readonly resizeMode: 'stretch';
+ /** Interpolation method used when resizing depth maps back to input image dimensions. */
+ readonly outInterpolation: InterpolationMethod;
+};
+
+/**
+ * Model configuration required to instantiate a depth estimator task runner.
+ * @category CV / Types
+ */
+export type DepthEstimatorModel = {
+ /** Local path or remote URL of the `.pte` model file. */
+ readonly modelPath: string;
+ /**
+ * Input preprocessing and output depth map interpolation. `resizeMode` is
+ * fixed to `'stretch'`. See {@link DepthEstimatorOptions}.
+ */
+ readonly modelOpts: DepthEstimatorOptions;
+};
+
+/**
+ * Depth estimation task runner.
+ * @category CV / Types
+ */
+export type DepthEstimator = {
+ /**
+ * Releases all allocated native resources.
+ */
+ readonly dispose: () => void;
+
+ /**
+ * Asynchronously performs depth estimation on the input image.
+ *
+ * The full input image is stretched to the model's input size, and the
+ * floating-point depth map is resized back to the input image dimensions.
+ * Depth values retain the model's original scale.
+ * @param input The input image buffer.
+ * @returns A promise resolving to the depth map at the input image resolution.
+ * @throws {RnExecuTorchError} With code `RESOURCE_BUSY` if the model is in
+ * use, or `RESOURCE_DISPOSED` if disposed.
+ */
+ readonly estimateDepth: (input: ImageBuffer) => Promise;
+
+ /**
+ * Synchronous version of {@link estimateDepth} to be executed directly on the
+ * caller or worklet thread.
+ */
+ readonly estimateDepthWorklet: (input: ImageBuffer) => DepthMap;
+};
+
+/**
+ * Creates a depth estimator runner for executing local depth estimation models.
+ *
+ * It validates the model inputs and outputs requirements, pre-allocates the
+ * necessary static execution tensors, sets up an image preprocessor, and
+ * registers clean disposal hooks to clear all native memory.
+ *
+ * Accepts batched or unbatched CHW RGB inputs and matching depth outputs.
+ * Output spatial dimensions may differ from the input tensor. Preprocessing
+ * and output interpolation follow `modelOpts`; depth values retain the model's
+ * original scale.
+ * @category CV / Tasks
+ * @param config Depth estimator task configuration containing path and options.
+ * See {@link DepthEstimatorModel}.
+ * @param runtime Optional worklet runtime thread on which to run the model
+ * execution.
+ * @returns A promise resolving to the instantiated {@link DepthEstimator} runner.
+ * @throws {RnExecuTorchError} With code `LOAD_FAILED` if model fails to load,
+ * or `SCHEMA_MISMATCH` if model schema does not match depth estimation spec.
+ */
+export async function createDepthEstimator(
+ config: DepthEstimatorModel,
+ runtime?: WorkletRuntime
+): Promise {
+ const scope = createResourceScope();
+ const dispose = scope.dispose;
+
+ try {
+ const { modelPath, modelOpts } = config;
+ const model = scope.track(await wrapAsync(loadModel, runtime)(modelPath));
+
+ const { variant, dims } = validateSpec(model.schema, {
+ batched: method(
+ 'forward', // prettier-ignore
+ [f32(1, 3, 'H', 'W')],
+ [f32(1, 'OH', 'OW')]
+ ),
+ unbatched: method(
+ 'forward', // prettier-ignore
+ [f32(3, 'H', 'W')],
+ [f32('OH', 'OW')]
+ ),
+ });
+
+ const [H, W, OH, OW] = dims.constant('H', 'W', 'OH', 'OW');
+ const inpShape = { batched: [1, 3, H, W], unbatched: [3, H, W] }[variant];
+ const outShape = { batched: [1, OH, OW], unbatched: [OH, OW] }[variant];
+
+ const tensors = [
+ scope.track(tensor('float32', outShape)),
+ scope.track(tensor('float32', [OH, OW, 1])),
+ ] as const;
+
+ const [tOutput, tDepth] = tensors;
+ const preprocessor = scope.track(createImagePreprocessor(modelOpts, inpShape));
+
+ const estimateDepthWorklet = (input: ImageBuffer): DepthMap => {
+ 'worklet';
+ const tInput = preprocessor.process(input);
+ model.execute('forward', [tInput], [tOutput]);
+
+ const tResize = tensor('float32', [input.height, input.width, 1]);
+ try {
+ const data = tOutput
+ .copyTo(tDepth)
+ .through(resize, tResize, { mode: 'stretch', interpolation: modelOpts.outInterpolation })
+ .getData(new Float32Array(input.width * input.height));
+
+ return { data, width: input.width, height: input.height };
+ } finally {
+ tResize.dispose();
+ }
+ };
+
+ const estimateDepth = wrapAsync(estimateDepthWorklet, runtime);
+
+ return {
+ dispose,
+ estimateDepth,
+ estimateDepthWorklet,
+ };
+ } catch (error) {
+ dispose();
+ throw error;
+ }
+}
diff --git a/packages/react-native-executorch/src/hooks/useDepthEstimator.ts b/packages/react-native-executorch/src/hooks/useDepthEstimator.ts
new file mode 100644
index 0000000000..53cac0963d
--- /dev/null
+++ b/packages/react-native-executorch/src/hooks/useDepthEstimator.ts
@@ -0,0 +1,37 @@
+import { useModel } from './useModel';
+import { useResourceDownload, type ResourceOptions } from './useResourceDownload';
+import {
+ createDepthEstimator,
+ type DepthEstimatorModel,
+} from '../extensions/cv/tasks/depthEstimation';
+
+/**
+ * React hook to load and run a depth estimation model.
+ *
+ * This hook manages downloading (if remote URLs are provided) and loading the
+ * model assets, compiling them, tracking download progress and load errors, and
+ * releasing native memory when the component unmounts or the configuration
+ * changes.
+ *
+ * For imperative usage, see {@link createDepthEstimator}.
+ * @category Hooks
+ * @param config The depth estimation model configuration.
+ * See {@link DepthEstimatorModel}.
+ * @param options Load and caching options. See {@link ResourceOptions}.
+ * @returns The same object as {@link DepthEstimator} (without `dispose`),
+ * combined with loading state and download progress.
+ * @see {@link DepthEstimator}
+ */
+export function useDepthEstimator(config: DepthEstimatorModel, options?: ResourceOptions) {
+ const { resource, downloadProgress, downloadError } = useResourceDownload(config, options);
+ const { model, error } = useModel(createDepthEstimator, resource);
+
+ return {
+ isReady: !!model,
+ error: downloadError || error,
+ downloadProgress,
+ resource,
+ estimateDepth: model?.estimateDepth,
+ estimateDepthWorklet: model?.estimateDepthWorklet,
+ };
+}
diff --git a/packages/react-native-executorch/src/index.ts b/packages/react-native-executorch/src/index.ts
index b9fff6b01a..9a1040aada 100644
--- a/packages/react-native-executorch/src/index.ts
+++ b/packages/react-native-executorch/src/index.ts
@@ -46,6 +46,7 @@
// Hooks — primary API for app developers
export * from './hooks/useClassifier';
export * from './hooks/useStyleTransfer';
+export * from './hooks/useDepthEstimator';
export * from './hooks/useSemanticSegmenter';
export * from './hooks/useInstanceSegmenter';
export * from './hooks/useKeypointDetector';
@@ -74,6 +75,7 @@ export * from './constants';
// Task APIs — for developers needing manual lifetime/disposal control
export * from './extensions/cv/tasks/classification';
export * from './extensions/cv/tasks/styleTransfer';
+export * from './extensions/cv/tasks/depthEstimation';
export * from './extensions/cv/tasks/semanticSegmentation';
export * from './extensions/cv/tasks/instanceSegmentation';
export * from './extensions/cv/tasks/keypointDetection';
diff --git a/packages/react-native-executorch/src/models.ts b/packages/react-native-executorch/src/models.ts
index de37ec051d..242a94614a 100644
--- a/packages/react-native-executorch/src/models.ts
+++ b/packages/react-native-executorch/src/models.ts
@@ -22,6 +22,7 @@ import type { StyleTransferModel } from './extensions/cv/tasks/styleTransfer';
import type { SemanticSegmenterModel } from './extensions/cv/tasks/semanticSegmentation';
import type { KeypointDetectorModel } from './extensions/cv/tasks/keypointDetection';
import type { InstanceSegmenterModel } from './extensions/cv/tasks/instanceSegmentation';
+import type { DepthEstimatorModel } from './extensions/cv/tasks/depthEstimation';
import type { ImageEmbedderModel } from './extensions/cv/tasks/imageEmbedding';
import type { SdxsTextToImageModel } from './extensions/cv/tasks/sdxsTextToImage';
import type { TextEmbedderModel } from './extensions/nlp/tasks/textEmbedding';
@@ -278,6 +279,70 @@ const STYLE_TRANSFER_UDNIE_VULKAN_FP16: StyleTransferModel = {
modelOpts: STYLE_TRANSFER_OPTS,
};
+// =============================================================================
+// Depth Estimation
+// =============================================================================
+
+const DEPTH_ANYTHING_OPTS = {
+ resizeMode: 'stretch' as const,
+ interpolation: 'cubic' as const,
+ normalizeOpts: IMAGENET_NORM,
+ outInterpolation: 'linear' as const,
+};
+const DEPTH_ANYTHING_V2_SMALL_XNNPACK_INT8: DepthEstimatorModel = {
+ modelPath: `${BASE_URL}-depth-anything-v2/${NEXT_VERSION_TAG}/small/xnnpack/depth_anything_v2_small_xnnpack_int8.pte`,
+ modelOpts: DEPTH_ANYTHING_OPTS,
+};
+const DEPTH_ANYTHING_V2_SMALL_XNNPACK_FP32: DepthEstimatorModel = {
+ modelPath: `${BASE_URL}-depth-anything-v2/${NEXT_VERSION_TAG}/small/xnnpack/depth_anything_v2_small_xnnpack_fp32.pte`,
+ modelOpts: DEPTH_ANYTHING_OPTS,
+};
+const DEPTH_ANYTHING_V2_SMALL_COREML_FP16: DepthEstimatorModel = {
+ modelPath: `${BASE_URL}-depth-anything-v2/${NEXT_VERSION_TAG}/small/coreml/depth_anything_v2_small_coreml_fp16.pte`,
+ modelOpts: DEPTH_ANYTHING_OPTS,
+};
+const DEPTH_ANYTHING_V2_SMALL_VULKAN_FP16: DepthEstimatorModel = {
+ modelPath: `${BASE_URL}-depth-anything-v2/${NEXT_VERSION_TAG}/small/vulkan/depth_anything_v2_small_vulkan_fp16.pte`,
+ modelOpts: DEPTH_ANYTHING_OPTS,
+};
+
+const DEPTH_ANYTHING_V3_SMALL_XNNPACK_INT8: DepthEstimatorModel = {
+ modelPath: `${BASE_URL}-depth-anything-v3/${NEXT_VERSION_TAG}/small/xnnpack/depth_anything_v3_small_xnnpack_int8.pte`,
+ modelOpts: DEPTH_ANYTHING_OPTS,
+};
+const DEPTH_ANYTHING_V3_SMALL_XNNPACK_FP32: DepthEstimatorModel = {
+ modelPath: `${BASE_URL}-depth-anything-v3/${NEXT_VERSION_TAG}/small/xnnpack/depth_anything_v3_small_xnnpack_fp32.pte`,
+ modelOpts: DEPTH_ANYTHING_OPTS,
+};
+const DEPTH_ANYTHING_V3_SMALL_COREML_FP16: DepthEstimatorModel = {
+ modelPath: `${BASE_URL}-depth-anything-v3/${NEXT_VERSION_TAG}/small/coreml/depth_anything_v3_small_coreml_fp16.pte`,
+ modelOpts: DEPTH_ANYTHING_OPTS,
+};
+const DEPTH_ANYTHING_V3_SMALL_VULKAN_FP16: DepthEstimatorModel = {
+ modelPath: `${BASE_URL}-depth-anything-v3/${NEXT_VERSION_TAG}/small/vulkan/depth_anything_v3_small_vulkan_fp16.pte`,
+ modelOpts: DEPTH_ANYTHING_OPTS,
+};
+
+const ZIPDEPTH_OPTS = {
+ resizeMode: 'stretch' as const,
+ interpolation: 'linear' as const,
+ // ZipDepth applies ImageNet normalization inside the model.
+ normalizeOpts: { alpha: 1 / 255, beta: 0 },
+ outInterpolation: 'linear' as const,
+};
+const ZIPDEPTH_BASE_XNNPACK_FP32: DepthEstimatorModel = {
+ modelPath: `${BASE_URL}-zipdepth/${NEXT_VERSION_TAG}/base/xnnpack/zipdepth_base_384_xnnpack_fp32.pte`,
+ modelOpts: ZIPDEPTH_OPTS,
+};
+const ZIPDEPTH_BASE_COREML_FP32: DepthEstimatorModel = {
+ modelPath: `${BASE_URL}-zipdepth/${NEXT_VERSION_TAG}/base/coreml/zipdepth_base_384_coreml_fp32.pte`,
+ modelOpts: ZIPDEPTH_OPTS,
+};
+const ZIPDEPTH_BASE_VULKAN_FP16: DepthEstimatorModel = {
+ modelPath: `${BASE_URL}-zipdepth/${NEXT_VERSION_TAG}/base/vulkan/zipdepth_base_384_vulkan_fp16.pte`,
+ modelOpts: ZIPDEPTH_OPTS,
+};
+
// =============================================================================
// Semantic Segmentation
// =============================================================================
@@ -2372,6 +2437,41 @@ export const models = {
}),
},
+ /**
+ * Depth estimation models that predict a floating-point depth value per pixel.
+ */
+ depthEstimation: {
+ /**
+ * Depth Anything V2 Small with a ViT-S/14 backbone. Predicts relative inverse
+ * depth from a single image; larger values indicate closer regions.
+ */
+ DEPTH_ANYTHING_V2_SMALL: variants({
+ XNNPACK_INT8: DEPTH_ANYTHING_V2_SMALL_XNNPACK_INT8,
+ XNNPACK_FP32: DEPTH_ANYTHING_V2_SMALL_XNNPACK_FP32,
+ COREML_FP16: DEPTH_ANYTHING_V2_SMALL_COREML_FP16,
+ VULKAN_FP16: DEPTH_ANYTHING_V2_SMALL_VULKAN_FP16,
+ }),
+ /**
+ * Depth Anything 3 Small, exported for single-image relative depth.
+ * Larger values indicate farther regions.
+ */
+ DEPTH_ANYTHING_V3_SMALL: variants({
+ XNNPACK_INT8: DEPTH_ANYTHING_V3_SMALL_XNNPACK_INT8,
+ XNNPACK_FP32: DEPTH_ANYTHING_V3_SMALL_XNNPACK_FP32,
+ COREML_FP16: DEPTH_ANYTHING_V3_SMALL_COREML_FP16,
+ VULKAN_FP16: DEPTH_ANYTHING_V3_SMALL_VULKAN_FP16,
+ }),
+ /**
+ * ZipDepth Base NPU with a 384 × 384 input for low-latency depth estimation.
+ * Predicts relative inverse depth; larger values indicate closer regions.
+ */
+ ZIPDEPTH_BASE: variants({
+ XNNPACK_FP32: ZIPDEPTH_BASE_XNNPACK_FP32,
+ COREML_FP32: ZIPDEPTH_BASE_COREML_FP32,
+ VULKAN_FP16: ZIPDEPTH_BASE_VULKAN_FP16,
+ }),
+ },
+
/**
* Semantic segmentation models that classify each pixel into target object or
* background classes.