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

#3
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