Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
db6325e
Preserve APG optimization campaign state before clean-up
constantinpape Sep 6, 2026
5a73e08
Remove the refuted APG optimizations and their experiment harness
constantinpape Sep 6, 2026
fe13294
Annotate the APG campaign notes and record the experimental set-up
constantinpape Sep 6, 2026
7df6eea
Revert the UniSAM2 decoder-width shim now that torch_em honours initi…
constantinpape Sep 6, 2026
49cb8e7
Fix the evaluation submitter defaults and record the v4 baseline results
constantinpape Sep 6, 2026
649b74c
Add the AIS optimization harness on cached decoder predictions
constantinpape Sep 6, 2026
57855d6
Add an opt-in boundary-magnitude instance filter to the flow post-pro…
constantinpape Sep 6, 2026
db515d9
Rank AIS sweep combinations under the generalization gate and record …
constantinpape Sep 6, 2026
1417567
Let the production evaluation run an AIS benchmark configuration
constantinpape Sep 6, 2026
d0ab0a0
Promote the optimized AIS defaults for hvit_t and make them dimension…
constantinpape Sep 6, 2026
5b89ee7
Disable the default filter explicitly in the unfiltered reference of …
constantinpape Sep 6, 2026
b24b1f0
Compare two tagged AIS production evaluations dataset by dataset
constantinpape Sep 6, 2026
8414599
Keep the registry smoothing and size floor for volumes, add only the …
constantinpape Sep 6, 2026
2b7c814
Record the AIS campaign's canonical, test-manifest and 2D production …
constantinpape Sep 6, 2026
abe8b2c
Record the 3D production results and the status of the AIS campaign
constantinpape Sep 6, 2026
6ed825d
Add an opt-in seed floor to the flow post-processing
constantinpape Sep 6, 2026
bcb2b49
Record the seed-floor screen and add the concise summary of the AIS c…
constantinpape Sep 6, 2026
ecf49ee
Add a contact channel and a boundary-weighted foreground loss to the …
constantinpape Sep 6, 2026
b3df7f4
Add the AIS decoder training campaign: decoder-only training, staging…
constantinpape Sep 6, 2026
2c99373
Add the per-variant evaluation driver of the AIS decoder campaign
constantinpape Sep 6, 2026
f098449
Fork the loader workers and redraw files the sampler rejects in the d…
constantinpape Sep 7, 2026
b8fa1e4
Record the epoch A5 bit-identity check and the resubmission in the de…
constantinpape Sep 7, 2026
b9ed44e
Outline the results section of the decoder campaign notes and record …
constantinpape Sep 7, 2026
e166a0f
Record the early training curves of the decoder campaign
constantinpape Sep 7, 2026
38cf12c
Record the preliminary fgcal-vs-production result of the decoder camp…
constantinpape Sep 7, 2026
cccee4b
Fix the diagnostics CLI and record the fgcal field diagnostics
constantinpape Sep 7, 2026
b44d69e
Add the unattended finalisation of the decoder campaign reports
constantinpape Sep 7, 2026
9e4a5bd
Record the preliminary both-vs-fgcal comparison of the decoder campaign
constantinpape Sep 7, 2026
61e1957
Record the 3D collapse of the five-channel decoder under the boundary…
constantinpape Sep 7, 2026
285e8f0
Add the contact-configuration screens and the unattended tuning launc…
constantinpape Sep 7, 2026
ca85351
Record the contact-head diagnostics and the tuning jobs of the decode…
constantinpape Sep 7, 2026
4cbbdd9
Record the fgcal sweep ranking and add its confirmation configurations
constantinpape Sep 7, 2026
455994c
Record the both sweep ranking and add the shared top-configuration sc…
constantinpape Sep 7, 2026
6c941ef
Record the contact-configuration screens of the both decoder
constantinpape Sep 7, 2026
1e58d8a
Record the tuned fgcal-vs-both comparison of the decoder campaign
constantinpape Sep 7, 2026
83ac719
Record the isolating baseline-referenced comparison of the decoder ca…
constantinpape Sep 7, 2026
b3ddc4a
Record the mechanisms behind the both-vs-baseline differences
constantinpape Sep 7, 2026
2f9d212
Record the four-way default comparison of the decoder campaign
constantinpape Sep 7, 2026
a380c23
Record the tuned four-way comparison and the conclusions of the decod…
constantinpape Sep 7, 2026
b6f6add
Add the full-boundary mode of the fifth channel, two boundary variant…
constantinpape Sep 7, 2026
e58844c
Record the round-2 launch in the decoder campaign hand-over
constantinpape Sep 7, 2026
9477f47
Chain the round-2 decoder analysis and record the launch
constantinpape Sep 7, 2026
c1bd2e9
Score the fifth channel against the target it was trained on
constantinpape Sep 7, 2026
cfedf42
Record the round-1 completions and rewrite the hand-over for reading
constantinpape Sep 7, 2026
a0c4869
Record what the ridge and mask modes mean for a full boundary
constantinpape Sep 7, 2026
ee403f9
Rank the baseline sweep, which revises the fgcal verdict
constantinpape Sep 7, 2026
183fee0
Close round 1: the contact sweep confirms the channel loses when tuned
constantinpape Sep 7, 2026
7c8b653
Record that the edited-script pitfall killed a second job
constantinpape Sep 7, 2026
42e46ad
Refresh the hand-over state for a deliberately stopped session
constantinpape Sep 7, 2026
f40192a
Pass launcher variants as arguments: SBATCH_EXPORT=none drops them
constantinpape Sep 8, 2026
526a9bb
Round 2 results: the boundary channel repairs the collateral damage
constantinpape Sep 8, 2026
289057f
The dic_hepg2 head is under-confident, not dead; ridge vs mask
constantinpape Sep 8, 2026
a0943a9
The boundary target does not inflate the foreground; restate the gain
constantinpape Sep 8, 2026
eb69afc
Retract the 10/11: compare against baseline at its own optimum
constantinpape Sep 8, 2026
19a92df
Six sweep optima separate the two loss changes with no exceptions
constantinpape Sep 8, 2026
4e8e816
The 3d LM failure of five-channel decoders is a flooded foreground
constantinpape Sep 8, 2026
3e43d96
Conclusive overview of the six decoders
constantinpape Sep 8, 2026
b149212
Record the sweep results cleanly, without dic_hepg2
constantinpape Sep 8, 2026
83dd1a8
Drop deepseas too, and describe both maps per setting
constantinpape Sep 8, 2026
fbd983c
Spell out how the boundary channel enters the height map
constantinpape Sep 8, 2026
b4e43d8
Finalize AIS decoder reoptimization campaign
constantinpape Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions development/check_apg_tiled_refinement.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,10 @@ def main():
tiled = _build(model, decoder, args.device, is_tiled=True)
# One tile covering the image. Its outer block is clipped to the image, so the halo is irrelevant.
tiled.initialize(image, ndim=2, tile_shape=tuple(image.shape[:2]), halo=(0, 0))
tiled_plain = tiled.generate()
tiled_proposals = tiled.propose()
tiled_plain = tiled.select(tiled_proposals)
tiled._last_generation_stats = {}
tiled_refined = tiled.generate(**generate_kwargs)
tiled_refined = tiled.select(tiled_proposals, **generate_kwargs)
_report("tiled, one tile", tiled_refined, labels, tiled._last_generation_stats)
tiled.clear_state()

