Kyle Pearson commited on
Commit
ac6a5ca
·
1 Parent(s): 9d59df9

intermediate seam fix algorithm

Browse files
DAPModel.mlpackage/Data/com.apple.CoreML/model.mlmodel CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:5696ae403e0a1861cc444ab08f34fbb5581c928ea244c2fca2a03fe395e0378e
3
- size 402224
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9c4d8be6cd9706f04b67b096824b2ca80f1ebfc99bc99650ba234122d52741d4
3
+ size 402181
DAPModel.mlpackage/Data/com.apple.CoreML/weights/weight.bin CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:8117c7e3129387f5219184f5e2d3a5c7c946c6f7905d39737a895dc29d6177c9
3
- size 1344013056
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:076cab651f9e9f84120e924c5720f228b9c3d73afa615f03f8471040909a4002
3
+ size 1338508032
DAPModel.mlpackage/Manifest.json CHANGED
@@ -1,18 +1,18 @@
1
  {
2
  "fileFormatVersion": "1.0.0",
3
  "itemInfoEntries": {
4
- "47133C03-7196-4F41-A4BA-71E0277FD33B": {
5
- "author": "com.apple.CoreML",
6
- "description": "CoreML Model Specification",
7
- "name": "model.mlmodel",
8
- "path": "com.apple.CoreML/model.mlmodel"
9
- },
10
- "BDE59133-8A31-4AEB-B879-19F2D41BD3A9": {
11
  "author": "com.apple.CoreML",
12
  "description": "CoreML Model Weights",
13
  "name": "weights",
14
  "path": "com.apple.CoreML/weights"
 
 
 
 
 
 
15
  }
16
  },
17
- "rootModelIdentifier": "47133C03-7196-4F41-A4BA-71E0277FD33B"
18
  }
 
1
  {
2
  "fileFormatVersion": "1.0.0",
3
  "itemInfoEntries": {
4
+ "509E05FC-B781-4EA5-A00E-7F81D58BD1E4": {
 
 
 
 
 
 
5
  "author": "com.apple.CoreML",
6
  "description": "CoreML Model Weights",
7
  "name": "weights",
8
  "path": "com.apple.CoreML/weights"
9
+ },
10
+ "5AA90D09-387D-4A76-B4A4-1520C0177479": {
11
+ "author": "com.apple.CoreML",
12
+ "description": "CoreML Model Specification",
13
+ "name": "model.mlmodel",
14
+ "path": "com.apple.CoreML/model.mlmodel"
15
  }
16
  },
17
+ "rootModelIdentifier": "5AA90D09-387D-4A76-B4A4-1520C0177479"
18
  }
DepthPredictor.swift CHANGED
@@ -18,60 +18,104 @@ import CoreImage
18
  import CoreGraphics
19
  import AppKit
20
 
21
- // MARK: - Colormap LUTs
22
 
23
- /// Precomputed jet colormap lookup table (256 entries)
24
- func jetColormapLUT() -> [(r: UInt8, g: UInt8, b: UInt8)] {
25
- var lut: [(r: UInt8, g: UInt8, b: UInt8)] = []
26
- lut.reserveCapacity(256)
27
- for i in 0...255 {
28
- let t = Float(i) / 255.0
29
- var r: Float, g: Float, b: Float
30
 
 
 
 
 
 
31
  if t < 1.0 / 3.0 {
32
  r = 0; g = 0
33
- b = 0.5 + 0.5 * (t / (1.0 / 3.0))
34
  } else if t < 2.0 / 3.0 {
 
35
  r = 0
36
- g = 0.5 + 0.5 * ((t - 1.0 / 3.0) / (1.0 / 3.0))
37
- b = 1.0 - (t - 1.0 / 3.0) / (1.0 / 3.0) * 0.5
38
  } else {
39
- r = 0.5 + 0.5 * ((t - 2.0 / 3.0) / (1.0 / 3.0))
40
- g = 1.0 - (t - 2.0 / 3.0) / (1.0 / 3.0) * 0.5
 
41
  b = 0
42
  }
43
-
44
- lut.append((
45
  r: UInt8(round(max(0, min(1, r)) * 255)),
46
  g: UInt8(round(max(0, min(1, g)) * 255)),
47
  b: UInt8(round(max(0, min(1, b)) * 255))
48
- ))
49
  }
50
- return lut
51
- }
52
 
53
- /// Turbo colormap (Google's perceptually-uniform alternative to jet)
54
- func turboColormapLUT() -> [(r: UInt8, g: UInt8, b: UInt8)] {
55
- func turboChannel(_ t: Float, _ c: [Float]) -> Float {
56
  let t2 = t * t, t3 = t2 * t, t4 = t3 * t, t5 = t4 * t
57
- return max(0, min(1, c[0] * t5 + c[1] * t4 + c[2] * t3 + c[3] * t2 + c[4] * t + c[5]))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  }
59
 
60
- let rC: [Float] = [-6.3733615, 15.04266179, -13.85162213, 5.08578778, -0.83861766, 0.16457028]
61
- let gC: [Float] = [2.25531523, -11.37426878, 21.82122831, -18.71443039, 6.26060447, -0.68049933]
62
- let bC: [Float] = [-4.13513668, 6.56872416, 4.79961124, -4.01387798, 1.33503302, 0.0088154]
 
 
63
 
64
- var lut: [(r: UInt8, g: UInt8, b: UInt8)] = []
65
- lut.reserveCapacity(256)
66
- for i in 0...255 {
67
- let t = Float(i) / 255.0
68
- lut.append((
69
- r: UInt8(round(turboChannel(t, rC) * 255)),
70
- g: UInt8(round(turboChannel(t, gC) * 255)),
71
- b: UInt8(round(turboChannel(t, bC) * 255))
72
- ))
 
 
 
 
 
 
73
  }
74
- return lut
75
  }
76
 
77
  // MARK: - Depth Result
