Kyle Pearson commited on
Commit
dc95a1d
·
1 Parent(s): cfcc093

Add validation utilities, update model conversion logic, fix manifest.json, rename deprecated flags, improve docs

Browse files
README.md CHANGED
@@ -63,7 +63,7 @@ Use the provided [sharp.swift](sharp.swift) inference script to load the model a
63
  swiftc -O -o run_sharp sharp.swift -framework CoreML -framework CoreImage -framework AppKit
64
 
65
  # Run inference on an image and decimate the output by 50%
66
- ./run_sharp sharp.mlpackage city.png city.ply -d 0.5
67
  ```
68
 
69
  > Inference on an Apple M4 Max takes ~1.9 seconds.
 
63
  swiftc -O -o run_sharp sharp.swift -framework CoreML -framework CoreImage -framework AppKit
64
 
65
  # Run inference on an image and decimate the output by 50%
66
+ ./run_sharp sharp.mlpackage test.png test.ply -d 0.5
67
  ```
68
 
69
  > Inference on an Apple M4 Max takes ~1.9 seconds.
convert.py CHANGED
@@ -8,6 +8,7 @@ from __future__ import annotations
8
 
9
  import argparse
10
  import logging
 
11
  from pathlib import Path
12
  from typing import Any
13
 
@@ -25,19 +26,92 @@ LOGGER = logging.getLogger(__name__)
25
 
26
  DEFAULT_MODEL_URL = "https://ml-site.cdn-apple.com/models/sharp/sharp_2572gikvuh.pt"
27
 
28
-
29
- class SafeClamp(nn.Module):
30
- """Safe clamp operation that avoids tracing issues."""
31
-
32
- def forward(self, x, min_val=1e-4, max_val=1e4):
33
- return torch.clamp(x, min=min_val, max=max_val)
34
-
35
-
36
- class SafeDivision(nn.Module):
37
- """Safe division that avoids division by zero."""
38
-
39
- def forward(self, numerator, denominator):
40
- return numerator / torch.clamp(denominator, min=1e-8)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
 
43
  class SharpModelTraceable(nn.Module):
@@ -61,10 +135,10 @@ class SharpModelTraceable(nn.Module):
61
  self.prediction_head = predictor.prediction_head
62
  self.gaussian_composer = predictor.gaussian_composer
63
  self.depth_alignment = predictor.depth_alignment
64
-
65
- # Replace problematic operations with custom modules
66
- self.safe_clamp = SafeClamp()
67
- self.safe_div = SafeDivision()
68
 