Expand All @@ -129,9 +130,11 @@ def main():

print(f"\nSmoke run with tiles {tuple(args.tile_shape)} and halo {tuple(args.halo)}:")
tiled.initialize(image, ndim=2, tile_shape=tuple(args.tile_shape), halo=tuple(args.halo))
# One round of prompting for both, as the screening harness does: only the selection differs.
proposals = tiled.propose()
for name, kwargs in (("tiled, no refinement", {}), ("tiled, refined", generate_kwargs)):
tiled._last_generation_stats = {}
segmentation = tiled.generate(**kwargs)
segmentation = tiled.select(proposals, **kwargs)
_report(name, segmentation, labels, tiled._last_generation_stats)
tiled.clear_state()

Expand Down
31 changes: 13 additions & 18 deletions finetuning/v2/evaluation/common.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
from pathlib import Path
import re
import ast
import csv
Expand Down Expand Up @@ -1708,36 +1709,29 @@ def resolve_params(overrides=None, ndim=2, model_type=None):
return params


def load_apg_overrides(path, dataset_name):
"""Read one APG configuration file and return its name and the overrides for one dataset.
def load_apg_overrides(path):
"""Read one APG configuration file and return its name and raw 2d parameter overrides.

The file has the format of the optimization benchmark: ``{"name": ..., "params_2d": {...},
"params_3d": {...}}``, with an optional ``params_dense``. Images use 'params_2d' and volumes use
'params_3d'. The dense-neuron EM volumes use 'params_dense' if the file has it. The function returns
the overrides unresolved, so that they can go on top of tuned parameters. `resolve_params` fills in
the defaults.
The file has the shape the optimization benchmark uses, ``{"name": ..., "params_2d": {...}}``
(``params_3d`` may be present and is ignored here). The overrides are returned unresolved, so
they can be layered over tuned parameters; `resolve_params` fills in the defaults.

