From bf3cb55715d4b4ff417ef1e7977d37398de28bc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 07:03:40 +0000 Subject: [PATCH 1/5] Initial plan From 2518052c71b9d8631a7bc393494e3c415dd4a5a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 07:32:31 +0000 Subject: [PATCH 2/5] Add MLX model, weight converter, Swift wrapper, and verification script Co-authored-by: atultw <30200282+atultw@users.noreply.github.com> --- DepthAnything3MLX/Package.swift | 33 + .../DepthAnything3MLX/DepthAnything3.swift | 1016 +++++++++++++++++ convert_to_mlx.py | 250 ++++ mlx_depth_anything_3/__init__.py | 9 + mlx_depth_anything_3/backbone.py | 378 ++++++ mlx_depth_anything_3/camera.py | 108 ++ mlx_depth_anything_3/dpt.py | 333 ++++++ mlx_depth_anything_3/inference.py | 168 +++ mlx_depth_anything_3/layers.py | 465 ++++++++ mlx_depth_anything_3/model.py | 137 +++ mlx_depth_anything_3/reference_view.py | 138 +++ mlx_depth_anything_3/transform.py | 183 +++ verify_parity.py | 217 ++++ 13 files changed, 3435 insertions(+) create mode 100644 DepthAnything3MLX/Package.swift create mode 100644 DepthAnything3MLX/Sources/DepthAnything3MLX/DepthAnything3.swift create mode 100644 convert_to_mlx.py create mode 100644 mlx_depth_anything_3/__init__.py create mode 100644 mlx_depth_anything_3/backbone.py create mode 100644 mlx_depth_anything_3/camera.py create mode 100644 mlx_depth_anything_3/dpt.py create mode 100644 mlx_depth_anything_3/inference.py create mode 100644 mlx_depth_anything_3/layers.py create mode 100644 mlx_depth_anything_3/model.py create mode 100644 mlx_depth_anything_3/reference_view.py create mode 100644 mlx_depth_anything_3/transform.py create mode 100644 verify_parity.py diff --git a/DepthAnything3MLX/Package.swift b/DepthAnything3MLX/Package.swift new file mode 100644 index 00000000..55454f5e --- /dev/null +++ b/DepthAnything3MLX/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version: 5.9 +// Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +// Licensed under the Apache License, Version 2.0 + +import PackageDescription + +let package = Package( + name: "DepthAnything3MLX", + platforms: [ + .macOS(.v14), + .iOS(.v17), + ], + products: [ + .library( + name: "DepthAnything3MLX", + targets: ["DepthAnything3MLX"] + ), + ], + dependencies: [ + .package(url: "https://github.com/ml-explore/mlx-swift.git", from: "0.21.0"), + ], + targets: [ + .target( + name: "DepthAnything3MLX", + dependencies: [ + .product(name: "MLX", package: "mlx-swift"), + .product(name: "MLXNN", package: "mlx-swift"), + .product(name: "MLXFast", package: "mlx-swift"), + ], + path: "Sources/DepthAnything3MLX" + ), + ] +) diff --git a/DepthAnything3MLX/Sources/DepthAnything3MLX/DepthAnything3.swift b/DepthAnything3MLX/Sources/DepthAnything3MLX/DepthAnything3.swift new file mode 100644 index 00000000..a9b1489b --- /dev/null +++ b/DepthAnything3MLX/Sources/DepthAnything3MLX/DepthAnything3.swift @@ -0,0 +1,1016 @@ +// Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +// Licensed under the Apache License, Version 2.0 + +// Depth Anything 3 – MLX-Swift Wrapper +// +// Complete inference wrapper supporting: +// - Multiple input images (dynamic batch of views) +// - Intrinsics/extrinsics conditioning +// - Dynamic spatial resolution (any multiple of 14) +// - Single forward pass → predicted depth per image +// +// Architecture: +// DinoV2 backbone (ViT) → DualDPT depth head → depth maps +// Optional CameraEnc for extrinsics/intrinsics conditioning + +import Foundation +import MLX +import MLXFast +import MLXNN + +// MARK: - Configuration + +/// Model configuration matching the Python YAML configs. +public struct DA3Config { + public let backboneName: String + public let embedDim: Int + public let depth: Int + public let numHeads: Int + public let outLayers: [Int] + public let altStart: Int + public let qknormStart: Int + public let ropeStart: Int + public let catToken: Bool + public let ffnLayer: String + public let headDimIn: Int + public let headOutputDim: Int + public let headFeatures: Int + public let headOutChannels: [Int] + public let camEncDimOut: Int + public let camDecDimIn: Int + public let patchSize: Int + + public static let da3Small = DA3Config( + backboneName: "vits", embedDim: 384, depth: 12, numHeads: 6, + outLayers: [5, 7, 9, 11], altStart: 4, qknormStart: 4, ropeStart: 4, + catToken: true, ffnLayer: "mlp", + headDimIn: 768, headOutputDim: 2, headFeatures: 64, + headOutChannels: [48, 96, 192, 384], + camEncDimOut: 384, camDecDimIn: 768, patchSize: 14 + ) + + public static let da3Base = DA3Config( + backboneName: "vitb", embedDim: 768, depth: 12, numHeads: 12, + outLayers: [5, 7, 9, 11], altStart: 4, qknormStart: 4, ropeStart: 4, + catToken: true, ffnLayer: "mlp", + headDimIn: 1536, headOutputDim: 2, headFeatures: 128, + headOutChannels: [96, 192, 384, 768], + camEncDimOut: 768, camDecDimIn: 1536, patchSize: 14 + ) + + public static let da3Large = DA3Config( + backboneName: "vitl", embedDim: 1024, depth: 24, numHeads: 16, + outLayers: [11, 15, 19, 23], altStart: 4, qknormStart: 4, ropeStart: 4, + catToken: true, ffnLayer: "mlp", + headDimIn: 2048, headOutputDim: 2, headFeatures: 256, + headOutChannels: [256, 512, 1024, 1024], + camEncDimOut: 1024, camDecDimIn: 2048, patchSize: 14 + ) + + public static let da3Giant = DA3Config( + backboneName: "vitg", embedDim: 1536, depth: 40, numHeads: 24, + outLayers: [19, 25, 31, 39], altStart: 4, qknormStart: 4, ropeStart: 4, + catToken: true, ffnLayer: "swiglu", + headDimIn: 3072, headOutputDim: 2, headFeatures: 384, + headOutChannels: [384, 768, 1536, 1536], + camEncDimOut: 1536, camDecDimIn: 3072, patchSize: 14 + ) +} + +// MARK: - Utility Functions + +/// ImageNet normalization constants. +private let imageNetMean: [Float] = [0.485, 0.456, 0.406] +private let imageNetStd: [Float] = [0.229, 0.224, 0.225] + +/// Bilinear interpolation for NHWC tensors. +func bilinearInterpolate(_ x: MLXArray, targetH: Int, targetW: Int, alignCorners: Bool = true) + -> MLXArray +{ + let shape = x.shape + let (n, h, w, c) = (shape[0], shape[1], shape[2], shape[3]) + if h == targetH && w == targetW { return x } + + let yCoords: MLXArray + let xCoords: MLXArray + if alignCorners && targetH > 1 && targetW > 1 { + yCoords = MLXArray.linspace(Float(0), Float(h - 1), count: targetH) + xCoords = MLXArray.linspace(Float(0), Float(w - 1), count: targetW) + } else { + yCoords = clip( + (MLXArray(Float32(0) ..< Float32(targetH)) + 0.5) * Float(h) / Float(targetH) - 0.5, + min: 0, + max: Float(h - 1)) + xCoords = clip( + (MLXArray(Float32(0) ..< Float32(targetW)) + 0.5) * Float(w) / Float(targetW) - 0.5, + min: 0, + max: Float(w - 1)) + } + + let y0 = clip(floor(yCoords).asType(.int32), min: 0, max: h - 1) + let y1 = clip(y0 + 1, min: 0, max: h - 1) + let x0 = clip(floor(xCoords).asType(.int32), min: 0, max: w - 1) + let x1 = clip(x0 + 1, min: 0, max: w - 1) + + let wy = expandedDimensions(yCoords - y0.asType(.float32), axes: [0, 2, 3]) + let wx = expandedDimensions(xCoords - x0.asType(.float32), axes: [0, 1, 3]) + + let topLeft = x[0..., y0, x0] + let topRight = x[0..., y0, x1] + let botLeft = x[0..., y1, x0] + let botRight = x[0..., y1, x1] + + return topLeft * (1 - wy) * (1 - wx) + topRight * (1 - wy) * wx + botLeft * wy * (1 - wx) + + botRight * wy * wx +} + +/// Create normalized UV grid for positional embeddings. +func createUVGrid(width: Int, height: Int, aspectRatio: Float? = nil) -> MLXArray { + let ar = aspectRatio ?? Float(width) / Float(height) + let diag = sqrt(ar * ar + 1.0) + let sx = ar / diag + let sy: Float = 1.0 / diag + let lx = -sx * Float(width - 1) / Float(width) + let rx = sx * Float(width - 1) / Float(width) + let ty = -sy * Float(height - 1) / Float(height) + let by = sy * Float(height - 1) / Float(height) + let xs = MLXArray.linspace(lx, rx, count: width) + let ys = MLXArray.linspace(ty, by, count: height) + let xx = broadcastTo(expandedDimensions(xs, axis: 0), shape: [height, width]) + let yy = broadcastTo(expandedDimensions(ys, axis: 1), shape: [height, width]) + return stacked([xx, yy], axis: -1) +} + +/// Sinusoidal positional embedding. +func makeSincosPosEmbed(embedDim: Int, pos: MLXArray, omega0: Float = 100.0) -> MLXArray { + let omega = MLXArray(Float32(0) ..< Float32(embedDim / 2)) / Float(embedDim / 2) + let invFreq = 1.0 / pow(MLXArray(omega0), omega) + let posFlat = pos.reshaped([-1]).asType(.float32) + let outer = expandedDimensions(posFlat, axis: 1) * expandedDimensions(invFreq, axis: 0) + return concatenated([sin(outer), cos(outer)], axis: 1).asType(.float32) +} + +/// Position grid to sinusoidal embed. +func positionGridToEmbed(_ grid: MLXArray, embedDim: Int, omega0: Float = 100.0) -> MLXArray { + let shape = grid.shape + let (h, w) = (shape[0], shape[1]) + let flat = grid.reshaped([-1, 2]) + let embX = makeSincosPosEmbed(embedDim: embedDim / 2, pos: flat[0..., 0], omega0: omega0) + let embY = makeSincosPosEmbed(embedDim: embedDim / 2, pos: flat[0..., 1], omega0: omega0) + return concatenated([embX, embY], axis: -1).reshaped([h, w, embedDim]) +} + +/// Affine inverse of a rigid transformation matrix. +func affineInverse(_ a: MLXArray) -> MLXArray { + let r = a[0..., 0..<3, 0..<3] + let t = a[0..., 0..<3, 3...] + let p = a[0..., 3..., 0...] + let rt = r.transposed(0, 1, 3, 2) + return concatenated([concatenated([rt, -matmul(rt, t)], axis: -1), p], axis: -2) +} + +/// Quaternion (XYZW) to rotation matrix. +func quatToMat(_ q: MLXArray) -> MLXArray { + let i = q[0..., 0] + let j = q[0..., 1] + let k = q[0..., 2] + let r = q[0..., 3] + let twoS = 2.0 / sum(q * q, axis: -1) + let elements = stacked([ + 1 - twoS * (j * j + k * k), + twoS * (i * j - k * r), + twoS * (i * k + j * r), + twoS * (i * j + k * r), + 1 - twoS * (i * i + k * k), + twoS * (j * k - i * r), + twoS * (i * k - j * r), + twoS * (j * k + i * r), + 1 - twoS * (i * i + j * j), + ], axis: -1) + var outShape = Array(q.shape.dropLast()) + outShape.append(contentsOf: [3, 3]) + return elements.reshaped(outShape) +} + +/// Rotation matrix to quaternion (XYZW). +func matToQuat(_ matrix: MLXArray) -> MLXArray { + let batchDim = Array(matrix.shape.dropLast(2)) + let flat = matrix.reshaped(batchDim + [9]) + let m00 = flat[0..., 0] + let m01 = flat[0..., 1] + let m02 = flat[0..., 2] + let m10 = flat[0..., 3] + let m11 = flat[0..., 4] + let m12 = flat[0..., 5] + let m20 = flat[0..., 6] + let m21 = flat[0..., 7] + let m22 = flat[0..., 8] + + let qAbs = sqrt( + maximum( + stacked([ + 1 + m00 + m11 + m22, + 1 + m00 - m11 - m22, + 1 - m00 + m11 - m22, + 1 - m00 - m11 + m22, + ], axis: -1), 0)) + + let quatByRijk = stacked([ + stacked([qAbs[0..., 0] ** 2, m21 - m12, m02 - m20, m10 - m01], axis: -1), + stacked([m21 - m12, qAbs[0..., 1] ** 2, m10 + m01, m02 + m20], axis: -1), + stacked([m02 - m20, m10 + m01, qAbs[0..., 2] ** 2, m12 + m21], axis: -1), + stacked([m10 - m01, m20 + m02, m21 + m12, qAbs[0..., 3] ** 2], axis: -1), + ], axis: -2) + + let denom = 2.0 * maximum(expandedDimensions(qAbs, axis: -1), MLXArray(Float(0.1))) + let candidates = quatByRijk / denom + let bestIdx = argMax(qAbs, axis: -1) + let oneHot = MLX.oneHot(bestIdx, num: 4) + let mask = expandedDimensions(oneHot, axis: -1) + var out = sum(candidates * mask, axis: -2) + // [r, i, j, k] -> [i, j, k, r] + out = concatenated([out[0..., 1..<2], out[0..., 2..<3], out[0..., 3..<4], out[0..., 0..<1]], axis: -1) + // Standardize: make real part non-negative + return MLX.where(out[0..., 3...] .< 0, -out, out) +} + +/// Convert extrinsics+intrinsics to 9D pose encoding. +func extriIntriToPoseEncoding(extrinsics: MLXArray, intrinsics: MLXArray, imageSize: (Int, Int)) + -> MLXArray +{ + let r = extrinsics[0..., 0..., 0..<3, 0..<3] + let t = extrinsics[0..., 0..., 0..<3, 3] + let quat = matToQuat(r) + let (h, w) = imageSize + let fovH = 2 * atan(MLXArray(Float(h) / 2.0) / intrinsics[0..., 0..., 1, 1]) + let fovW = 2 * atan(MLXArray(Float(w) / 2.0) / intrinsics[0..., 0..., 0, 0]) + return concatenated( + [t, quat, expandedDimensions(fovH, axis: -1), expandedDimensions(fovW, axis: -1)], axis: -1 + ).asType(.float32) +} + +// MARK: - Layer Scale + +class LayerScaleLayer: Module { + let gamma: MLXArray + + init(dim: Int, initValues: Float = 1e-5) { + gamma = MLXArray.ones([dim]) * initValues + } + + func callAsFunction(_ x: MLXArray) -> MLXArray { + x * gamma + } +} + +// MARK: - MLP + +class MlpLayer: Module { + let fc1: Linear + let fc2: Linear + + init(inFeatures: Int, hiddenFeatures: Int, outFeatures: Int, bias: Bool = true) { + fc1 = Linear(inFeatures, hiddenFeatures, bias: bias) + fc2 = Linear(hiddenFeatures, outFeatures, bias: bias) + } + + func callAsFunction(_ x: MLXArray) -> MLXArray { + var h = fc1(x) + h = gelu(h) + h = fc2(h) + return h + } +} + +// MARK: - SwiGLU FFN + +class SwiGLUFFNLayer: Module { + let w12: Linear + let w3: Linear + + init(inFeatures: Int, hiddenFeatures: Int, outFeatures: Int, bias: Bool = true) { + let hf = ((hiddenFeatures * 2 / 3) + 7) / 8 * 8 + w12 = Linear(inFeatures, 2 * hf, bias: bias) + w3 = Linear(hf, outFeatures, bias: bias) + } + + func callAsFunction(_ x: MLXArray) -> MLXArray { + let x12 = w12(x) + let half = x12.shape.last! / 2 + let x1 = x12[0..., 0..