vincentl065 commited on
Commit
f4386b1
·
verified ·
1 Parent(s): 9cb86e5

Add --resample so Hecate can read the 8.64 µm eligible volumes

Browse files

Hecate refuses any render whose spacing is more than 2 % away from 9.6 µm, and the message says to resample first. Half of the
First Letters eligible volumes (11 of 22: 0175A, 0175B, 0268, 0306B, 0343, 0483A, 0483B, 0490A, 0490B, 0800, 1218) are 8.64 µm,
so on those you either can't run it, or you pass `--spacing-um 9.6` and the model sees letters about 11 % bigger than in training.

This adds an opt-in `--resample` flag: the input stack is rescaled by spacing / 9.6 (linear interpolation) before inference, and
the prediction is mapped back to the input grid so it stays aligned with the segment. Without the flag nothing changes (the error
message now mentions the option). It's refused together with `--output-3d`. `scipy` is added to requirements.txt; it's only
imported when `--resample` is used.

What I measured, on the three PHerc0841 segments with papyrologist labels (official 9.366 µm renders, treated as if they were
8.64 µm scans):
- w00: declared 9.6 without resampling AUC 0.899 → with `--resample` 0.925 (reference at true spacing 0.931)
- AG1: 0.769 → 0.781 (ref 0.782)
- AG2: 0.895 → 0.905 (ref 0.914)

Same direction on all three. Caveat: Hecate saw 0841 during training, so the absolute numbers are optimistic, but the gap between
the two runs is the point.

I know resampling by hand was already recommended; this just makes it one flag and checks that it actually helps.

Files changed (2) hide show
  1. hecate.py +22 -3
  2. requirements.txt +1 -0
hecate.py CHANGED
@@ -411,6 +411,9 @@ def main():
411
  parser.add_argument('--batch-size', type=int, default=1)
412
  parser.add_argument('--stride', type=int, help='XY stride; default half the trained patch width')
413
  parser.add_argument('--precision', choices=('fp32','bf16'), default='fp32')
 
 
 
414
  args = parser.parse_args()
415
  if not args.output and not args.output_3d:
416
  parser.error('Specify --output and/or --output-3d')
@@ -420,21 +423,37 @@ def main():
420
  if path is not None and (path.suffix.lower()!=suffix or path.exists()):
421
  parser.error(f'Choose a new output filename ending in {suffix}: {path}')
422
  model = load_model(args.checkpoint,args.device)
423
- if abs(args.spacing_um/model.sampling_um-1) > .02:
424
- parser.error(f'Resample the render to {model.sampling_um} um first (no automatic resampling)')
 
 
 
425
  volume = open_volume(args.input,args.array)
 
 
 
 
 
 
426
  if output is not None:
427
  output.parent.mkdir(parents=True,exist_ok=True)
428
  with tempfile.TemporaryDirectory(prefix='hecate-') as tmp:
429
  result = np.memmap(Path(tmp)/'prediction',mode='w+',dtype='uint8',shape=volume.shape[-2:])
430
  predict(model,volume,result,args.reverse,args.stride,args.batch_size,args.precision)
 
 
 
 
 
 
431
  partial = output.with_suffix('.partial')
432
  write_png(result,partial)
433
  partial.replace(output)
434
  del result
435
  metadata = {
436
  'checkpoint':Path(args.checkpoint).name,'sampling_um':args.spacing_um,
437
- 'shape_yx':list(volume.shape[-2:]),'reverse_z':args.reverse,
 
438
  'central_depth':model.patch_size[0],'precision':args.precision,
439
  'stride':args.stride or model.patch_size[1]//2,
440
  'probabilities':'uint8 = round(255 * probability); no min-max scaling'
 
411
  parser.add_argument('--batch-size', type=int, default=1)
412
  parser.add_argument('--stride', type=int, help='XY stride; default half the trained patch width')
413
  parser.add_argument('--precision', choices=('fp32','bf16'), default='fp32')
414
+ parser.add_argument('--resample', action='store_true',
415
+ help='Rescale the render from --spacing-um to the model spacing before inference (linear), and map the 2D '
416
+ 'prediction back to the input YX grid. Not supported with --output-3d.')
417
  args = parser.parse_args()
418
  if not args.output and not args.output_3d:
419
  parser.error('Specify --output and/or --output-3d')
 
423
  if path is not None and (path.suffix.lower()!=suffix or path.exists()):
424
  parser.error(f'Choose a new output filename ending in {suffix}: {path}')
425
  model = load_model(args.checkpoint,args.device)
426
+ off_scale = abs(args.spacing_um/model.sampling_um-1) > .02
427
+ if off_scale and not args.resample:
428
+ parser.error(f'Resample the render to {model.sampling_um} um first, or pass --resample')
429
+ if args.resample and output3d is not None:
430
+ parser.error('--resample is only supported for the 2D --output')
431
  volume = open_volume(args.input,args.array)
432
+ input_yx = volume.shape[-2:]
433
+ if args.resample and off_scale:
434
+ from scipy import ndimage
435
+ factor = args.spacing_um/model.sampling_um
436
+ volume = ndimage.zoom(np.asarray(volume),factor,order=1)
437
+ print(f'resampled {args.spacing_um} um -> {model.sampling_um} um: {input_yx} -> {volume.shape[-2:]}',flush=True)
438
  if output is not None:
439
  output.parent.mkdir(parents=True,exist_ok=True)
440
  with tempfile.TemporaryDirectory(prefix='hecate-') as tmp:
441
  result = np.memmap(Path(tmp)/'prediction',mode='w+',dtype='uint8',shape=volume.shape[-2:])
442
  predict(model,volume,result,args.reverse,args.stride,args.batch_size,args.precision)
443
+ if tuple(result.shape)!=tuple(input_yx):
444
+ from scipy import ndimage
445
+ back = ndimage.zoom(np.asarray(result,dtype=np.float32),
446
+ (input_yx[0]/result.shape[0],input_yx[1]/result.shape[1]),order=1)
447
+ back = np.clip(np.round(back),0,255).astype('uint8')[:input_yx[0],:input_yx[1]]
448
+ result = np.zeros(input_yx,dtype='uint8'); result[:back.shape[0],:back.shape[1]] = back
449
  partial = output.with_suffix('.partial')
450
  write_png(result,partial)
451
  partial.replace(output)
452
  del result
453
  metadata = {
454
  'checkpoint':Path(args.checkpoint).name,'sampling_um':args.spacing_um,
455
+ 'shape_yx':list(input_yx),'reverse_z':args.reverse,
456
+ 'resampled_to_um':model.sampling_um if (args.resample and off_scale) else None,
457
  'central_depth':model.patch_size[0],'precision':args.precision,
458
  'stride':args.stride or model.patch_size[1]//2,
459
  'probabilities':'uint8 = round(255 * probability); no min-max scaling'
requirements.txt CHANGED
@@ -2,3 +2,4 @@ torch>=2.6,<3
2
  numpy>=1.26,<3
3
  zarr>=2.18,<4
4
  numcodecs>=0.12,<1
 
 
2
  numpy>=1.26,<3
3
  zarr>=2.18,<4
4
  numcodecs>=0.12,<1
5
+ scipy>=1.11,<2