Args:
path: The JSON configuration file.
dataset_name: The dataset that the overrides are for. It selects the section.

Returns:
The configuration name and the overrides, keyed as `generate` takes them.
The configuration name and the 2d overrides, keyed as `generate` takes them.
"""
import json

with open(path) as f:
config = json.load(f)
unknown_top_level = set(config) - {"name", "params_2d", "params_3d", "params_dense"}
unknown_top_level = set(config) - {"name", "params_2d", "params_3d"}
if unknown_top_level:
raise ValueError(f"Unknown configuration fields in '{path}': {sorted(unknown_top_level)}.")
if dataset_name in DATASETS_DENSE and "params_dense" in config:
section = "params_dense"
else:
section = "params_3d" if dataset_name in DATASETS_3D else "params_2d"
overrides = config.get(section, {})
overrides = config.get("params_2d", {})
if not isinstance(overrides, dict):
raise TypeError(f"'{section}' in '{path}' must be an object.")
raise TypeError(f"'params_2d' in '{path}' must be an object.")
unknown = set(overrides) - set(GENERATE_PARAM_KEYS)
if unknown:
raise ValueError(f"Unknown APG parameters in '{path}': {sorted(unknown)}.")
Expand Down Expand Up @@ -2023,7 +2017,7 @@ def predict_unisam2(model, raw, ndim, device, normalization=None, devices=None):


def postprocess_unisam2(out, dataset_name, model_type, params=None):
"""Turn a (4, *spatial) prediction into an instance segmentation.
"""Turn a (4, *spatial) prediction (or (5, *spatial) with a contact channel) into an instance segmentation.

EM datasets use the dense (multicut) mode, all others the sparse (flow) mode. 'params' overrides
the postprocessing defaults, e.g. with the best combination found by grid_search_automatic_cells.
Expand All @@ -2039,7 +2033,8 @@ def postprocess_unisam2(out, dataset_name, model_type, params=None):
seg = run_multicut(boundary_map, distances, model_type=model_type, **params)
else:
spacing = DATASET_SPACING.get(dataset_name, None)
seg = flow_instance_segmentation(fg, out[1:], model_type=model_type, spacing=spacing, **params)
contact = {"contact": out[4]} if out.shape[0] > 4 else {}
seg = flow_instance_segmentation(fg, out[1:4], model_type=model_type, spacing=spacing, **contact, **params)
return seg.astype("uint32")


Expand Down
103 changes: 80 additions & 23 deletions finetuning/v2/evaluation/evaluate_automatic_segmentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,51 @@
import torch

from common import (
DATA_ROOT, DATASETS_2D, DATASETS_3D, DATASET_SPACING, MODEL_TYPES, MODES, VOLUME_SPEED_OPTIONS, build_model,
check_data_download, evaluate_samples, has_val_split, load_apg_overrides, postprocess_unisam2, predict_unisam2,
read_tuned_params, resolve_checkpoint_identity,
DATA_ROOT, DATASETS_2D, DATASETS_3D, DATASETS_DENSE, DATASET_SPACING, GT_MIN_SIZE_2D, MODEL_TYPES, MODES,
VOLUME_SPEED_OPTIONS, build_model, check_data_download, drop_severed_objects, genuine_misses,
has_val_split, load_apg_overrides, load_data, n_samples, postprocess_unisam2, predict_unisam2,
read_tuned_params, resolve_checkpoint_identity, run_dataset_evaluation,
)


def segment(model, mode, raw, ndim, dataset_name, model_type, params, device, spacing=None, devices=None):
"""Segment one sample with the tuned parameters of a mode."""
"""Segment one sample with the tuned parameters of a mode.

