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); + }} + /> + )} + +