@@ -84,19 +128,8 @@ struct DepthResult {
84
  var width: Int { multiArray.shape[3].intValue }
85
  var height: Int { multiArray.shape[2].intValue }
86
 
87
- /// Get all depth values as a flat Float32 array (row-major).
88
- func getDepthValues() -> [Float32] {
89
- let h = height, w = width
90
- let planeStride = multiArray.strides[2].intValue
91
- let ptr = multiArray.dataPointer.bindMemory(to: Float32.self, capacity: h * w)
92
- var values = [Float32](repeating: 0, count: h * w)
93
- for row in 0..<h {
94
- let src = ptr.advanced(by: row * planeStride)
95
- let dst = values.withUnsafeMutableBufferPointer { $0.baseAddress!.advanced(by: row * w) }
96
- memcpy(dst, src, w * MemoryLayout<Float32>.stride)
97
- }
98
- return values
99
- }
100
  }
101
 
102
  // MARK: - Depth Predictor
@@ -115,11 +148,33 @@ final class DepthPredictor {
115
  setupModel(modelURL: modelURL, computeUnits: computeUnits)
116
  }
117
 
118
- // MARK: - Inference
119
 
120
  /// Predict depth from a CGImage. Completion receives a ``DepthResult`` with
121
  /// both a renderable CIImage and the raw Float32 depth multi-array.
122
- func predictDepth(from cgImage: CGImage, completion: @escaping (DepthResult?) -> Void) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  guard let visionModel else {
124
  print("[DepthPredictor] Model not loaded")
125
  completion(nil)
@@ -159,16 +214,173 @@ final class DepthPredictor {
159
  }
160
  }
161
 
162
- // MARK: - Colormap
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
  /// Apply a jet colormap to depth values -> 8-bit RGB CIImage.
165
  func applyJetColormap(to depth: DepthResult) -> CIImage? {
166
- applyColormap(to: depth, lut: jetColormapLUT())
167
  }
168
 
169
  /// Apply a turbo colormap to depth values -> 8-bit RGB CIImage.
170
  func applyTurboColormap(to depth: DepthResult) -> CIImage? {
171
- applyColormap(to: depth, lut: turboColormapLUT())
172
  }
173
 
174
  /// Apply a grayscale visualization with optional contrast.
@@ -182,48 +394,41 @@ final class DepthPredictor {
182
  return filter.outputImage ?? ciImage
183
  }
184
 