For 'ais' the parameters may be the nested form ``{"sparse": {...}, "dense": {...}}`` of an AIS
benchmark configuration (see `load_ais_params`); the dataset's pipeline picks its own dict.
"""
if mode == "apg":
model.clear_state()
model.initialize(raw, ndim=ndim, **(VOLUME_SPEED_OPTIONS if ndim == 3 else {}))
volume_params = {"spacing": spacing} if ndim == 3 else {}
return model.generate(**{**volume_params, **params}).astype("uint32")

if set(params) & {"sparse", "dense"}:
params = params["dense" if dataset_name in DATASETS_DENSE else "sparse"]
prediction = predict_unisam2(model, raw, ndim=ndim, device=device, devices=devices)
return postprocess_unisam2(prediction, dataset_name, model_type=model_type, params=params)


def load_ais_params(path, model_type, ndim):
"""Read an AIS benchmark configuration and resolve its parameters for images or volumes.

The file has the shape `benchmark_ais_optimization.py` uses (``{"name", "mode", "params_2d",
"params_3d"}``); the result is ``{"sparse": {...}, "dense": {...}}`` with every post-processing
keyword resolved against the library defaults, so the evaluation runs exactly the benchmarked
configuration.
"""
import sys
from pathlib import Path
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "optimization"))
from benchmark_ais_optimization import load_config

name, _, params_2d, params_3d = load_config(Path(path), model_type)
return name, (params_3d if ndim == 3 else params_2d)


def run_evaluation(
model, mode, dataset_name, data_root, experiment_folder, model_type, params, device, limit,
crop_shape=None, checkpoint_id=None, devices=None, tuned=None, result_tag=None, config_name=None, sample_index=None,
model, mode, dataset_name, data_root, experiment_folder, model_type, params, device,
crop_shape=None, checkpoint_id=None, devices=None, tuned=None, result_tag=None, config_name=None,
):
"""Score the test split with the given parameters and write the result CSV.

