Add --resample so Hecate can read the 8.64 µm eligible volumes
Browse filesHecate 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.
- hecate.py +22 -3
- requirements.txt +1 -0
|
@@ -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 |
-
|
| 424 |
-
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
| 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'
|
|
@@ -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
|