185
- private func applyColormap(to depth: DepthResult, lut: [(r: UInt8, g: UInt8, b: UInt8)]) -> CIImage? {
186
- let values = depth.getDepthValues()
187
- let width = depth.width
188
- let height = depth.height
189
-
190
- // Find min/max for normalization
191
- var minDepth = Float.greatestFiniteMagnitude
192
- var maxDepth: Float32 = -Float.greatestFiniteMagnitude
193
- for i in 0..<values.count {
194
- let v = values[i]
195
- if v > 0 {
196
- minDepth = min(minDepth, v)
197
- maxDepth = max(maxDepth, v)
198
- }
199
- }
200
  let range = maxDepth - minDepth
201
- let invRange = range > 0 ? 1.0 / range : 1.0
202
 
203
- // Map to RGBA output buffer
204
- let outputBufferSize = width * height * 4
205
  guard let outputBuffer = malloc(outputBufferSize) else { return nil }
206
  defer { free(outputBuffer) }
207
 
208
  let outPtr = outputBuffer.bindMemory(to: UInt8.self, capacity: outputBufferSize)
209
- for i in 0..<values.count {
210
- let normalized = max(0, min(1, (values[i] - minDepth) * invRange))
211
- let index = min(Int(normalized * 255), 255)
212
- let color = lut[index]
213
- outPtr[i * 4 + 0] = color.r
214
- outPtr[i * 4 + 1] = color.g
215
- outPtr[i * 4 + 2] = color.b
216
- outPtr[i * 4 + 3] = 255
 
 
 
 
 
217
  }
218
 
219
- // Create CGImage from RGBA buffer
220
  let colorSpace = CGColorSpaceCreateDeviceRGB()
221
  guard let bitmapContext = CGContext(
222
  data: outPtr,
223
- width: width,
224
- height: height,
225
  bitsPerComponent: 8,
226
- bytesPerRow: width * 4,
227
  space: colorSpace,
228
  bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue
229
  ) else { return nil }
@@ -232,49 +437,44 @@ final class DepthPredictor {
232
  return CIImage(cgImage: cgImage)
233
  }
234
 
235
- // MARK: - Save
236
 
237
  /// Save depth values as a 16-bit grayscale PNG (normalized to [0, 65535]).
 
238
  static func saveDepthAsGrayscale(_ depth: DepthResult, to path: URL) throws {
239
- let values = depth.getDepthValues()
240
- let width = depth.width
241
- let height = depth.height
242
-
243
- // Find min/max
244
- var minDepth = Float.greatestFiniteMagnitude
245
- var maxDepth: Float32 = -Float.greatestFiniteMagnitude
246
- for v in values {
247
- if v > 0 {
248
- minDepth = min(minDepth, v)
249
- maxDepth = max(maxDepth, v)
250
- }
251
- }
252
  let range = maxDepth - minDepth
253
- let invRange = range > 0 ? 1.0 / range : 1.0
254
 
255
  // Create 16-bit grayscale buffer (big-endian)
256
- let bufferSize = width * height * 2
257
  guard let buffer = malloc(bufferSize) else {
258
  throw NSError(domain: "DepthPredictor", code: 7,
259
  userInfo: [NSLocalizedDescriptionKey: "Failed to allocate buffer"])
260
  }
261
  defer { free(buffer) }
262
 
263
- let ptr = buffer.bindMemory(to: UInt8.self, capacity: bufferSize)
264
- for i in 0..<values.count {
265
- let normalized = (values[i] - minDepth) * invRange
266
- let value = UInt16(max(0, min(65535, normalized * 65535)))
267
- ptr[i * 2 + 0] = UInt8(value >> 8)
268
- ptr[i * 2 + 1] = UInt8(value & 0xFF)
 
 
 
 
 
269
  }
270
 
271
  let colorSpace = CGColorSpaceCreateDeviceGray()
272
  guard let bitmapContext = CGContext(
273
- data: ptr,
274
- width: width,
275
- height: height,
276
  bitsPerComponent: 16,
277
- bytesPerRow: width * 2,
278
  space: colorSpace,
279
  bitmapInfo: CGImageAlphaInfo.none.rawValue | CGBitmapInfo.byteOrder16Big.rawValue
280
  ) else {
@@ -303,7 +503,7 @@ final class DepthPredictor {
303
  try pngData.write(to: path)
304
  }
305
 
306
- // MARK: - Private
307
 
308
  private func setupModel(modelURL: URL, computeUnits: MLComputeUnits) {
309
  do {
@@ -386,17 +586,65 @@ final class DepthPredictor {
386
  guard let destination = CVPixelBufferGetBaseAddress(buffer) else { return nil }
387
 
388
  let planeStride = multiArray.strides[2].intValue
 
 
389
  for h in 0..<height {
390
- let srcRow = multiArray.dataPointer.bindMemory(to: Float32.self, capacity: width * height)
391
- .advanced(by: h * planeStride)
392
- let dstRow = destination.advanced(by: h * width * MemoryLayout<Float32>.stride)
393
- memcpy(dstRow, srcRow, width * MemoryLayout<Float32>.stride)
394
  }
395
 
396
  return CIImage(cvPixelBuffer: buffer)
397
  }
398
  }
399
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  // MARK: - Image Loading
401
 
402
  extension DepthPredictor {
@@ -434,6 +682,8 @@ struct CommandLineArgs {
434
  let imagePath: URL
435
  let outputPath: URL
436
  let colormap: String // "grayscale", "jet", "turbo"
 
 
437
 
438
  static func parse() -> CommandLineArgs? {
439
  let args = CommandLine.arguments
@@ -442,6 +692,8 @@ struct CommandLineArgs {
442
  var imagePath: URL?
443
  var outputPath: URL?
444
  var colormap = "grayscale"
 
 
445
 
446
  var i = 1
447
  while i < args.count {
@@ -464,6 +716,16 @@ struct CommandLineArgs {
464
  i += 1
465
  if i < args.count { colormap = args[i].lowercased() }
466
 
 
 
 
 
 
 
 
 
 
 
467
  case "-h", "--help":
468
  printUsage()
469
  return nil
@@ -496,7 +758,9 @@ struct CommandLineArgs {
496
  modelPath: m,
497
  imagePath: image,
498
  outputPath: output,
499
- colormap: colormap
 
 
500
  )
501
  }
502
 
@@ -519,6 +783,10 @@ struct CommandLineArgs {
519
  -c, --colormap STYLE Colormap: grayscale (default), jet, turbo
520
  grayscale = 16-bit depth values
521
  jet/turbo = 8-bit colorized visualization
 
 
 
 
522
  -h, --help Show this help message
523
 
524
  Examples:
@@ -528,8 +796,8 @@ struct CommandLineArgs {
528
  # Colorized with jet colormap
529
  \(execName) -m DAPModel.mlpackage -i panorama.jpg -o depth.png -c jet
530
 
531
- # Turbo colormap
532
- \(execName) -m DAPModel.mlpackage -i panorama.jpg -o depth.png -c turbo
533
 
534
  The model is automatically compiled on first use and cached for subsequent runs.
535
  """)
@@ -559,12 +827,18 @@ func main() {
559
  print(" Image size: \(cgImage.width)x\(cgImage.height)")
560
 
561
  // Run inference (async -> sync via semaphore)
 
 
 
 
 
 
562
  print("Running inference...")
563
  let startTime = CFAbsoluteTimeGetCurrent()
564
 
565
  var depthResult: DepthResult?
566
  let semaphore = DispatchSemaphore(value: 0)
567
- predictor.predictDepth(from: cgImage) { result in
568
  depthResult = result
569
  semaphore.signal()
570
  }
@@ -614,4 +888,4 @@ func main() {
614
  }
615
  }
616
 
617
- main()
 
18
  import CoreGraphics
19
  import AppKit
20
 
21
+ // MARK: - Colormap LUTs (computed once, cached)
22
 
23
+ /// Packed RGB colormap entry — stored contiguously for cache-friendly LUT access.
24
+ struct RGB {
25
+ let r: UInt8
26
+ let g: UInt8
27
+ let b: UInt8
28
+ }
 
29
 
30
+ /// Precomputed jet colormap lookup table (256 entries, built once).
31
+ let jetLUT: [RGB] = {
32
+ (0...255).map { i in
33
+ let t = Float(i) / 255.0
34
+ let r, g, b: Float
35
  if t < 1.0 / 3.0 {
36
  r = 0; g = 0
37
+ b = 0.5 + 0.5 * (t * 3.0)
38
  } else if t < 2.0 / 3.0 {
39
+ let u = (t - 1.0 / 3.0) * 3.0
40
  r = 0
41
+ g = 0.5 + 0.5 * u
42
+ b = 1.0 - u * 0.5
43
  } else {
44
+ let u = (t - 2.0 / 3.0) * 3.0
45
+ r = 0.5 + 0.5 * u
46
+ g = 1.0 - u * 0.5
47
  b = 0
48
  }
49
+ return RGB(
 
50
  r: UInt8(round(max(0, min(1, r)) * 255)),
51
  g: UInt8(round(max(0, min(1, g)) * 255)),
52
  b: UInt8(round(max(0, min(1, b)) * 255))
53
+ )
54
  }
55
+ }()
 
56
 
57
+ /// Turbo colormap (Google's perceptually-uniform alternative to jet, built once).
58
+ let turboLUT: [RGB] = {
59
+ func channel(_ t: Float, _ c: (Float, Float, Float, Float, Float, Float)) -> Float {
60
  let t2 = t * t, t3 = t2 * t, t4 = t3 * t, t5 = t4 * t
61
+ return max(0, min(1, c.0 * t5 + c.1 * t4 + c.2 * t3 + c.3 * t2 + c.4 * t + c.5))
62
+ }
63
+ let rC = (-6.3733615 as Float, 15.04266179 as Float, -13.85162213 as Float,
64
+ 5.08578778 as Float, -0.83861766 as Float, 0.16457028 as Float)
65
+ let gC = ( 2.25531523 as Float, -11.37426878 as Float, 21.82122831 as Float,
66
+ -18.71443039 as Float, 6.26060447 as Float, -0.68049933 as Float)
67
+ let bC = (-4.13513668 as Float, 6.56872416 as Float, 4.79961124 as Float,
68
+ -4.01387798 as Float, 1.33503302 as Float, 0.0088154 as Float)
69
+ return (0...255).map { i in
70
+ let t = Float(i) / 255.0
71
+ return RGB(
72
+ r: UInt8(round(channel(t, rC) * 255)),
73
+ g: UInt8(round(channel(t, gC) * 255)),
74
+ b: UInt8(round(channel(t, bC) * 255))
75
+ )
76
+ }
77
+ }()
78
+
79
+ // MARK: - MLMultiArray Helpers
80
+
81
+ /// Provides direct, strided read access to an MLMultiArray's Float32 data
82
+ /// without copying. The caller must keep the source MLMultiArray alive for
83
+ /// the lifetime of this wrapper.
84
+ struct DepthArrayView {
85
+ let ptr: UnsafeMutablePointer<Float32>
86
+ let width: Int
87
+ let height: Int
88
+ let rowStride: Int // stride between rows in Float32 units
89
+
90
+ init(_ multiArray: MLMultiArray) {
91
+ width = multiArray.shape[3].intValue
92
+ height = multiArray.shape[2].intValue
93
+ rowStride = multiArray.strides[2].intValue
94
+ ptr = multiArray.dataPointer.bindMemory(to: Float32.self, capacity: height * rowStride)
95
  }
96
 
97
+ /// Read a single value at (row, col).
98
+ @inline(__always)
99
+ func value(row: Int, col: Int) -> Float32 {
100
+ ptr[row * rowStride + col]
101
+ }
102
 
103
+ /// Compute min/max across all values (skipping non-positive).
104
+ func minMax() -> (min: Float32, max: Float32) {
105
+ var lo: Float32 = .greatestFiniteMagnitude
106
+ var hi: Float32 = -.greatestFiniteMagnitude
107
+ for row in 0..<height {
108
+ let base = row * rowStride
109
+ for col in 0..<width {
110
+ let v = ptr[base + col]
111
+ if v > 0 {
112
+ if v < lo { lo = v }
113
+ if v > hi { hi = v }
114
+ }
115
+ }
116
+ }
117
+ return (lo, hi)
118
  }
 
119
  }
120
 
121
  // MARK: - Depth Result
 
128
  var width: Int { multiArray.shape[3].intValue }
129
  var height: Int { multiArray.shape[2].intValue }
130
 
131
+ /// Zero-copy view into the underlying depth data.
132
+ var view: DepthArrayView { DepthArrayView(multiArray) }
 
 
 
 
 
 
 
 
 
 
 
133
  }
134
 
135
  // MARK: - Depth Predictor
 
148
  setupModel(modelURL: modelURL, computeUnits: computeUnits)
149
  }
150
 
151
+ // MARK: Inference
152
 
153
  /// Predict depth from a CGImage. Completion receives a ``DepthResult`` with
154
  /// both a renderable CIImage and the raw Float32 depth multi-array.
155
+ ///
156
+ /// - Parameter fixSeam: When true, runs dual-inference seam fix: infers depth
157
+ /// on both the original and a half-shifted copy, then patches the seam region
158
+ /// from the shifted result into the original to eliminate edge artifacts.
159
+ /// - Parameter debugDir: When provided, intermediate depth maps are saved here
160
+ /// for debugging (depth_original.png, depth_shifted.png, depth_stitched.png).
161
+ func predictDepth(
162
+ from cgImage: CGImage,
163
+ fixSeam: Bool = true,
164
+ debugDir: URL? = nil,
165
+ completion: @escaping (DepthResult?) -> Void
166
+ ) {
167
+ if fixSeam {
168
+ fixSeamWithDualInference(on: cgImage, debugDir: debugDir, completion: completion)
169
+ } else {
170
+ runSingleInference(on: cgImage) { result in
171
+ completion(result)
172
+ }
173
+ }
174
+ }
175
+
176
+ /// Run a single pass of depth inference on a CGImage.
177
+ private func runSingleInference(on cgImage: CGImage, completion: @escaping (DepthResult?) -> Void) {
178
  guard let visionModel else {
179
  print("[DepthPredictor] Model not loaded")
180
  completion(nil)
 
214
  }
215
  }
216
 
217
+ /// Fix the left/right seam by running depth inference on both the original
218
+ /// and a half-shifted copy, then stitching the shifted seam region into the
219
+ /// original depth map.
220
+ ///
221
+ /// Strategy (mirrors the Python approach):
222
+ /// 1. Run depth inference on the original equirectangular image.
223
+ /// 2. Roll the image left by half its width so the seam moves to the center.
224
+ /// 3. Run depth inference on the shifted image — the center of this result
225
+ /// covers what was the original seam, artifact-free.
226
+ /// 4. Roll the original depth left by half (matching the shifted coordinate
227
+ /// space), paste a strip from the shifted depth over the center, then
228
+ /// roll the result back to the original orientation.
229
+ ///
230
+ /// - Parameter patchHalfWidth: Half-width of the strip (in depth-map pixels)
231
+ /// to paste from the shifted depth. The total patch width is 2× this value.
232
+ /// Defaults to 25 px, which works well for 1024-wide depth outputs. Scale
233
+ /// proportionally for other resolutions.
234
+ private func fixSeamWithDualInference(
235
+ on cgImage: CGImage,
236
+ debugDir: URL?,
237
+ patchHalfWidth: Int = 25,
238
+ completion: @escaping (DepthResult?) -> Void
239
+ ) {
240
+ let imageWidth = cgImage.width
241
+ let half = imageWidth / 2
242
+
243
+ // Shift the source image left by half — the seam moves to the center
244
+ guard let shiftedImage = DepthPredictor.shiftImageHorizontally(cgImage, by: half) else {
245
+ print("[DepthPredictor] Failed to shift image for seam fix")
246
+ completion(nil)
247
+ return
248
+ }
249
+
250
+ // Debug: save shifted input
251
+ if let debugDir {
252
+ try? DepthPredictor.saveImage(
253
+ CIImage(cgImage: shiftedImage),
254
+ to: debugDir.appendingPathComponent("input_shifted.png")
255
+ )
256
+ }
257
+
258
+ // 1. Infer depth on the original image
259
+ runSingleInference(on: cgImage) { [weak self] originalDepth in
260
+ guard let self, let originalDepth else {
261
+ completion(nil)
262
+ return
263
+ }
264
+
265
+ if let debugDir {
266
+ try? DepthPredictor.saveDepthAsGrayscale(
267
+ originalDepth,
268
+ to: debugDir.appendingPathComponent("depth_original.png")
269
+ )
270
+ }
271
+
272
+ // 2. Infer depth on the shifted image
273
+ self.runSingleInference(on: shiftedImage) { shiftedDepth in
274
+ guard let shiftedDepth else {
275
+ completion(nil)
276
+ return
277
+ }
278
+
279
+ let w = originalDepth.width
280
+ let h = originalDepth.height
281
+
282
+ if let debugDir {
283
+ try? DepthPredictor.saveDepthAsGrayscale(
284
+ shiftedDepth,
285
+ to: debugDir.appendingPathComponent("depth_shifted.png")
286
+ )
287
+ }
288
+
289
+ // 3. Stitch: roll original depth, patch center, roll back
290
+ guard let stitched = self.stitchSeamFromShiftedDepth(
291
+ original: originalDepth.multiArray,
292
+ shifted: shiftedDepth.multiArray,
293
+ width: w,
294
+ height: h,
295
+ depthHalf: w / 2,
296
+ patchHalfWidth: patchHalfWidth
297
+ ) else {
298
+ completion(nil)
299
+ return
300
+ }
301
+
302
+ let ciImage = self.multiArrayToCIImage(stitched) ?? originalDepth.ciImage
303
+
304
+ if let debugDir {
305
+ let stitchedResult = DepthResult(ciImage: ciImage, multiArray: stitched)
306
+ try? DepthPredictor.saveDepthAsGrayscale(
307
+ stitchedResult,
308
+ to: debugDir.appendingPathComponent("depth_stitched.png")
309
+ )
310
+ }
311
+
312
+ completion(DepthResult(ciImage: ciImage, multiArray: stitched))
313
+ }
314
+ }
315
+ }
316
+
317
+ /// Stitch the seam region using a single output buffer — no intermediate copies.
318
+ ///
319
+ /// For each output pixel (row, col), we determine whether it falls in the
320
+ /// patch zone (the strip around the original seam at column `depthHalf`).
321
+ /// If so, we read from the shifted depth at the corresponding shifted
322
+ /// column; otherwise we read from the original depth at `col` directly.
323
+ ///
324
+ /// This replaces the prior 3-step roll→patch→unroll with 4 temporary arrays.
325
+ private func stitchSeamFromShiftedDepth(
326
+ original: MLMultiArray,
327
+ shifted: MLMultiArray,
328
+ width: Int,
329
+ height: Int,
330
+ depthHalf: Int,
331
+ patchHalfWidth: Int
332
+ ) -> MLMultiArray? {
333
+ let origView = DepthArrayView(original)
334
+ let shiftView = DepthArrayView(shifted)
335
+
336
+ // Patch zone in the *shifted* coordinate space is centered at width/2
337
+ let centerX = width / 2
338
+ let dx = min(patchHalfWidth, centerX)
339
+ let patchLeft = centerX - dx
340
+ let patchRight = centerX + dx // exclusive
341
+
342
+ // Create output MLMultiArray
343
+ let output: MLMultiArray
344
+ do {
345
+ output = try MLMultiArray(shape: original.shape.map { $0 }, dataType: original.dataType)
346
+ } catch {
347
+ print("[DepthPredictor] Failed to create MLMultiArray for stitch: \(error)")
348
+ return nil
349
+ }
350
+
351
+ let outStride = output.strides[2].intValue
352
+ let outPtr = output.dataPointer.bindMemory(to: Float32.self, capacity: width * height)
353
+
354
+ for row in 0..<height {
355
+ let outBase = row * outStride
356
+ for col in 0..<width {
357
+ // Map this output col into the shifted coordinate space:
358
+ // shifting left by depthHalf means shiftedCol = (col + depthHalf) % width
359
+ let shiftedCol = (col + depthHalf) % width
360
+
361
+ if shiftedCol >= patchLeft && shiftedCol < patchRight {
362
+ // This pixel is in the patch zone — use shifted depth
363
+ outPtr[outBase + col] = shiftView.value(row: row, col: shiftedCol)
364
+ } else {
365
+ // Outside patch — use original depth (identity mapping)
366
+ outPtr[outBase + col] = origView.value(row: row, col: col)
367
+ }
368
+ }
369
+ }
370
+
371
+ return output
372
+ }
373
+
374
+ // MARK: Colormap
375
 
376
  /// Apply a jet colormap to depth values -> 8-bit RGB CIImage.
377
  func applyJetColormap(to depth: DepthResult) -> CIImage? {
378
+ applyColormap(to: depth, lut: jetLUT)
379
  }
380
 
381
  /// Apply a turbo colormap to depth values -> 8-bit RGB CIImage.
382
  func applyTurboColormap(to depth: DepthResult) -> CIImage? {
383
+ applyColormap(to: depth, lut: turboLUT)
384
  }
385
 
386
  /// Apply a grayscale visualization with optional contrast.
 
394
  return filter.outputImage ?? ciImage
395
  }
396
 
397
+ /// Apply a colormap LUT to depth values, reading directly from the
398
+ /// MLMultiArray without copying into an intermediate Swift array.
399
+ private func applyColormap(to depth: DepthResult, lut: [RGB]) -> CIImage? {
400
+ let dv = depth.view
401
+ let (minDepth, maxDepth) = dv.minMax()
 
 
 
 
 
 
 
 
 
 
402
  let range = maxDepth - minDepth
403
+ let invRange: Float32 = range > 0 ? 1.0 / range : 1.0
404
 
405
+ let outputBufferSize = dv.width * dv.height * 4
 
406
  guard let outputBuffer = malloc(outputBufferSize) else { return nil }
407
  defer { free(outputBuffer) }
408
 
409
  let outPtr = outputBuffer.bindMemory(to: UInt8.self, capacity: outputBufferSize)
410
+ for row in 0..<dv.height {
411
+ let rowBase = row * dv.rowStride
412
+ let outRowBase = row * dv.width * 4
413
+ for col in 0..<dv.width {
414
+ let normalized = max(0, min(1, (dv.ptr[rowBase + col] - minDepth) * invRange))
415
+ let index = min(Int(normalized * 255), 255)
416
+ let color = lut[index]
417
+ let px = outRowBase + col * 4
418
+ outPtr[px] = color.r
419
+ outPtr[px + 1] = color.g
420
+ outPtr[px + 2] = color.b
421
+ outPtr[px + 3] = 255
422
+ }
423
  }
424
 
 
425
  let colorSpace = CGColorSpaceCreateDeviceRGB()
426
  guard let bitmapContext = CGContext(
427
  data: outPtr,
428
+ width: dv.width,
429
+ height: dv.height,
430
  bitsPerComponent: 8,
431
+ bytesPerRow: dv.width * 4,
432
  space: colorSpace,
433
  bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue
434
  ) else { return nil }
 
437
  return CIImage(cgImage: cgImage)
438
  }
439
 
440
+ // MARK: Save
441
 
442
  /// Save depth values as a 16-bit grayscale PNG (normalized to [0, 65535]).
443
+ /// Reads directly from the MLMultiArray — no intermediate Float32 copy.
444
  static func saveDepthAsGrayscale(_ depth: DepthResult, to path: URL) throws {
445
+ let dv = depth.view
446
+ let (minDepth, maxDepth) = dv.minMax()
 
 
 
 
 
 
 
 
 
 
 
447
  let range = maxDepth - minDepth
448
+ let invRange: Float32 = range > 0 ? 1.0 / range : 1.0
449
 
450
  // Create 16-bit grayscale buffer (big-endian)
451
+ let bufferSize = dv.width * dv.height * 2
452
  guard let buffer = malloc(bufferSize) else {
453
  throw NSError(domain: "DepthPredictor", code: 7,
454
  userInfo: [NSLocalizedDescriptionKey: "Failed to allocate buffer"])
455
  }
456
  defer { free(buffer) }
457
 
458
+ let outPtr = buffer.bindMemory(to: UInt8.self, capacity: bufferSize)
459
+ for row in 0..<dv.height {
460
+ let rowBase = row * dv.rowStride
461
+ let outRowBase = row * dv.width * 2
462
+ for col in 0..<dv.width {
463
+ let normalized = (dv.ptr[rowBase + col] - minDepth) * invRange
464
+ let value = UInt16(max(0, min(65535, normalized * 65535)))
465
+ let px = outRowBase + col * 2
466
+ outPtr[px] = UInt8(value >> 8)
467
+ outPtr[px + 1] = UInt8(value & 0xFF)
468
+ }
469
  }
470
 
471
  let colorSpace = CGColorSpaceCreateDeviceGray()
472
  guard let bitmapContext = CGContext(
473
+ data: outPtr,
474
+ width: dv.width,
475
+ height: dv.height,
476
  bitsPerComponent: 16,
477
+ bytesPerRow: dv.width * 2,
478
  space: colorSpace,
479
  bitmapInfo: CGImageAlphaInfo.none.rawValue | CGBitmapInfo.byteOrder16Big.rawValue
480
  ) else {
 
503
  try pngData.write(to: path)
504
  }
505
 
506
+ // MARK: Private
507
 
508
  private func setupModel(modelURL: URL, computeUnits: MLComputeUnits) {
509
  do {
 
586
  guard let destination = CVPixelBufferGetBaseAddress(buffer) else { return nil }
587
 
588
  let planeStride = multiArray.strides[2].intValue
589
+ let srcBase = multiArray.dataPointer.bindMemory(to: Float32.self, capacity: height * planeStride)
590
+ let rowBytes = width * MemoryLayout<Float32>.stride
591
  for h in 0..<height {
592
+ let srcRow = srcBase.advanced(by: h * planeStride)
593
+ let dstRow = destination.advanced(by: h * rowBytes)
594
+ memcpy(dstRow, srcRow, rowBytes)
 
595
  }
596
 
597
  return CIImage(cvPixelBuffer: buffer)
598
  }
599
  }
600
 
601
+ // MARK: - Image Shifting
602
+
603
+ extension DepthPredictor {
604
+ /// Horizontally roll a CGImage by `offset` pixels (positive = shift left, wrapping around).
605
+ ///
606
+ /// Draws the source image twice into a CGContext with horizontal translations
607
+ /// so the pixels wrap around correctly.
608
+ static func shiftImageHorizontally(_ cgImage: CGImage, by offset: Int) -> CGImage? {
609
+ let w = cgImage.width
610
+ let h = cgImage.height
611
+ let actualOffset = offset % w
612
+ guard actualOffset > 0 else { return cgImage }
613
+
614
+ let colorSpace = cgImage.colorSpace ?? CGColorSpaceCreateDeviceRGB()
615
+
616
+ // Try with the source bitmapInfo first, fall back to explicit RGBA
617
+ var bitmapInfoRaw: UInt32 = cgImage.bitmapInfo.rawValue
618
+ var ctx: CGContext?
619
+
620
+ ctx = CGContext(data: nil, width: w, height: h, bitsPerComponent: 8,
621
+ bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfoRaw)
622
+ if ctx == nil {
623
+ bitmapInfoRaw = CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.noneSkipLast.rawValue
624
+ ctx = CGContext(data: nil, width: w, height: h, bitsPerComponent: 8,
625
+ bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfoRaw)
626
+ }
627
+
628
+ guard let context = ctx else {
629
+ print("[DepthPredictor] shiftImageHorizontally: CGContext creation failed (source bitmapInfo=0x\(String(cgImage.bitmapInfo.rawValue, radix: 16)))")
630
+ return nil
631
+ }
632
+
633
+ // Draw source shifted left by actualOffset (wraps: right portion appears on left)
634
+ context.translateBy(x: -CGFloat(actualOffset), y: 0)
635
+ context.draw(cgImage, in: CGRect(x: 0, y: 0, width: w, height: h))
636
+ // Draw again at +w to fill the wrap-around on the right
637
+ context.translateBy(x: CGFloat(w), y: 0)
638
+ context.draw(cgImage, in: CGRect(x: 0, y: 0, width: w, height: h))
639
+
640
+ guard let result = context.makeImage() else {
641
+ print("[DepthPredictor] shiftImageHorizontally: makeImage() returned nil")
642
+ return nil
643
+ }
644
+ return result
645
+ }
646
+ }
647
+
648
  // MARK: - Image Loading
649
 
650
  extension DepthPredictor {
 
682
  let imagePath: URL
683
  let outputPath: URL
684
  let colormap: String // "grayscale", "jet", "turbo"
685
+ let fixSeam: Bool
686
+ let debugSeamDir: URL? // directory for intermediate seam-fix outputs
687
 
688
  static func parse() -> CommandLineArgs? {
689
  let args = CommandLine.arguments
 
692
  var imagePath: URL?
693
  var outputPath: URL?
694
  var colormap = "grayscale"
695
+ var fixSeam = true
696
+ var debugSeamDir: URL?
697
 
698
  var i = 1
699
  while i < args.count {
 
716
  i += 1
717
  if i < args.count { colormap = args[i].lowercased() }
718
 
719
+ case "-f", "--fix-seam":
720
+ fixSeam = true
721
+
722
+ case "--no-fix-seam":
723
+ fixSeam = false
724
+
725
+ case "--debug-seam":
726
+ i += 1
727
+ if i < args.count { debugSeamDir = URL(fileURLWithPath: args[i]) }
728
+
729
  case "-h", "--help":
730
  printUsage()
731
  return nil
 
758
  modelPath: m,
759
  imagePath: image,
760
  outputPath: output,
761
+ colormap: colormap,
762
+ fixSeam: fixSeam,
763
+ debugSeamDir: debugSeamDir
764
  )
765
  }
766
 
 
783
  -c, --colormap STYLE Colormap: grayscale (default), jet, turbo
784
  grayscale = 16-bit depth values
785
  jet/turbo = 8-bit colorized visualization
786
+ -f, --fix-seam Fix left/right seam artifact via dual-inference stitch (default: on)
787
+ --no-fix-seam Disable seam fixing
788
+ --debug-seam DIR Save intermediate seam-fix outputs to DIR/
789
+ (depth_original.png, depth_shifted.png, depth_stitched.png)
790
  -h, --help Show this help message
791
 
792
  Examples:
 
796
  # Colorized with jet colormap
797
  \(execName) -m DAPModel.mlpackage -i panorama.jpg -o depth.png -c jet
798
 
799
+ # Debug seam fix intermediates
800
+ \(execName) -m DAPModel.mlpackage -i panorama.jpg -o depth.png --debug-seam /tmp/seam_debug
801
 
802
  The model is automatically compiled on first use and cached for subsequent runs.
803
  """)
 
827
  print(" Image size: \(cgImage.width)x\(cgImage.height)")
828
 
829
  // Run inference (async -> sync via semaphore)
830
+ let seamDebugDir: URL? = args.debugSeamDir
831
+ if let debugDir = seamDebugDir {
832
+ try FileManager.default.createDirectory(at: debugDir, withIntermediateDirectories: true)
833
+ print("Seam debug outputs will be saved to \(debugDir.path)")
834
+ }
835
+
836
  print("Running inference...")
837
  let startTime = CFAbsoluteTimeGetCurrent()
838
 
839
  var depthResult: DepthResult?
840
  let semaphore = DispatchSemaphore(value: 0)
841
+ predictor.predictDepth(from: cgImage, fixSeam: args.fixSeam, debugDir: seamDebugDir) { result in
842
  depthResult = result
843
  semaphore.signal()
844
  }
 
888
  }
889
  }
890
 
891
+ main()
PanoramaSplat.swift CHANGED
@@ -19,7 +19,7 @@ import CoreImage
19
  import CoreGraphics
20
  import AppKit
21
 
22
- // MARK: - Command Line Arguments
23
 
24
  struct CLIArgs {
25
  let modelPath: URL
@@ -79,7 +79,7 @@ struct CLIArgs {
79
  }
80
  }
81
 
82
- // MARK: - CoreML Depth Inference
83
 
84
  func compileModelIfNeeded(at url: URL) throws -> URL {
85
  let ext = url.pathExtension.lowercased()
@@ -147,7 +147,7 @@ func runDepthInference(modelURL: URL, image: CGImage) throws -> (depths: [Float3
147
  return (depths, w, h)
148
  }
149
 
150
- // MARK: - Image Pixel Loading
151
 
152
  /// Load image as RGBA pixels resized to target dimensions.
153
  func loadImagePixels(_ image: CGImage, targetW: Int, targetH: Int) -> [UInt8] {
@@ -174,7 +174,7 @@ func loadImagePixels(_ image: CGImage, targetW: Int, targetH: Int) -> [UInt8] {
174
  return pixels
175
  }
176
 
177
- // MARK: - Equirectangular to 3D Projection
178
 
179
  func equiToSphereDirection(u: Float, v: Float, width: Int, height: Int) -> (x: Float, y: Float, z: Float) {
180
  let lon = (u / Float(width) - 0.5) * 2.0 * Float.pi
@@ -183,7 +183,7 @@ func equiToSphereDirection(u: Float, v: Float, width: Int, height: Int) -> (x: F
183
  return (cosLat * cos(lon), sin(lat), cosLat * sin(lon))
184
  }
185
 
186
- // MARK: - PLY Export (binary_little_endian, matches Sharp format)
187
 
188
  func writePLY(gaussians: [(x: Float, y: Float, z: Float,
189
  f0: Float, f1: Float, f2: Float,
@@ -276,7 +276,7 @@ func writePLY(gaussians: [(x: Float, y: Float, z: Float,
276
  try data.write(to: url, options: .atomic)
277
  }
278
 
279
- // MARK: - Depth Map Seam Fix
280
 
281
  /// Equirectangular panoramas wrap at the left/right edges, which creates a
282
  /// seam artifact in the depth prediction. Fix by:
@@ -320,7 +320,7 @@ func fixDepthSeam(_ depths: [Float32], width: Int, height: Int, band: Int = 32)
320
  return result
321
  }
322
 
323
- // MARK: - Main Pipeline
324
 
325
  func main() {
326
  guard let args = CLIArgs.parse() else { exit(1) }
 
19
  import CoreGraphics
20
  import AppKit
21
 
22
+ // - Command Line Arguments
23
 
24
  struct CLIArgs {
25
  let modelPath: URL
 
79
  }
80
  }
81
 
82
+ // - CoreML Depth Inference
83
 
84
  func compileModelIfNeeded(at url: URL) throws -> URL {
85
  let ext = url.pathExtension.lowercased()
 
147
  return (depths, w, h)
148
  }
149
 
150
+ // Image Pixel Loading
151
 
152
  /// Load image as RGBA pixels resized to target dimensions.
153
  func loadImagePixels(_ image: CGImage, targetW: Int, targetH: Int) -> [UInt8] {
 
174
  return pixels
175
  }
176
 
177
+ // Equirectangular to 3D Projection
178
 
179
  func equiToSphereDirection(u: Float, v: Float, width: Int, height: Int) -> (x: Float, y: Float, z: Float) {
180
  let lon = (u / Float(width) - 0.5) * 2.0 * Float.pi
 
183
  return (cosLat * cos(lon), sin(lat), cosLat * sin(lon))
184
  }
185
 
186
+ // - PLY Export (binary_little_endian, matches Sharp format)
187
 
188
  func writePLY(gaussians: [(x: Float, y: Float, z: Float,
189
  f0: Float, f1: Float, f2: Float,
 
276
  try data.write(to: url, options: .atomic)
277
  }
278
 
279
+ // - Depth Map Seam Fix
280
 
281
  /// Equirectangular panoramas wrap at the left/right edges, which creates a
282
  /// seam artifact in the depth prediction. Fix by:
 
320
  return result
321
  }
322
 
323
+ // - Main Pipeline
324
 
325
  func main() {
326
  guard let args = CLIArgs.parse() else { exit(1) }
export_and_validate_coreml.py CHANGED
@@ -7,7 +7,7 @@ compatible with Vision framework and the included DepthPredictor.swift.
7
 
8
  Usage:
9
  python export_and_validate_coreml.py
10
- python export_and_validate_coreml.py --height 512 --width 1024
11
  """
12
 
13
  import os
@@ -192,8 +192,8 @@ def save_comparison_viz(depth_pt, depth_cl, metrics, output_dir):
192
  def main():
193
  parser = ArgumentParser(description="Export DAP to CoreML and validate against PyTorch")
194
  parser.add_argument("--image", default=os.path.join(os.path.dirname(__file__), "test", "test.png"))
195
- parser.add_argument("--height", type=int, default=512)
196
- parser.add_argument("--width", type=int, default=1024)
197
  parser.add_argument("--model_type", choices=["vits", "vitb", "vitl", "vitg"], default="vitl")
198
  parser.add_argument("--weights", default=os.path.join(os.path.dirname(__file__), "model.pth"))
199
  parser.add_argument("--output", default=os.path.join(os.path.dirname(__file__), "DAPModel.mlpackage"))
 
7
 
8
  Usage:
9
  python export_and_validate_coreml.py
10
+ python export_and_validate_coreml.py --height 768 --width 1536
11
  """
12
 
13
  import os
 
192
  def main():
193
  parser = ArgumentParser(description="Export DAP to CoreML and validate against PyTorch")
194
  parser.add_argument("--image", default=os.path.join(os.path.dirname(__file__), "test", "test.png"))
195
+ parser.add_argument("--height", type=int, default=768)
196
+ parser.add_argument("--width", type=int, default=1536)
197
  parser.add_argument("--model_type", choices=["vits", "vitb", "vitl", "vitg"], default="vitl")
198
  parser.add_argument("--weights", default=os.path.join(os.path.dirname(__file__), "model.pth"))
199
  parser.add_argument("--output", default=os.path.join(os.path.dirname(__file__), "DAPModel.mlpackage"))
test/depth.png DELETED

Git LFS Details

  • SHA256: 67984f5d1ea97f1774d523e32e0761e5cb3e25d896c13d12d2004c18adc7001e
  • Pointer size: 131 Bytes
  • Size of remote file: 514 kB
test_output/comparison.png CHANGED

Git LFS Details

  • SHA256: aa3c7ccaae0adea3d782d4f8ea5629835b0741d0fcf2bce7386918e9b5c94091
  • Pointer size: 131 Bytes
  • Size of remote file: 519 kB

Git LFS Details

  • SHA256: d3d1b2f56b9292dc4d00f24ea6bf6dc1944f9095863317596509c15432b16900
  • Pointer size: 131 Bytes
  • Size of remote file: 391 kB
test_output/pytorch_depth.npy CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:10c5b39aa3f1220448efd33e9b3655c2f3c642a5a4fa1d2e25f76e37dfffce93
3
- size 8388736
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a66437ea7424bcaac987f906dc805c856647d71832c6bebbe76482b8220b1aa5
3
+ size 4718720