Expand All @@ -72,7 +96,6 @@ def run_evaluation(
result_tag: Optional tag appended to the result file name, so that a run with explicit
parameter overrides does not collide with the plain evaluation.
config_name: The name of the configuration the overrides came from, stored in the results.
sample_index: The index of the only sample to score, for one array task. See `common.evaluate_samples`.

Returns:
The results as a DataFrame, or None while the rows of other samples are missing.
Expand All @@ -82,8 +105,6 @@ def run_evaluation(
tag = "tuned" if tuned else "default"
if result_tag:
tag = f"{tag}_{result_tag}"
if limit is not None:
tag = f"{tag}_n{limit}"
legacy_path = os.path.join(
experiment_folder, "results", f"{dataset_name}_micro_sam2_{model_type}_{mode}_{tag}.csv"
)
Expand All @@ -100,16 +121,40 @@ def run_evaluation(

ndim = 3 if dataset_name in DATASETS_3D else 2
spacing = DATASET_SPACING.get(dataset_name)
extra_columns = {"parameters": json.dumps(params, sort_keys=True, default=str) if params else "default"}
border_min_size = GT_MIN_SIZE_2D.get(dataset_name, 0) if ndim == 2 else 0
total = n_samples(dataset_name, data_root)
samples = load_data(dataset_name, data_root, ndim, crop_shape=crop_shape)

all_gt, all_seg, misses = [], [], []
for raw, labels, valid_roi in tqdm(samples, total=total, desc=f"{mode}-{model_type}"):
if labels.max() == 0: # Nothing to score without ground-truth.
continue
seg = segment(
model, mode, raw, ndim, dataset_name, model_type, params or {}, device, spacing=spacing,
devices=devices,
)
if valid_roi is not None:
seg[~valid_roi] = 0
if ndim == 2:
# The ground truth has no severed objects either, so predicting one is not a false positive.
seg = drop_severed_objects(seg, border_min_size)
else:
misses.append(genuine_misses(labels, seg))
all_gt.append(labels)
all_seg.append(seg)

os.makedirs(os.path.dirname(save_path), exist_ok=True)
results = run_dataset_evaluation(all_gt, all_seg, dataset_name, save_path)
if misses:
# The aggregate metric hides which objects went missing.
results["unmatched"] = sum(count[0] for count in misses)
results["genuine_misses"] = sum(count[1] for count in misses)
results["parameters"] = json.dumps(params, sort_keys=True, default=str) if params else "default"
if config_name is not None:
extra_columns["config_name"] = config_name
return evaluate_samples(
lambda raw: segment(
model, mode, raw, ndim, dataset_name, model_type, params or {}, device, spacing=spacing, devices=devices,
),
dataset_name, data_root, save_path, desc=f"{mode}-{model_type}", limit=limit, crop_shape=crop_shape,
sample_index=sample_index, extra_columns=extra_columns,
)
results["config_name"] = config_name
results.to_csv(save_path, index=False)
print(results)
return results


def main():
Expand Down Expand Up @@ -138,18 +183,25 @@ def main():
parser.add_argument("--devices", nargs="*", default=None, help="Inference devices. All visible GPUs by default.")
parser.add_argument(
"--apg_params", type=str, default=None,
help="APG only. A JSON configuration in the benchmark format. Its section for the dataset ('params_2d', "
"'params_3d' or 'params_dense') overrides the tuned parameters, or the defaults with --skip_tuning.",
help="APG only. A benchmark-style JSON configuration whose 'params_2d' are layered over the tuned "
"parameters (or the defaults with --skip_tuning).",
)
parser.add_argument(
"--ais_params", type=str, default=None,
help="AIS only. An AIS benchmark configuration ('params_2d' / 'params_3d', flat or "
"{'sparse', 'dense'}) whose resolved post-processing parameters replace the tuned ones.",
)
parser.add_argument(
"--result_tag", type=str, default=None,
help="Tag appended to the result file name. Defaults to the --apg_params configuration name.",
help="Tag appended to the result file name. Defaults to the --apg_params / --ais_params configuration name.",
)
args = parser.parse_args()

check_data_download(args.dataset_name, args.input_path)
if args.apg_params is not None and args.mode != "apg":
parser.error("--apg_params applies to --mode apg only.")
if args.ais_params is not None and args.mode != "ais":
parser.error("--ais_params applies to --mode ais only.")

print("Device:", torch.cuda.get_device_name() if torch.cuda.is_available() else "CPU")
device = "cuda" if torch.cuda.is_available() else "cpu"
Expand Down Expand Up @@ -190,16 +242,21 @@ def main():

config_name, result_tag = None, args.result_tag
if args.apg_params is not None:
config_name, overrides = load_apg_overrides(args.apg_params, args.dataset_name)
config_name, overrides = load_apg_overrides(args.apg_params)
params = {**(params or {}), **overrides}
if result_tag is None:
result_tag = config_name
if args.ais_params is not None:
# The configuration is complete (every keyword resolved), so it replaces rather than layers.
config_name, params = load_ais_params(args.ais_params, args.model_type, ndim)
tuned = False
if result_tag is None:
result_tag = config_name

run_evaluation(
model, args.mode, args.dataset_name, args.input_path, args.experiment_folder, args.model_type,
params, device, crop_shape=crop_shape, checkpoint_id=checkpoint_id,
devices=args.devices or None, tuned=tuned, result_tag=result_tag, config_name=config_name,
limit=args.n_samples, sample_index=args.sample_index,
)


Expand Down
Loading
Loading