69
  def forward(
70
  self,
@@ -95,12 +169,17 @@ class SharpModelTraceable(nn.Module):
95
  # Apply depth alignment (inference mode)
96
  monodepth, _ = self.depth_alignment(monodepth, None, monodepth_output.decoder_features)
97
 
 
 
 
 
98
  # Initialize gaussians
99
  init_output = self.init_model(image, monodepth)
100
 
101
- # Store global_scale for debugging if in eval mode (not during tracing)
102
- if hasattr(self, '_store_global_scale'):
103
- self._stored_global_scale = init_output.global_scale
 
104
 
105
  # Extract features
106
  image_features = self.feature_model(
@@ -358,142 +437,6 @@ def convert_to_coreml(
358
  return mlmodel
359
 
360
 
361
- def convert_to_coreml_with_preprocessing(
362
- predictor: RGBGaussianPredictor,
363
- output_path: Path,
364
- input_shape: tuple[int, int] = (1536, 1536),
365
- ) -> ct.models.MLModel:
366
- """Convert SHARP model to Core ML with built-in image preprocessing.
367
-
368
- This version includes image normalization as part of the model,
369
- accepting uint8 images as input.
370
-
371
- Args:
372
- predictor: The SHARP RGBGaussianPredictor model.
373
- output_path: Path to save the .mlmodel file.
374
- input_shape: Input image shape (height, width).
375
-
376
- Returns:
377
- The converted Core ML model.
378
- """
379
-
380
- class SharpWithPreprocessing(nn.Module):
381
- """SHARP model with integrated preprocessing."""
382
-
383
- def __init__(self, base_model: SharpModelTraceable):
384
- super().__init__()
385
- self.base_model = base_model
386
-
387
- def forward(
388
- self,
389
- image: torch.Tensor,
390
- disparity_factor: torch.Tensor
391
- ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
392
- # Normalize image from [0, 255] to [0, 1]
393
- image_normalized = image / 255.0
394
- return self.base_model(image_normalized, disparity_factor)
395
-
396
- model_wrapper = SharpWithPreprocessing(SharpModelTraceable(predictor))
397
- model_wrapper.eval()
398
-
399
- height, width = input_shape
400
- example_image = torch.randint(0, 256, (1, 3, height, width), dtype=torch.float32)
401
- example_disparity_factor = torch.tensor([1.0])
402
-
403
- LOGGER.info("Tracing model with preprocessing...")
404
- with torch.no_grad():
405
- traced_model = torch.jit.trace(
406
- model_wrapper,
407
- (example_image, example_disparity_factor),
408
- strict=False,
409
- )
410
-
411
- inputs = [
412
- ct.ImageType(
413
- name="image",
414
- shape=(1, 3, height, width),
415
- scale=1.0, # Will be normalized in the model
416
- color_layout=ct.colorlayout.RGB,
417
- ),
418
- ct.TensorType(
419
- name="disparity_factor",
420
- shape=(1,),
421
- dtype=np.float32,
422
- ),
423
- ]
424
-
425
- # Define output names with clear, descriptive labels
426
- output_names = [
427
- "mean_vectors_3d_positions", # 3D positions (NDC space)
428
- "singular_values_scales", # Scale parameters (diagonal of covariance)
429
- "quaternions_rotations", # Rotation as quaternions
430
- "colors_rgb_linear", # RGB colors in linear color space
431
- "opacities_alpha_channel", # Opacity values (alpha)
432
- ]
433
-
434
- # Define outputs with proper names for Core ML conversion
435
- outputs = [
436
- ct.TensorType(name=output_names[0], dtype=np.float32),
437
- ct.TensorType(name=output_names[1], dtype=np.float32),
438
- ct.TensorType(name=output_names[2], dtype=np.float32),
439
- ct.TensorType(name=output_names[3], dtype=np.float32),
440
- ct.TensorType(name=output_names[4], dtype=np.float32),
441
- ]
442
-
443
- mlmodel = ct.convert(
444
- traced_model,
445
- inputs=inputs,
446
- outputs=outputs, # Specify output names during conversion
447
- convert_to="mlprogram",
448
- compute_precision=ct.precision.FLOAT16,
449
- )
450
-
451
- mlmodel.author = "Apple Inc."
452
- mlmodel.short_description = "SHARP model with integrated image preprocessing"
453
- mlmodel.version = "1.0.0"
454
-
455
- # Output descriptions with clear intent and units
456
- output_descriptions = {
457
- "mean_vectors_3d_positions": (
458
- "3D positions of Gaussian splats in normalized device coordinates (NDC). "
459
- "Shape: (1, N, 3), where N is the number of Gaussians."
460
- ),
461
- "singular_values_scales": (
462
- "Scale factors for each Gaussian along its principal axes. "
463
- "Represents size and anisotropy. Shape: (1, N, 3)."
464
- ),
465
- "quaternions_rotations": (
466
- "Rotation of each Gaussian as a unit quaternion [w, x, y, z]. "
467
- "Used to orient the ellipsoid. Shape: (1, N, 4)."
468
- ),
469
- "colors_rgb_linear": (
470
- "RGB color values in linear RGB space (not gamma-corrected). "
471
- "Shape: (1, N, 3), with range [0, 1]."
472
- ),
473
- "opacities_alpha_channel": (
474
- "Opacity value per Gaussian (alpha channel), used for blending. "
475
- "Shape: (1, N), where values are in [0, 1]."
476
- ),
477
- }
478
-
479
- # Update output names and descriptions via spec BEFORE saving
480
- spec = mlmodel.get_spec()
481
-
482
- # Set output descriptions
483
- for i, name in enumerate(output_names):
484
- if i < len(spec.description.output):
485
- output = spec.description.output[i]
486
- output.name = name
487
- output.shortDescription = output_descriptions[name]
488
-
489
- LOGGER.info("Output names after update: %s", [o.name for o in spec.description.output])
490
-
491
- # Save the model with correct names
492
- mlmodel.save(str(output_path))
493
-
494
- return mlmodel
495
-
496
-
497
  class QuaternionValidator:
498
  """Validator for quaternion comparisons with configurable tolerances and outlier analysis."""
499
 
@@ -658,6 +601,130 @@ class QuaternionValidator:
658
  }
659
 
660
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
661
  def format_validation_table(
662
  validation_results: list[dict],
663
  image_name: str,
@@ -1222,89 +1289,29 @@ def validate_with_single_image_detailed(
1222
  """
1223
  # Load and preprocess the input image
1224
  test_image = load_and_preprocess_image(image_path, input_shape)
1225
- test_disparity = np.array([1.0], dtype=np.float32)
1226
-
1227
- # Run PyTorch model
1228
- traceable_wrapper = SharpModelTraceable(pytorch_model)
1229
- traceable_wrapper.eval()
1230
-
1231
- with torch.no_grad():
1232
- pt_outputs = traceable_wrapper(test_image, torch.from_numpy(test_disparity))
1233
-
1234
- # Run Core ML model
1235
- test_image_np = test_image.numpy()
1236
- coreml_inputs = {
1237
- "image": test_image_np,
1238
- "disparity_factor": test_disparity,
1239
- }
1240
- coreml_outputs = mlmodel.predict(coreml_inputs)
1241
-
1242
- # Output configuration
1243
- output_names = ["mean_vectors_3d_positions", "singular_values_scales", "quaternions_rotations", "colors_rgb_linear", "opacities_alpha_channel"]
1244
-
1245
  # Tolerances for real image validation
1246
- tolerances = {
1247
- "mean_vectors_3d_positions": 1.2,
1248
- "singular_values_scales": 0.01,
1249
- "colors_rgb_linear": 0.01,
1250
- "opacities_alpha_channel": 0.05,
1251
- "quaternions_rotations": 5.0,
1252
- }
1253
-
1254
- # Use provided validator or create default
1255
  if quat_validator is None:
1256
- quat_validator = QuaternionValidator()
1257
-
1258
- # Collect validation results
1259
- validation_results = []
1260
-
1261
- for i, name in enumerate(output_names):
1262
- pt_output = pt_outputs[i].numpy()
1263
-
1264
- # Find matching Core ML output
1265
- coreml_key = None
1266
- if name in coreml_outputs:
1267
- coreml_key = name
1268
- else:
1269
- for key in coreml_outputs:
1270
- base_name = name.split('_')[0]
1271
- if base_name in key.lower():
1272
- coreml_key = key
1273
- break
1274
- if coreml_key is None:
1275
- coreml_key = list(coreml_outputs.keys())[i]
1276
-
1277
- coreml_output = coreml_outputs[coreml_key]
1278
- result = {"output": name, "passed": True, "failure_reason": ""}
1279
-
1280
- if name == "quaternions_rotations":
1281
- # Use QuaternionValidator
1282
- quat_result = quat_validator.validate(pt_output, coreml_output, image_name=image_path.name)
1283
-
1284
- result.update({
1285
- "max_diff": f"{quat_result['stats']['max']:.6f}",
1286
- "mean_diff": f"{quat_result['stats']['mean']:.6f}",
1287
- "p99_diff": f"{quat_result['stats']['p99']:.6f}",
1288
- "passed": quat_result["passed"],
1289
- "failure_reason": "; ".join(quat_result["failure_reasons"]) if quat_result["failure_reasons"] else "",
1290
- })
1291
- else:
1292
- diff = np.abs(pt_output - coreml_output)
1293
- output_tolerance = tolerances.get(name, 0.01)
1294
- max_diff = np.max(diff)
1295
-
1296
- result.update({
1297
- "max_diff": f"{max_diff:.6f}",
1298
- "mean_diff": f"{np.mean(diff):.6f}",
1299
- "p99_diff": f"{np.percentile(diff, 99):.6f}",
1300
- })
1301
-
1302
- if max_diff > output_tolerance:
1303
- result["passed"] = False
1304
- result["failure_reason"] = f"max diff {max_diff:.6f} > tolerance {output_tolerance:.6f}"
1305
-
1306
- validation_results.append(result)
1307
-
1308
  return validation_results
1309
 
1310
 
@@ -1469,11 +1476,6 @@ def main():
1469
  action="store_true",
1470
  help="Validate Core ML model against PyTorch",
1471
  )
1472
- parser.add_argument(
1473
- "--with-preprocessing",
1474
- action="store_true",
1475
- help="Include image preprocessing (uint8 -> float normalization)",
1476
- )
1477
  parser.add_argument(
1478
  "-v", "--verbose",
1479
  action="store_true",
@@ -1522,21 +1524,13 @@ def main():
1522
  precision = ct.precision.FLOAT16 if args.precision == "float16" else ct.precision.FLOAT32
1523
 
1524
  # Convert to Core ML
1525
- if args.with_preprocessing:
1526
- LOGGER.info("Converting with integrated preprocessing...")
1527
- mlmodel = convert_to_coreml_with_preprocessing(
1528
- predictor,
1529
- args.output,
1530
- input_shape=input_shape,
1531
- )
1532
- else:
1533
- LOGGER.info("Converting using direct tracing...")
1534
- mlmodel = convert_to_coreml(
1535
- predictor,
1536
- args.output,
1537
- input_shape=input_shape,
1538
- compute_precision=precision,
1539
- )
1540
 
1541
  LOGGER.info(f"Core ML model saved to {args.output}")
1542
 
@@ -1570,3 +1564,4 @@ def main():
1570
 
1571
  if __name__ == "__main__":
1572
  exit(main())
 
 
8
 
9
  import argparse
10
  import logging
11
+ from dataclasses import dataclass
12
  from pathlib import Path
13
  from typing import Any
14
 
 
26
 
27
  DEFAULT_MODEL_URL = "https://ml-site.cdn-apple.com/models/sharp/sharp_2572gikvuh.pt"
28
 
29
+ # ============================================================================
30
+ # Constants & Configuration
31
+ # ============================================================================
32
+
33
+ # Output names for Core ML model
34
+ OUTPUT_NAMES = [
35
+ "mean_vectors_3d_positions",
36
+ "singular_values_scales",
37
+ "quaternions_rotations",
38
+ "colors_rgb_linear",
39
+ "opacities_alpha_channel",
40
+ ]
41
+
42
+ # Output descriptions for Core ML metadata
43
+ OUTPUT_DESCRIPTIONS = {
44
+ "mean_vectors_3d_positions": (
45
+ "3D positions of Gaussian splats in normalized device coordinates (NDC). "
46
+ "Shape: (1, N, 3), where N is the number of Gaussians."
47
+ ),
48
+ "singular_values_scales": (
49
+ "Scale factors for each Gaussian along its principal axes. "
50
+ "Represents size and anisotropy. Shape: (1, N, 3)."
51
+ ),
52
+ "quaternions_rotations": (
53
+ "Rotation of each Gaussian as a unit quaternion [w, x, y, z]. "
54
+ "Used to orient the ellipsoid. Shape: (1, N, 4)."
55
+ ),
56
+ "colors_rgb_linear": (
57
+ "RGB color values in linear RGB space (not gamma-corrected). "
58
+ "Shape: (1, N, 3), with range [0, 1]."
59
+ ),
60
+ "opacities_alpha_channel": (
61
+ "Opacity value per Gaussian (alpha channel), used for blending. "
62
+ "Shape: (1, N), where values are in [0, 1]."
63
+ ),
64
+ }
65
+
66
+
67
+ @dataclass
68
+ class ToleranceConfig:
69
+ """Tolerance configuration for validation."""
70
+
71
+ # Tolerances for random validation (tight)
72
+ random_tolerances: dict[str, float] = None
73
+
74
+ # Tolerances for real image validation (more lenient)
75
+ image_tolerances: dict[str, float] = None
76
+
77
+ # Angular tolerances for quaternions (in degrees)
78
+ angular_tolerances_random: dict[str, float] = None
79
+ angular_tolerances_image: dict[str, float] = None
80
+
81
+ def __post_init__(self):
82
+ if self.random_tolerances is None:
83
+ self.random_tolerances = {
84
+ "mean_vectors_3d_positions": 0.001,
85
+ "singular_values_scales": 0.0001,
86
+ "quaternions_rotations": 2.0,
87
+ "colors_rgb_linear": 0.002,
88
+ "opacities_alpha_channel": 0.005,
89
+ }
90
+
91
+ if self.image_tolerances is None:
92
+ self.image_tolerances = {
93
+ "mean_vectors_3d_positions": 1.2,
94
+ "singular_values_scales": 0.01,
95
+ "quaternions_rotations": 5.0,
96
+ "colors_rgb_linear": 0.01,
97
+ "opacities_alpha_channel": 0.05,
98
+ }
99
+
100
+ if self.angular_tolerances_random is None:
101
+ self.angular_tolerances_random = {
102
+ "mean": 0.01,
103
+ "p99": 0.1,
104
+ "p99_9": 1.0,
105
+ "max": 5.0,
106
+ }
107
+
108
+ if self.angular_tolerances_image is None:
109
+ self.angular_tolerances_image = {
110
+ "mean": 0.2,
111
+ "p99": 2.0,
112
+ "p99_9": 5.0,
113
+ "max": 25.0,
114
+ }
115
 
116
 
117
  class SharpModelTraceable(nn.Module):
 
135
  self.prediction_head = predictor.prediction_head
136
  self.gaussian_composer = predictor.gaussian_composer
137
  self.depth_alignment = predictor.depth_alignment
138
+
139
+ # For debugging: store global_scale
140
+ self.last_global_scale = None
141
+ self.last_monodepth_min = None
142
 
143
  def forward(
144
  self,
 
169
  # Apply depth alignment (inference mode)
170
  monodepth, _ = self.depth_alignment(monodepth, None, monodepth_output.decoder_features)
171
 
172
+ # Store monodepth min for debugging (before normalization)
173
+ if not torch.jit.is_scripting() and not torch.jit.is_tracing():
174
+ self.last_monodepth_min = monodepth.flatten().min().item()
175
+
176
  # Initialize gaussians
177
  init_output = self.init_model(image, monodepth)
178
 
179
+ # Store global_scale for debugging
180
+ if not torch.jit.is_scripting() and not torch.jit.is_tracing():
181
+ if init_output.global_scale is not None:
182
+ self.last_global_scale = init_output.global_scale.item()
183
 
184
  # Extract features
185
  image_features = self.feature_model(
 
437
  return mlmodel
438
 
439
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  class QuaternionValidator:
441
  """Validator for quaternion comparisons with configurable tolerances and outlier analysis."""
442
 
 
601
  }
602
 
603
 
604
+ def find_coreml_output_key(name: str, coreml_outputs: dict) -> str:
605
+ """Find matching Core ML output key for a given output name.
606
+
607
+ Args:
608
+ name: The expected output name
609
+ coreml_outputs: Dictionary of Core ML outputs
610
+
611
+ Returns:
612
+ The matching key from coreml_outputs
613
+ """
614
+ if name in coreml_outputs:
615
+ return name
616
+
617
+ # Try partial match
618
+ for key in coreml_outputs:
619
+ base_name = name.split('_')[0]
620
+ if base_name in key.lower():
621
+ return key
622
+
623
+ # Fallback to index-based lookup
624
+ output_index = OUTPUT_NAMES.index(name) if name in OUTPUT_NAMES else 0
625
+ return list(coreml_outputs.keys())[output_index]
626
+
627
+
628
+ def run_inference_pair(
629
+ pytorch_model: RGBGaussianPredictor,
630
+ mlmodel: ct.models.MLModel,
631
+ image_tensor: torch.Tensor,
632
+ disparity_factor: float = 1.0,
633
+ ) -> tuple[list[np.ndarray], dict[str, np.ndarray]]:
634
+ """Run inference on both PyTorch and Core ML models.
635
+
636
+ Args:
637
+ pytorch_model: The PyTorch model
638
+ mlmodel: The Core ML model
639
+ image_tensor: Input image tensor
640
+ disparity_factor: Disparity factor value
641
+
642
+ Returns:
643
+ Tuple of (pytorch_outputs, coreml_outputs)
644
+ """
645
+ # Run PyTorch model
646
+ traceable_wrapper = SharpModelTraceable(pytorch_model)
647
+ traceable_wrapper.eval()
648
+
649
+ test_disparity_pt = torch.tensor([disparity_factor])
650
+ with torch.no_grad():
651
+ pt_outputs = traceable_wrapper(image_tensor, test_disparity_pt)
652
+
653
+ # Convert to numpy
654
+ pt_outputs_np = [o.numpy() for o in pt_outputs]
655
+
656
+ # Run Core ML model
657
+ test_image_np = image_tensor.numpy()
658
+ test_disparity_np = np.array([disparity_factor], dtype=np.float32)
659
+ coreml_inputs = {
660
+ "image": test_image_np,
661
+ "disparity_factor": test_disparity_np,
662
+ }
663
+ coreml_outputs = mlmodel.predict(coreml_inputs)
664
+
665
+ return pt_outputs_np, coreml_outputs
666
+
667
+
668
+ def compare_outputs(
669
+ pt_outputs: list[np.ndarray],
670
+ coreml_outputs: dict[str, np.ndarray],
671
+ tolerances: dict[str, float],
672
+ quat_validator: QuaternionValidator,
673
+ image_name: str = "Unknown",
674
+ ) -> list[dict]:
675
+ """Compare PyTorch and Core ML outputs.
676
+
677
+ Args:
678
+ pt_outputs: List of PyTorch outputs
679
+ coreml_outputs: Dictionary of Core ML outputs
680
+ tolerances: Tolerance values per output type
681
+ quat_validator: QuaternionValidator instance
682
+ image_name: Name of the image being validated
683
+
684
+ Returns:
685
+ List of validation result dictionaries
686
+ """
687
+ validation_results = []
688
+
689
+ for i, name in enumerate(OUTPUT_NAMES):
690
+ pt_output = pt_outputs[i]
691
+ coreml_key = find_coreml_output_key(name, coreml_outputs)
692
+ coreml_output = coreml_outputs[coreml_key]
693
+
694
+ result = {"output": name, "passed": True, "failure_reason": ""}
695
+
696
+ if name == "quaternions_rotations":
697
+ # Use QuaternionValidator for quaternions
698
+ quat_result = quat_validator.validate(pt_output, coreml_output, image_name=image_name)
699
+
700
+ result.update({
701
+ "max_diff": f"{quat_result['stats']['max']:.6f}",
702
+ "mean_diff": f"{quat_result['stats']['mean']:.6f}",
703
+ "p99_diff": f"{quat_result['stats']['p99']:.6f}",
704
+ "passed": quat_result["passed"],
705
+ "failure_reason": "; ".join(quat_result["failure_reasons"]) if quat_result["failure_reasons"] else "",
706
+ })
707
+ else:
708
+ # Standard numerical comparison
709
+ diff = np.abs(pt_output - coreml_output)
710
+ output_tolerance = tolerances.get(name, 0.01)
711
+ max_diff = np.max(diff)
712
+
713
+ result.update({
714
+ "max_diff": f"{max_diff:.6f}",
715
+ "mean_diff": f"{np.mean(diff):.6f}",
716
+ "p99_diff": f"{np.percentile(diff, 99):.6f}",
717
+ })
718
+
719
+ if max_diff > output_tolerance:
720
+ result["passed"] = False
721
+ result["failure_reason"] = f"max diff {max_diff:.6f} > tolerance {output_tolerance:.6f}"
722
+
723
+ validation_results.append(result)
724
+
725
+ return validation_results
726
+
727
+
728
  def format_validation_table(
729
  validation_results: list[dict],
730
  image_name: str,
 
1289
  """
1290
  # Load and preprocess the input image
1291
  test_image = load_and_preprocess_image(image_path, input_shape)
1292
+
1293
+ # Run inference on both models
1294
+ pt_outputs, coreml_outputs = run_inference_pair(pytorch_model, mlmodel, test_image)
1295
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1296
  # Tolerances for real image validation
1297
+ tolerance_config = ToleranceConfig()
1298
+ tolerances = tolerance_config.image_tolerances
1299
+
1300
+ # Use provided validator or create default with image tolerances
 
 
 
 
 
1301
  if quat_validator is None:
1302
+ quat_validator = QuaternionValidator(
1303
+ angular_tolerances=tolerance_config.angular_tolerances_image
1304
+ )
1305
+
1306
+ # Compare outputs
1307
+ validation_results = compare_outputs(
1308
+ pt_outputs,
1309
+ coreml_outputs,
1310
+ tolerances,
1311
+ quat_validator,
1312
+ image_name=image_path.name
1313
+ )
1314
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1315
  return validation_results
1316
 
1317
 
 
1476
  action="store_true",
1477
  help="Validate Core ML model against PyTorch",
1478
  )
 
 
 
 
 
1479
  parser.add_argument(
1480
  "-v", "--verbose",
1481
  action="store_true",
 
1524
  precision = ct.precision.FLOAT16 if args.precision == "float16" else ct.precision.FLOAT32
1525
 
1526
  # Convert to Core ML
1527
+ LOGGER.info("Converting using direct tracing...")
1528
+ mlmodel = convert_to_coreml(
1529
+ predictor,
1530
+ args.output,
1531
+ input_shape=input_shape,
1532
+ compute_precision=precision,
1533
+ )
 
 
 
 
 
 
 
 
1534
 
1535
  LOGGER.info(f"Core ML model saved to {args.output}")
1536
 
 
1564
 
1565
  if __name__ == "__main__":
1566
  exit(main())
1567
+ exit(main())
sharp.mlpackage/Data/com.apple.CoreML/model.mlmodel CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:ca2a548947bdf1616a9c7ddf093c27dc0aeb8225a1e50cb40eb098d7aa47a2b5
3
  size 938769
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3e9fd96f088b6d324250226cfcbe7e197b735dbb9322687c177b4c2a8377fb51
3
  size 938769
sharp.mlpackage/Manifest.json CHANGED
@@ -1,18 +1,18 @@
1
  {
2
  "fileFormatVersion": "1.0.0",
3
  "itemInfoEntries": {
4
- "1504890B-E584-4EC2-A1CF-F87AE1A1BAA0": {
5
- "author": "com.apple.CoreML",
6
- "description": "CoreML Model Weights",
7
- "name": "weights",
8
- "path": "com.apple.CoreML/weights"
9
- },
10
- "D59C5780-FA59-423A-8088-BCF64225C1B3": {
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": "D59C5780-FA59-423A-8088-BCF64225C1B3"
18
  }
 
1
  {
2
  "fileFormatVersion": "1.0.0",
3
  "itemInfoEntries": {
4
+ "551E6A6B-AAB8-4DA8-B1D0-2D3A73254AD2": {
 
 
 
 
 
 
5
  "author": "com.apple.CoreML",
6
  "description": "CoreML Model Specification",
7
  "name": "model.mlmodel",
8
  "path": "com.apple.CoreML/model.mlmodel"
9
+ },
10
+ "DD041C71-3C41-47F0-830E-A829C8EEC1EA": {
11
+ "author": "com.apple.CoreML",
12
+ "description": "CoreML Model Weights",
13
+ "name": "weights",
14
+ "path": "com.apple.CoreML/weights"
15
  }
16
  },
17
+ "rootModelIdentifier": "551E6A6B-AAB8-4DA8-B1D0-2D3A73254AD2"
18
  }