Skip to content

Commit 9d975ff

Browse files
committed
Separate mixed operations with Curation vs Bookmark concerns
1 parent 6f004dc commit 9d975ff

6 files changed

Lines changed: 360 additions & 163 deletions

File tree

‎src/main/java/sc/fiji/snt/CurationManager.java‎

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public class CurationManager implements PlausibilityMonitor.WarningListener {
9898
private JCheckBox terminalNearAncestorCheckbox;
9999
private JCheckBox radiusJumpsCheckbox;
100100
private JCheckBox radiusMonoCheckbox;
101+
private JCheckBox invalidRadiusCheckbox;
101102
private JCheckBox zExtentCheckbox;
102103
private JCheckBox uncertainTerminalCheckbox;
103104
private JCheckBox intensityValleyCheckbox;
@@ -817,8 +818,8 @@ private JPanel buildOnDemandParamsPanel() {
817818
final int savedLeft = c.insets.left;
818819
c.insets.left += sectionChildIndent(onDemandHeaderCheckbox);
819820

820-
// Path overlap
821-
final PlausibilityCheck.PathOverlap overlapCheck = monitor.getDeepCheck(PlausibilityCheck.PathOverlap.class);
821+
// Crossovers
822+
final PlausibilityCheck.Crossovers overlapCheck = monitor.getDeepCheck(PlausibilityCheck.Crossovers.class);
822823
// On-demand check UI controls
823824
overlapCheckbox = new JCheckBox("Cross-over detection: max proximity", overlapCheck != null && overlapCheck.isEnabled());
824825
overlapCheckbox.setToolTipText("Detects regions where distinct paths run suspiciously close, suggesting duplicate tracing");
@@ -829,7 +830,7 @@ private JPanel buildOnDemandParamsPanel() {
829830
if (overlapCheck != null) overlapCheck.setProximityUm((Double) overlapSpinner.getValue());
830831
});
831832
final JButton overlapHist = (overlapCheck == null) ? null
832-
: CurationHistograms.button("Path overlap",
833+
: CurationHistograms.button("Crossovers",
833834
paths -> monitor.measure(overlapCheck, paths),
834835
this::currentPaths,
835836
() -> ((Number) overlapSpinner.getValue()).doubleValue(),
@@ -935,6 +936,15 @@ private JPanel buildOnDemandParamsPanel() {
935936
CurationHistograms.Side.RIGHT_FLAGGED, p);
936937
addCheckRow(p, c, radiusMonoCheckbox, spinnerWithHistogram(radiusMonoSpinner, radiusMonoHist));
937938

939+
// Invalid radii (zero/NaN) -- flag-only, no threshold
940+
final PlausibilityCheck.InvalidRadius invalidRadiusCheck =
941+
monitor.getDeepCheck(PlausibilityCheck.InvalidRadius.class);
942+
invalidRadiusCheckbox = new JCheckBox("Invalid thickness: flag",
943+
invalidRadiusCheck != null && invalidRadiusCheck.isEnabled());
944+
invalidRadiusCheckbox.setToolTipText("Flags paths containing nodes with zero, negative, or NaN radius (typically un-fitted nodes)");
945+
wireCheckbox(invalidRadiusCheckbox, null, invalidRadiusCheck);
946+
addCheckRow(p, c, invalidRadiusCheckbox, null);
947+
938948
// Image signal quality
939949
final PlausibilityCheck.SignalQuality signalCheck = monitor.getDeepCheck(PlausibilityCheck.SignalQuality.class);
940950
signalQualityCheckbox = new JCheckBox("Path signal quality: min contrast",
@@ -1050,7 +1060,7 @@ private JPanel buildOnDemandParamsPanel() {
10501060
// Collect and wire section-level toggling
10511061
onDemandCheckboxes = List.of(overlapCheckbox, bundledPathsCheckbox,
10521062
terminalNearAncestorCheckbox, zExtentCheckbox,
1053-
radiusJumpsCheckbox, radiusMonoCheckbox,
1063+
radiusJumpsCheckbox, radiusMonoCheckbox, invalidRadiusCheckbox,
10541064
signalQualityCheckbox, uncertainTerminalCheckbox,
10551065
intensityValleyCheckbox);
10561066
wireSectionHeader(onDemandHeaderCheckbox, onDemandCheckboxes);
@@ -1542,11 +1552,11 @@ private void syncUIFromMonitor() {
15421552
constantRadiiCheckbox.setSelected(cr.isEnabled());
15431553
}
15441554
// Deep checks
1545-
final PlausibilityCheck.PathOverlap po = monitor.getDeepCheck(PlausibilityCheck.PathOverlap.class);
1546-
if (po != null) {
1547-
overlapSpinner.setValue(po.getProximityUm());
1548-
overlapCheckbox.setSelected(po.isEnabled());
1549-
overlapSpinner.setEnabled(po.isEnabled());
1555+
final PlausibilityCheck.Crossovers co = monitor.getDeepCheck(PlausibilityCheck.Crossovers.class);
1556+
if (co != null) {
1557+
overlapSpinner.setValue(co.getProximityUm());
1558+
overlapCheckbox.setSelected(co.isEnabled());
1559+
overlapSpinner.setEnabled(co.isEnabled());
15501560
}
15511561
final PlausibilityCheck.RadiusJumps rj = monitor.getDeepCheck(PlausibilityCheck.RadiusJumps.class);
15521562
if (rj != null) {
@@ -1560,6 +1570,10 @@ private void syncUIFromMonitor() {
15601570
radiusMonoCheckbox.setSelected(rm.isEnabled());
15611571
radiusMonoSpinner.setEnabled(rm.isEnabled());
15621572
}
1573+
final PlausibilityCheck.InvalidRadius ir = monitor.getDeepCheck(PlausibilityCheck.InvalidRadius.class);
1574+
if (ir != null && invalidRadiusCheckbox != null) {
1575+
invalidRadiusCheckbox.setSelected(ir.isEnabled());
1576+
}
15631577
final PlausibilityCheck.SignalQuality sq =
15641578
monitor.getDeepCheck(PlausibilityCheck.SignalQuality.class);
15651579
if (signalQualityCheckbox != null && sq != null) {
@@ -1942,12 +1956,13 @@ private static String getDocAnchor(final String checkName) {
19421956
case "Tortuosity consistency" -> "#tortuosity-max-mismatch-at-fork";
19431957
case "Constant radii" -> "#uniform-thickness-flag";
19441958
// On-demand (deep) checks
1945-
case "Path overlap" -> "#cross-over-detection-max-proximity";
1959+
case "Crossovers" -> "#cross-over-detection-max-proximity";
19461960
case "Bundled paths" -> "#bundle-detection-max-angle-";
19471961
case "Missed-fork candidate" -> "#missed-fork-candidate-max-proximity";
19481962
case "Z-extent ratio" -> "#z-extent-min-ratio";
19491963
case "Thickness jumps" -> "#thickness-jumps-max-ratio";
19501964
case "Thickness inversions" -> "#thickness-inversions-min-run-length";
1965+
case "Invalid radius" -> "#invalid-thickness-flag";
19511966
case "Path signal quality" -> "#path-signal-quality-min-contrast";
19521967
case "Tip signal quality" -> "#tip-signal-quality-min-contrast";
19531968
case "Path signal quality dips" -> "#path-signal-quality-dips-min-drop";

‎src/main/java/sc/fiji/snt/Path.java‎

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ public double getLength() {
586586
if (nodes.size() < 2) return 0;
587587

588588
double totalLength = 0;
589-
PathNode prevNode = nodes.get(0);
589+
PathNode prevNode = nodes.getFirst();
590590
for (int i = 1; i < nodes.size(); ++i) {
591591
final PathNode currentNode = nodes.get(i);
592592
final double xdiff = currentNode.x - prevNode.x;
@@ -947,7 +947,7 @@ public double getExtensionAngle3D(final Vector3d referenceVector) {
947947
// Calculate angle between direction and reference vector
948948
final double dotProduct = direction.dot(referenceVector);
949949
// Clamp to [-1, 1] to handle floating point precision issues
950-
final double clampedDot = Math.max(-1.0, Math.min(1.0, dotProduct));
950+
final double clampedDot = Math.clamp(dotProduct, -1.0, 1.0);
951951
final double angle = Math.acos(clampedDot);
952952

953953
return Math.toDegrees(angle);
@@ -1254,9 +1254,29 @@ protected PathNode getNode(String filter) {
12541254
.max(java.util.Comparator.comparingDouble(Path.PathNode::getZ))
12551255
.orElse(null);
12561256
}
1257-
default -> {
1258-
throw new IllegalArgumentException("unsupported filter");
1257+
case "min angle", "max angle" -> {
1258+
// Walk interior nodes (index >= 3 per Path#getAngle) tracking the chosen extremum.
1259+
// Returns null if no interior angle can be computed (size < 4 or all NaN).
1260+
final boolean wantMin = "min angle".equals(filter.toLowerCase().trim());
1261+
int bestIdx = -1;
1262+
double best = wantMin ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY;
1263+
for (int i = 3; i < size(); i++) {
1264+
final double a = getAngle(i);
1265+
if (Double.isNaN(a)) continue;
1266+
if (wantMin ? a < best : a > best) { best = a; bestIdx = i; }
1267+
}
1268+
return (bestIdx < 0) ? null : getNodeWithoutChecks(bestIdx);
1269+
}
1270+
case "min value", "max value" -> {
1271+
// Requires intensity values to have been assigned via PathProfiler#assignValues
1272+
if (!hasNodeValues()) return null;
1273+
final boolean wantMin = "min value".equals(filter.toLowerCase().trim());
1274+
final java.util.Comparator<Path.PathNode> byV = java.util.Comparator.comparingDouble(n -> n.v);
1275+
return wantMin
1276+
? nodes.stream().filter(n -> !Double.isNaN(n.v)).min(byV).orElse(null)
1277+
: nodes.stream().filter(n -> !Double.isNaN(n.v)).max(byV).orElse(null);
12591278
}
1279+
default -> throw new IllegalArgumentException("unsupported filter");
12601280
}
12611281
}
12621282

@@ -3254,7 +3274,7 @@ private Map<Integer, Double> interpolateAndApply(final List<Integer> validIndice
32543274
}
32553275

32563276
// Clamp to valid range
3257-
r = Math.max(clampMin, Math.min(clampMax, r));
3277+
r = Math.clamp(r, clampMin, clampMax);
32583278

32593279
result.put(nodeIdx, r);
32603280
if (apply) {
@@ -3884,7 +3904,7 @@ public double getApproximatedVolume() {
38843904

38853905
double totalVolume = 0;
38863906
// Cache node references to avoid repeated method calls
3887-
PathNode prevNode = nodes.get(0);
3907+
PathNode prevNode = nodes.getFirst();
38883908
for (int i = 1; i < nodes.size(); ++i) {
38893909
final PathNode currentNode = nodes.get(i);
38903910

@@ -3924,7 +3944,7 @@ public double getApproximatedSurface() {
39243944

39253945
double totalSurface = 0;
39263946
// Cache node references to avoid repeated method calls
3927-
PathNode prevNode = nodes.get(0);
3947+
PathNode prevNode = nodes.getFirst();
39283948
for (int i = 1; i < nodes.size(); ++i) {
39293949
final PathNode currentNode = nodes.get(i);
39303950

0 commit comments

Comments
 (0)