From c35e98acbe5b8d786cff3e33ef706dd47284f9de Mon Sep 17 00:00:00 2001 From: Lollum89 Date: Wed, 15 May 2024 19:21:20 +0200 Subject: [PATCH 1/4] Improved kalman tracker --- .vscode/settings.json | 3 + pom copy.xml | 280 ++++++++++ pom.xml | 8 +- ...anelAdvancedKalmanTrackerSettingsMain.java | 127 ++++- .../tracker/KalmanTrackerConfigPanel.java | 294 +++++----- .../trackmate/tracking/TrackerKeys.java | 5 + .../kalman/AdvancedKalmanTracker.java | 5 +- .../kalman/AdvancedKalmanTrackerFactory.java | 439 ++++++++------- .../tracking/kalman/EX_KalmanTracker | 524 ++++++++++++++++++ .../tracking/kalman/KalmanTracker.java | 106 ++-- .../tracking/kalman/KalmanTrackerFactory.java | 365 ++++++------ .../kalman/KalmanTrackerInteractiveTest.java | 2 +- .../kalman/KalmanTrackerInteractiveTest2.java | 2 +- .../kalman/KalmanTrackerInteractiveTest3.java | 2 +- 14 files changed, 1544 insertions(+), 618 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 pom copy.xml create mode 100644 src/main/java/fiji/plugin/trackmate/tracking/kalman/EX_KalmanTracker diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..c5f3f6b9c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/pom copy.xml b/pom copy.xml new file mode 100644 index 000000000..3e8b63877 --- /dev/null +++ b/pom copy.xml @@ -0,0 +1,280 @@ + + + 4.0.0 + + + org.scijava + pom-scijava + 37.0.0 + + + + sc.fiji + TrackMate + 7.12.2-SNAPSHOT + + TrackMate + TrackMate plugin for Fiji. + https://imagej.net/plugins/trackmate + 2010 + + TrackMate + https://github.com/trackmate-sc + + + + GNU General Public License v3+ + https://www.gnu.org/licenses/gpl.html + repo + + + + + + tinevez + Jean-Yves Tinevez + https://imagej.net/people/tinevez + + founder + lead + developer + debugger + reviewer + support + maintainer + + + + ctrueden + Curtis Rueden + https://imagej.net/people/ctrueden + + maintainer + + + + + + Nick Perry + https://imagej.net/people/nickp + founder + nickp + + + Johannes Schindelin + https://imagej.net/people/dscho + founder + dscho + + Gary Baker + + Albert Cardona + https://imagej.net/people/acardona + acardona + + + Barry DeZonia + https://imagej.net/people/bdezonia + bdezonia + + + Stefan Helfrich + https://imagej.net/people/stelfrich + stelfrich + + + Mark Hiner + https://imagej.net/people/hinerm + hinerm + + + Hadrien Mary + https://imagej.net/people/hadim + hadim + + + Tobias Pietzsch + https://imagej.net/peoplel/pietzsch + tpietzsch + + Barak Naveh + Chen Ye + + Robert Haase + https://imagej.net/people/haesleinhuepf + haesleinhuepf + + + Jan Eglinger + https://imagej.net/people/imagejan + imagejan + + + + + + Image.sc Forum + https://forum.image.sc/tag/trackmate + + + + + scm:git:https://github.com/trackmate-sc/TrackMate + scm:git:git@github.com:trackmate-sc/TrackMate + HEAD + https://github.com/trackmate-sc/TrackMate + + + GitHub Issues + https://github.com/trackmate-sc/TrackMate/issues + + + GitHub Actions + https://github.com/trackmate-sc/TrackMate + + + + fiji.plugin.trackmate + gpl_v3 + TrackMate developers. + TrackMate: your buddy for everyday tracking. + + + sign,deploy-to-scijava + + + imglib2 + + 2.5.2 + 0.11.1 + 6.1.0 + + + + + + sc.fiji + fiji-lib + + + + + net.imagej + ij + + + net.imagej + imagej-common + + + + + net.imglib2 + imglib2 + + + net.imglib2 + imglib2-algorithm + + + net.imglib2 + imglib2-algorithm-gpl + + + net.imglib2 + imglib2-ij + + + net.imglib2 + imglib2-realtransform + + + net.imglib2 + imglib2-roi + + + + + org.scijava + scijava-common + + + org.scijava + scijava-listeners + + + + + com.github.vlsi.mxgraph + jgraphx + + + com.google.code.gson + gson + + + com.itextpdf + itextpdf + + + com.opencsv + opencsv + + + gov.nist.math + jama + + + net.sf.trove4j + trove4j + + + org.apache.xmlgraphics + batik-dom + + + org.apache.xmlgraphics + batik-svggen + + + org.jdom + jdom2 + + + org.jfree + jfreechart + + + org.jgrapht + jgrapht-core + + + org.apache.commons + commons-math3 + + + math.geom2d + javaGeom + ${javaGeom.version} + + + org.drjekyll + fontchooser + ${fontchooser.version} + + + + + junit + junit + test + + + + + + scijava.public + https://maven.scijava.org/content/groups/public + + + diff --git a/pom.xml b/pom.xml index 3e8b63877..8976dcc07 100644 --- a/pom.xml +++ b/pom.xml @@ -10,11 +10,11 @@ sc.fiji - TrackMate - 7.12.2-SNAPSHOT + LP_TrackMate + Test_1 - TrackMate - TrackMate plugin for Fiji. + LP_TrackMate + LP Modified TrackMate plugin for Fiji. https://imagej.net/plugins/trackmate 2010 diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelAdvancedKalmanTrackerSettingsMain.java b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelAdvancedKalmanTrackerSettingsMain.java index 3e1da6e82..eb721e124 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelAdvancedKalmanTrackerSettingsMain.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelAdvancedKalmanTrackerSettingsMain.java @@ -33,6 +33,8 @@ import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_ALLOW_TRACK_SPLITTING; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_GAP_CLOSING_MAX_FRAME_GAP; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_KALMAN_SEARCH_RADIUS; +import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_EXPECTED_MOVEMENT; +import static fiji.plugin.trackmate.tracking.TrackerKeys.DEFAULT_EXPECTED_MOVEMENT; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_LINKING_FEATURE_PENALTIES; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_LINKING_MAX_DISTANCE; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_MERGING_FEATURE_PENALTIES; @@ -62,6 +64,14 @@ import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.tracking.jaqaman.LAPUtils; +// Add at the beginning with other import statements +import javax.swing.JTextField; +import javax.swing.JOptionPane; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; + + + public class JPanelAdvancedKalmanTrackerSettingsMain extends javax.swing.JPanel { @@ -109,6 +119,10 @@ public class JPanelAdvancedKalmanTrackerSettingsMain extends javax.swing.JPanel private final JLabel lbl16; + // Add these fields in the class + private final JLabel lblExpectedMovement; + private final JTextField txtfldExpectedMovement; + public JPanelAdvancedKalmanTrackerSettingsMain( final String trackerName, final String spaceUnits, final Collection< String > features, final Map< String, String > featureNames ) { final DecimalFormat decimalFormat = new DecimalFormat( "0.0" ); @@ -174,6 +188,29 @@ public JPanelAdvancedKalmanTrackerSettingsMain( final String trackerName, final lblSearchRadiusUnits.setFont( SMALL_FONT ); lblSearchRadiusUnits.setText( spaceUnits ); + ycur++; + lblExpectedMovement = new JLabel(); + this.add(lblExpectedMovement, new GridBagConstraints(0, ycur, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(0, 10, 0, 10), 0, 0)); + lblExpectedMovement.setText("Expected movement (X,Y,Z):"); + lblExpectedMovement.setFont(SMALL_FONT); + + txtfldExpectedMovement = new JTextField("0,0,0"); + this.add(txtfldExpectedMovement, new GridBagConstraints(1, ycur, 2, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0)); + txtfldExpectedMovement.setFont(SMALL_FONT); + txtfldSearchRadius.setSize( TEXTFIELD_DIMENSION ); + txtfldExpectedMovement.setHorizontalAlignment(JTextField.CENTER); + + // Adding input validation + txtfldExpectedMovement.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + if (!validateExpectedMovementInput(txtfldExpectedMovement.getText())) { + JOptionPane.showMessageDialog(null, "Invalid input. Please enter a comma-separated list of three numbers (e.g., '1.0,0.0,0.0').", "Invalid Input", JOptionPane.ERROR_MESSAGE); + //txtfldExpectedMovement.setText("0,0,0"); + } + } + }); + ycur++; final JLabel lbl3c = new JLabel(); this.add( lbl3c, new GridBagConstraints( 0, ycur, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets( 0, 10, 0, 10 ), 0, 0 ) ); @@ -331,35 +368,38 @@ public JPanelAdvancedKalmanTrackerSettingsMain( final String trackerName, final * PUBLIC METHODS */ - @SuppressWarnings( "unchecked" ) - void echoSettings( final Map< String, Object > settings ) - { - txtfldInitialSearchRadius.setValue( settings.get( KEY_LINKING_MAX_DISTANCE ) ); - if ( settings.get( KEY_KALMAN_SEARCH_RADIUS ) == null ) - txtfldSearchRadius.setValue( DEFAULT_KALMAN_SEARCH_RADIUS ); - else - txtfldSearchRadius.setValue( settings.get( KEY_KALMAN_SEARCH_RADIUS ) ); - txtfldMaxFrameGap.setValue( settings.get( KEY_GAP_CLOSING_MAX_FRAME_GAP ) ); - panelKalmanFeatures.setSelectedFeaturePenalties( ( Map< String, Double > ) settings.get( KEY_LINKING_FEATURE_PENALTIES ) ); - - chkboxAllowSplitting.setSelected( ( Boolean ) settings.get( KEY_ALLOW_TRACK_SPLITTING ) ); - txtfldSplittingMaxDistance.setValue( settings.get( KEY_SPLITTING_MAX_DISTANCE ) ); - panelSplittingFeatures.setSelectedFeaturePenalties( ( Map< String, Double > ) settings.get( KEY_SPLITTING_FEATURE_PENALTIES ) ); - - chkboxAllowMerging.setSelected( ( Boolean ) settings.get( KEY_ALLOW_TRACK_MERGING ) ); - txtfldMergingMaxDistance.setValue( settings.get( KEY_MERGING_MAX_DISTANCE ) ); - panelMergingFeatures.setSelectedFeaturePenalties( ( Map< String, Double > ) settings.get( KEY_MERGING_FEATURE_PENALTIES ) ); - - setEnabled( new Component[] { - lbl10, txtfldSplittingMaxDistance, lblSplittingMaxDistanceUnit, - lbl15, scrpneSplittingFeatures, panelSplittingFeatures }, - chkboxAllowSplitting.isSelected() ); - - setEnabled( new Component[] { - lbl13, txtfldMergingMaxDistance, lblMergingMaxDistanceUnit, - lbl16, scrpneMergingFeatures, panelMergingFeatures }, - chkboxAllowMerging.isSelected() ); - } + @SuppressWarnings("unchecked") + void echoSettings(final Map settings) { + txtfldInitialSearchRadius.setValue(settings.get(KEY_LINKING_MAX_DISTANCE)); + if (settings.get(KEY_KALMAN_SEARCH_RADIUS) == null) + txtfldSearchRadius.setValue(DEFAULT_KALMAN_SEARCH_RADIUS); + else + txtfldSearchRadius.setValue(settings.get(KEY_KALMAN_SEARCH_RADIUS)); + txtfldMaxFrameGap.setValue(settings.get(KEY_GAP_CLOSING_MAX_FRAME_GAP)); + panelKalmanFeatures.setSelectedFeaturePenalties((Map) settings.get(KEY_LINKING_FEATURE_PENALTIES)); + + chkboxAllowSplitting.setSelected((Boolean) settings.get(KEY_ALLOW_TRACK_SPLITTING)); + txtfldSplittingMaxDistance.setValue(settings.get(KEY_SPLITTING_MAX_DISTANCE)); + panelSplittingFeatures.setSelectedFeaturePenalties((Map) settings.get(KEY_SPLITTING_FEATURE_PENALTIES)); + + chkboxAllowMerging.setSelected((Boolean) settings.get(KEY_ALLOW_TRACK_MERGING)); + txtfldMergingMaxDistance.setValue(settings.get(KEY_MERGING_MAX_DISTANCE)); + panelMergingFeatures.setSelectedFeaturePenalties((Map) settings.get(KEY_MERGING_FEATURE_PENALTIES)); + + // Echo expected movement + double[] expectedMovement = (double[]) settings.get(KEY_EXPECTED_MOVEMENT); + txtfldExpectedMovement.setText(String.format("%.1f,%.1f,%.1f", expectedMovement[0], expectedMovement[1], expectedMovement[2])); + + setEnabled(new Component[]{ + lbl10, txtfldSplittingMaxDistance, lblSplittingMaxDistanceUnit, + lbl15, scrpneSplittingFeatures, panelSplittingFeatures}, + chkboxAllowSplitting.isSelected()); + + setEnabled(new Component[]{ + lbl13, txtfldMergingMaxDistance, lblMergingMaxDistanceUnit, + lbl16, scrpneMergingFeatures, panelMergingFeatures}, + chkboxAllowMerging.isSelected()); + } /** * @return a new settings {@link Map} with values taken from this panel. @@ -383,6 +423,9 @@ public Map< String, Object > getSettings() settings.put( KEY_MERGING_MAX_DISTANCE, ( ( Number ) txtfldMergingMaxDistance.getValue() ).doubleValue() ); settings.put( KEY_MERGING_FEATURE_PENALTIES, panelMergingFeatures.getFeaturePenalties() ); + // Save expected movement + settings.put(KEY_EXPECTED_MOVEMENT, parseExpectedMovement(txtfldExpectedMovement.getText())); + return settings; } @@ -392,6 +435,7 @@ public static final Map< String, Object > getDefaultKalmanSettingsMap() settings.put( KEY_LINKING_MAX_DISTANCE, DEFAULT_LINKING_MAX_DISTANCE ); settings.put( KEY_KALMAN_SEARCH_RADIUS, DEFAULT_KALMAN_SEARCH_RADIUS ); settings.put( KEY_LINKING_FEATURE_PENALTIES, new HashMap<>( DEFAULT_LINKING_FEATURE_PENALTIES ) ); + settings.put( KEY_EXPECTED_MOVEMENT, DEFAULT_EXPECTED_MOVEMENT); return settings; } @@ -399,6 +443,31 @@ public static final Map< String, Object > getDefaultKalmanSettingsMap() * PRIVATE METHODS */ + // Add this method in the class for input validation + private boolean validateExpectedMovementInput(String input) { + String[] parts = input.split(","); + if (parts.length != 3) return false; + try { + for (String part : parts) { + Double.parseDouble(part.trim()); + } + } catch (NumberFormatException e) { + return false; + } + return true; + } + + // Add this method to parse the expected movement from the text field + private double[] parseExpectedMovement(String text) { + String[] parts = text.split(","); + double[] result = new double[3]; + for (int i = 0; i < parts.length; i++) { + result[i] = Double.parseDouble(parts[i].trim()); + } + return result; + } + + private void setEnabled( final Component[] components, final boolean enable ) { for ( final Component component : components ) diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/KalmanTrackerConfigPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/KalmanTrackerConfigPanel.java index eb4c5ee3f..f597616c1 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/KalmanTrackerConfigPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/KalmanTrackerConfigPanel.java @@ -1,24 +1,3 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2024 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ package fiji.plugin.trackmate.gui.components.tracker; import static fiji.plugin.trackmate.gui.Fonts.BIG_FONT; @@ -27,129 +6,176 @@ import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_GAP_CLOSING_MAX_FRAME_GAP; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_LINKING_MAX_DISTANCE; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_KALMAN_SEARCH_RADIUS; +import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_EXPECTED_MOVEMENT; import java.awt.Font; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; import java.util.HashMap; import java.util.Map; import javax.swing.JFormattedTextField; import javax.swing.JLabel; +import javax.swing.JOptionPane; import javax.swing.SwingConstants; +import javax.swing.JTextField; import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.gui.components.ConfigurationPanel; -public class KalmanTrackerConfigPanel extends ConfigurationPanel -{ - private static final long serialVersionUID = 1L; - - private final JFormattedTextField tfInitSearchRadius; - - private final JFormattedTextField tfSearchRadius; - - private final JFormattedTextField tfMaxFrameGap; - - public KalmanTrackerConfigPanel( final String trackerName, final String infoText, final String spaceUnits ) - { - setLayout( null ); - - final JLabel lbl1 = new JLabel( "Settings for tracker:" ); - lbl1.setBounds( 6, 6, 288, 16 ); - lbl1.setFont( FONT ); - add( lbl1 ); - - final JLabel lblTrackerName = new JLabel( trackerName ); - lblTrackerName.setFont( BIG_FONT ); - lblTrackerName.setHorizontalAlignment( SwingConstants.CENTER ); - lblTrackerName.setBounds( 6, 34, 288, 32 ); - add( lblTrackerName ); - - final JLabel lblTrackerDescription = new JLabel( "" ); - lblTrackerDescription.setFont( FONT.deriveFont( Font.ITALIC ) ); - lblTrackerDescription.setVerticalAlignment( SwingConstants.TOP ); - lblTrackerDescription.setBounds( 6, 81, 288, 175 ); - lblTrackerDescription.setText( infoText - .replace( "
", "" ) - .replace( "

", "

" ) - .replace( "", "

" ) ); - add( lblTrackerDescription ); - - final JLabel lblInitSearchRadius = new JLabel( "Initial search radius:" ); - lblInitSearchRadius.setFont( FONT ); - lblInitSearchRadius.setBounds( 6, 348, 173, 16 ); - add( lblInitSearchRadius ); - - final JLabel lblSearchRadius = new JLabel( "Search radius:" ); - lblSearchRadius.setFont( FONT ); - lblSearchRadius.setBounds( 6, 376, 173, 16 ); - add( lblSearchRadius ); - - final JLabel lblMaxFrameGap = new JLabel( "Max frame gap:" ); - lblMaxFrameGap.setFont( FONT ); - lblMaxFrameGap.setBounds( 6, 404, 173, 16 ); - add( lblMaxFrameGap ); - - tfInitSearchRadius = new JFormattedTextField( 15. ); - tfInitSearchRadius.setHorizontalAlignment( SwingConstants.CENTER ); - tfInitSearchRadius.setFont( FONT ); - tfInitSearchRadius.setBounds( 167, 348, 60, 28 ); - add( tfInitSearchRadius ); - tfInitSearchRadius.setSize( TEXTFIELD_DIMENSION ); - - tfSearchRadius = new JFormattedTextField( 15. ); - tfSearchRadius.setHorizontalAlignment( SwingConstants.CENTER ); - tfSearchRadius.setFont( FONT ); - tfSearchRadius.setBounds( 167, 376, 60, 28 ); - add( tfSearchRadius ); - tfSearchRadius.setSize( TEXTFIELD_DIMENSION ); - - tfMaxFrameGap = new JFormattedTextField( 2 ); - tfMaxFrameGap.setHorizontalAlignment( SwingConstants.CENTER ); - tfMaxFrameGap.setFont( FONT ); - tfMaxFrameGap.setBounds( 167, 404, 60, 28 ); - add( tfMaxFrameGap ); - tfMaxFrameGap.setSize( TEXTFIELD_DIMENSION ); - - final JLabel lblSpaceUnits1 = new JLabel( spaceUnits ); - lblSpaceUnits1.setFont( FONT ); - lblSpaceUnits1.setBounds( 219, 348, 51, 16 ); - add( lblSpaceUnits1 ); - - final JLabel lblSpaceUnits2 = new JLabel( spaceUnits ); - lblSpaceUnits2.setFont( FONT ); - lblSpaceUnits2.setBounds( 219, 376, 51, 16 ); - add( lblSpaceUnits2 ); - - final JLabel lblFrameUnits = new JLabel( "frames" ); - lblFrameUnits.setFont( FONT ); - lblFrameUnits.setBounds( 219, 404, 51, 16 ); - add( lblFrameUnits ); - - // Select text-fields content on focus. - GuiUtils.selectAllOnFocus( tfInitSearchRadius ); - GuiUtils.selectAllOnFocus( tfMaxFrameGap ); - GuiUtils.selectAllOnFocus( tfSearchRadius ); - } - - @Override - public void setSettings( final Map< String, Object > settings ) - { - tfInitSearchRadius.setValue( settings.get( KEY_LINKING_MAX_DISTANCE ) ); - tfSearchRadius.setValue( settings.get( KEY_KALMAN_SEARCH_RADIUS ) ); - tfMaxFrameGap.setValue( settings.get( KEY_GAP_CLOSING_MAX_FRAME_GAP ) ); - } - - @Override - public Map< String, Object > getSettings() - { - final Map< String, Object > settings = new HashMap<>(); - settings.put( KEY_LINKING_MAX_DISTANCE, ( ( Number ) tfInitSearchRadius.getValue() ).doubleValue() ); - settings.put( KEY_KALMAN_SEARCH_RADIUS, ( ( Number ) tfSearchRadius.getValue() ).doubleValue() ); - settings.put( KEY_GAP_CLOSING_MAX_FRAME_GAP, ( ( Number ) tfMaxFrameGap.getValue() ).intValue() ); - return settings; - } - - @Override - public void clean() - {} +public class KalmanTrackerConfigPanel extends ConfigurationPanel { + private static final long serialVersionUID = 1L; + + private final JFormattedTextField tfInitSearchRadius; + private final JFormattedTextField tfSearchRadius; + private final JFormattedTextField tfMaxFrameGap; + private final JTextField tfExpectedMovement; // New text field for expected movement + + public KalmanTrackerConfigPanel(final String trackerName, final String infoText, final String spaceUnits) { + setLayout(null); + + final JLabel lbl1 = new JLabel("Settings for tracker:"); + lbl1.setBounds(6, 6, 288, 16); + lbl1.setFont(FONT); + add(lbl1); + + final JLabel lblTrackerName = new JLabel(trackerName); + lblTrackerName.setFont(BIG_FONT); + lblTrackerName.setHorizontalAlignment(SwingConstants.CENTER); + lblTrackerName.setBounds(6, 34, 288, 32); + add(lblTrackerName); + + final JLabel lblTrackerDescription = new JLabel(""); + lblTrackerDescription.setFont(FONT.deriveFont(Font.ITALIC)); + lblTrackerDescription.setVerticalAlignment(SwingConstants.TOP); + lblTrackerDescription.setBounds(6, 81, 288, 175); + lblTrackerDescription.setText(infoText + .replace("
", "") + .replace("

", "

") + .replace("", "

")); + add(lblTrackerDescription); + + final JLabel lblInitSearchRadius = new JLabel("Initial search radius:"); + lblInitSearchRadius.setFont(FONT); + lblInitSearchRadius.setBounds(6, 348, 173, 16); + add(lblInitSearchRadius); + + final JLabel lblSearchRadius = new JLabel("Search radius:"); + lblSearchRadius.setFont(FONT); + lblSearchRadius.setBounds(6, 376, 173, 16); + add(lblSearchRadius); + + final JLabel lblMaxFrameGap = new JLabel("Max frame gap:"); + lblMaxFrameGap.setFont(FONT); + lblMaxFrameGap.setBounds(6, 404, 173, 16); + add(lblMaxFrameGap); + + final JLabel lblExpectedMovement = new JLabel("Expected movement X,Y,Z:"); // New label + lblExpectedMovement.setFont(FONT); + lblExpectedMovement.setBounds(6, 432, 250, 16); // Adjust position as needed + add(lblExpectedMovement); + + tfInitSearchRadius = new JFormattedTextField(15.); + tfInitSearchRadius.setHorizontalAlignment(SwingConstants.CENTER); + tfInitSearchRadius.setFont(FONT); + tfInitSearchRadius.setBounds(167, 348, 60, 28); + add(tfInitSearchRadius); + tfInitSearchRadius.setSize(TEXTFIELD_DIMENSION); + + tfSearchRadius = new JFormattedTextField(15.); + tfSearchRadius.setHorizontalAlignment(SwingConstants.CENTER); + tfSearchRadius.setFont(FONT); + tfSearchRadius.setBounds(167, 376, 60, 28); + add(tfSearchRadius); + tfSearchRadius.setSize(TEXTFIELD_DIMENSION); + + tfMaxFrameGap = new JFormattedTextField(2); + tfMaxFrameGap.setHorizontalAlignment(SwingConstants.CENTER); + tfMaxFrameGap.setFont(FONT); + tfMaxFrameGap.setBounds(167, 404, 60, 28); + add(tfMaxFrameGap); + tfMaxFrameGap.setSize(TEXTFIELD_DIMENSION); + + tfExpectedMovement = new JTextField("0,0,0"); // Initialize with default value + tfExpectedMovement.setHorizontalAlignment(SwingConstants.CENTER); + tfExpectedMovement.setFont(FONT); + tfExpectedMovement.setBounds(167, 432, 100, 28); // Adjust position as needed + add(tfExpectedMovement); + + // Adding input validation + tfExpectedMovement.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + if (!validateExpectedMovementInput(tfExpectedMovement.getText())) { + JOptionPane.showMessageDialog(null, "Invalid input. Please enter a comma-separated list of three numbers (e.g., '1.0,0.0,0.0').", "Invalid Input", JOptionPane.ERROR_MESSAGE); + tfExpectedMovement.setText("0,0,0"); + } + } + }); + + final JLabel lblSpaceUnits1 = new JLabel(spaceUnits); + lblSpaceUnits1.setFont(FONT); + lblSpaceUnits1.setBounds(219, 348, 51, 16); + add(lblSpaceUnits1); + + final JLabel lblSpaceUnits2 = new JLabel(spaceUnits); + lblSpaceUnits2.setFont(FONT); + lblSpaceUnits2.setBounds(219, 376, 51, 16); + add(lblSpaceUnits2); + + final JLabel lblFrameUnits = new JLabel("frames"); + lblFrameUnits.setFont(FONT); + lblFrameUnits.setBounds(219, 404, 51, 16); + add(lblFrameUnits); + + // Select text-fields content on focus. + GuiUtils.selectAllOnFocus(tfInitSearchRadius); + GuiUtils.selectAllOnFocus(tfMaxFrameGap); + GuiUtils.selectAllOnFocus(tfSearchRadius); + GuiUtils.selectAllOnFocus(tfExpectedMovement); + } + + private boolean validateExpectedMovementInput(String input) { + String[] parts = input.split(","); + if (parts.length != 3) + return false; + try { + for (String part : parts) { + Double.parseDouble(part.trim()); + } + } catch (NumberFormatException e) { + return false; + } + return true; + } + + @Override + public void setSettings(final Map settings) { + tfInitSearchRadius.setValue(settings.get(KEY_LINKING_MAX_DISTANCE)); + tfSearchRadius.setValue(settings.get(KEY_KALMAN_SEARCH_RADIUS)); + tfMaxFrameGap.setValue(settings.get(KEY_GAP_CLOSING_MAX_FRAME_GAP)); + double[] expectedMovementArray = (double[]) settings.get(KEY_EXPECTED_MOVEMENT); + tfExpectedMovement.setText(expectedMovementArray[0] + "," + expectedMovementArray[1] + "," + expectedMovementArray[2]); + } + + @Override + public Map getSettings() { + final Map settings = new HashMap<>(); + settings.put(KEY_LINKING_MAX_DISTANCE, ((Number) tfInitSearchRadius.getValue()).doubleValue()); + settings.put(KEY_KALMAN_SEARCH_RADIUS, ((Number) tfSearchRadius.getValue()).doubleValue()); + settings.put(KEY_GAP_CLOSING_MAX_FRAME_GAP, ((Number) tfMaxFrameGap.getValue()).intValue()); + + String[] parts = tfExpectedMovement.getText().split(","); + double[] expectedMovement = new double[3]; + for (int i = 0; i < parts.length; i++) { + expectedMovement[i] = Double.parseDouble(parts[i].trim()); + } + settings.put(KEY_EXPECTED_MOVEMENT, expectedMovement); + + return settings; + } + + @Override + public void clean() { + } } diff --git a/src/main/java/fiji/plugin/trackmate/tracking/TrackerKeys.java b/src/main/java/fiji/plugin/trackmate/tracking/TrackerKeys.java index 5c45912a1..790fb3b92 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/TrackerKeys.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/TrackerKeys.java @@ -19,6 +19,7 @@ * . * #L% */ +// modified by Lorenzo Pedrolli, 2024 package fiji.plugin.trackmate.tracking; import java.util.HashMap; @@ -101,6 +102,10 @@ public class TrackerKeys /** A default value for the {@value #KEY_KALMAN_SEARCH_RADIUS} parameter. */ public static final double DEFAULT_KALMAN_SEARCH_RADIUS = 20.0; + // New key for expected movement + public static final String KEY_EXPECTED_MOVEMENT = "EXPECTED_MOVEMENT"; + public static final double[] DEFAULT_EXPECTED_MOVEMENT = {0.0, 0.0, 0.0}; + /** * Key for the parameter specifying whether we allow the detection of * gap-closing events. Expected values are {@link Boolean}s. diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kalman/AdvancedKalmanTracker.java b/src/main/java/fiji/plugin/trackmate/tracking/kalman/AdvancedKalmanTracker.java index 17c74dcac..0bc789875 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/kalman/AdvancedKalmanTracker.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/kalman/AdvancedKalmanTracker.java @@ -21,6 +21,7 @@ */ package fiji.plugin.trackmate.tracking.kalman; +import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_EXPECTED_MOVEMENT; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_GAP_CLOSING_MAX_FRAME_GAP; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_KALMAN_SEARCH_RADIUS; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_LINKING_FEATURE_PENALTIES; @@ -135,12 +136,13 @@ public boolean process() * 1. Apply Kalman with feature penalties. */ final double maxSearchRadius = ( Double ) kalSettings.get( KEY_KALMAN_SEARCH_RADIUS ); + final double[] expectedMovement = ( double[] ) kalSettings.get( KEY_EXPECTED_MOVEMENT ); final double initialSearchRadius = ( Double ) kalSettings.get( KEY_LINKING_MAX_DISTANCE ); final int maxFrameGap = ( Integer ) kalSettings.get( KEY_GAP_CLOSING_MAX_FRAME_GAP ); @SuppressWarnings( "unchecked" ) final Map< String, Double > featurePenalties = ( Map< String, Double > ) kalSettings.get( KEY_LINKING_FEATURE_PENALTIES ); - final KalmanTracker kalmanTracker = new KalmanTracker( spots, maxSearchRadius, maxFrameGap, initialSearchRadius, featurePenalties ); + final KalmanTracker kalmanTracker = new KalmanTracker( spots, maxSearchRadius, maxFrameGap, initialSearchRadius, featurePenalties , expectedMovement); kalmanTracker.setLogger( logger ); if ( !kalmanTracker.checkInput() || !kalmanTracker.process() ) { @@ -212,6 +214,7 @@ protected boolean checkSettingsValidity( final Map< String, Object > settings, f ok = ok & checkParameter( settings, KEY_KALMAN_SEARCH_RADIUS, Double.class, errorHolder ); ok = ok & checkParameter( settings, KEY_GAP_CLOSING_MAX_FRAME_GAP, Integer.class, errorHolder ); ok = ok & checkFeatureMap( settings, KEY_LINKING_FEATURE_PENALTIES, errorHolder ); + ok = ok & checkParameter( settings, KEY_EXPECTED_MOVEMENT, double[].class, errorHolder ); // Check keys final List< String > mandatoryKeys = new ArrayList<>(); mandatoryKeys.add( KEY_KALMAN_SEARCH_RADIUS ); diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kalman/AdvancedKalmanTrackerFactory.java b/src/main/java/fiji/plugin/trackmate/tracking/kalman/AdvancedKalmanTrackerFactory.java index 3184c2073..1edfa6faa 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/kalman/AdvancedKalmanTrackerFactory.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/kalman/AdvancedKalmanTrackerFactory.java @@ -1,24 +1,3 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2024 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ package fiji.plugin.trackmate.tracking.kalman; import static fiji.plugin.trackmate.io.IOUtils.marshallMap; @@ -31,6 +10,8 @@ import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_KALMAN_SEARCH_RADIUS; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_LINKING_FEATURE_PENALTIES; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_LINKING_MAX_DISTANCE; +import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_EXPECTED_MOVEMENT; +import static fiji.plugin.trackmate.tracking.TrackerKeys.DEFAULT_EXPECTED_MOVEMENT; import static fiji.plugin.trackmate.tracking.jaqaman.LAPUtils.XML_ELEMENT_NAME_FEATURE_PENALTIES; import static fiji.plugin.trackmate.tracking.jaqaman.LAPUtils.XML_ELEMENT_NAME_LINKING; @@ -62,191 +43,233 @@ public class AdvancedKalmanTrackerFactory extends SegmentTrackerFactory { - public static final String THIS_TRACKER_KEY = "ADVANCED_KALMAN_TRACKER"; - - public static final String THIS_NAME = "Advanced Kalman Tracker"; - - public static final String THIS_INFO_TEXT = "" - + "This tracker is an extended version of the Kalman tracker, that adds " - + "the possibility to customize linking costs and detect track fusion " - + "(segments merging) and track division (segments splitting). " - + "

" - + "This tracker is especially well suited to objects that move following " - + "a nearly constant velocity vector. The velocity vectors of each object " - + "can be completely different from one another. But for the velocity " - + "vector of one object need not to change too much from one frame to another. " - + "

" - + "In the frame-to-frame linking step, the classic Kalman tracker " - + "infer most likely spot positions in the target frame from growing " - + "tracks and link all extrapolated positions against all spots in the " - + "target frame, based on the square distance. " - + "This advanced version of the tracker allows for penalizing " - + "links to spots with different features values " - + "using the same framework that of the LAP tracker in TrackMate. " - + "Also, after the frame-to-frame linking step, track segments are " - + "post-processed to detect splitting and merging events, and perform " - + "gap-closing. This is again based on the LAP tracker implementation. " - + ""; - - @Override - public String getInfoText() - { - return THIS_INFO_TEXT; - } - - @Override - public ImageIcon getIcon() - { - return null; - } - - @Override - public String getKey() - { - return THIS_TRACKER_KEY; - } - - @Override - public String getName() - { - return THIS_NAME; - } - - @Override - public SpotTracker create( final SpotCollection spots, final Map< String, Object > settings ) - { - return new AdvancedKalmanTracker( spots, settings ); - } - - @Override - public AdvancedKalmanTrackerFactory copy() - { - return new AdvancedKalmanTrackerFactory(); - } - - @Override - public Map< String, Object > getDefaultSettings() - { - final Map< String, Object > settings = LAPUtils.getDefaultSegmentSettingsMap(); - settings.put( KEY_LINKING_MAX_DISTANCE, DEFAULT_LINKING_MAX_DISTANCE ); - settings.put( KEY_KALMAN_SEARCH_RADIUS, DEFAULT_KALMAN_SEARCH_RADIUS ); - settings.put( KEY_LINKING_FEATURE_PENALTIES, new HashMap<>( DEFAULT_LINKING_FEATURE_PENALTIES ) ); - return settings; - } - - /** - * Marshall into the {@link Element} the settings in the {@link Map} that - * relate to segment linking: gap-closing, merging segments, splitting - * segments. - */ - @Override - public boolean marshall( final Map< String, Object > settings, final Element element ) - { - boolean ok = true; - final StringBuilder str = new StringBuilder(); - - ok = ok & writeAttribute( settings, element, KEY_KALMAN_SEARCH_RADIUS, Double.class, str ); - // Linking - final Element linkingElement = new Element( XML_ELEMENT_NAME_LINKING ); - ok = ok & writeAttribute( settings, linkingElement, KEY_LINKING_MAX_DISTANCE, Double.class, str ); - // feature penalties - @SuppressWarnings( "unchecked" ) - final Map< String, Double > lfpm = ( Map< String, Double > ) settings.get( KEY_LINKING_FEATURE_PENALTIES ); - final Element lfpElement = new Element( XML_ELEMENT_NAME_FEATURE_PENALTIES ); - marshallMap( lfpm, lfpElement ); - linkingElement.addContent( lfpElement ); - element.addContent( linkingElement ); - return ( ok & super.marshall( settings, element ) ); - } - - @Override - public boolean unmarshall( final Element element, final Map< String, Object > settings ) - { - final StringBuilder errorHolder = new StringBuilder(); - // common parameters - boolean ok = unmarshallSegment( element, settings, errorHolder ); - - ok = ok & readDoubleAttribute( element, settings, KEY_KALMAN_SEARCH_RADIUS, errorHolder ); - - // Linking - final Element linkingElement = element.getChild( XML_ELEMENT_NAME_LINKING ); - if ( null == linkingElement ) - { - errorHolder.append( "Could not found the " + XML_ELEMENT_NAME_LINKING + " element in XML.\n" ); - ok = false; - - } - else - { - ok = ok & readDoubleAttribute( linkingElement, settings, KEY_LINKING_MAX_DISTANCE, errorHolder ); - // feature penalties - final Map< String, Double > lfpMap = new HashMap<>(); - final Element lfpElement = linkingElement.getChild( XML_ELEMENT_NAME_FEATURE_PENALTIES ); - if ( null != lfpElement ) - { - ok = ok & unmarshallMap( lfpElement, lfpMap, errorHolder ); - } - settings.put( KEY_LINKING_FEATURE_PENALTIES, lfpMap ); - } - if ( !checkSettingsValidity( settings ) ) - { - ok = false; - errorHolder.append( errorMessage ); // append validity check message - - } - - if ( !ok ) - { - errorMessage = errorHolder.toString(); - } - return ok; - - } - - @Override - public boolean checkSettingsValidity( final Map< String, Object > settings ) - { - if ( null == settings ) - { - errorMessage = "Settings map is null.\n"; - return false; - } - - final StringBuilder str = new StringBuilder(); - final boolean ok = LAPUtils.checkSettingsValidity( settings, str, true ); - if ( !ok ) - { - errorMessage = str.toString(); - } - return ok; - } - - @Override - @SuppressWarnings( "unchecked" ) - public String toString( final Map< String, Object > sm ) - { - if ( !checkSettingsValidity( sm ) ) - { return errorMessage; } - - final StringBuilder str = new StringBuilder(); - final double maxSearchRadius = ( Double ) sm.get( KEY_KALMAN_SEARCH_RADIUS ); - final double initialSearchRadius = ( Double ) sm.get( KEY_LINKING_MAX_DISTANCE ); - - str.append( String.format( " - initial search radius: %.1f\n", initialSearchRadius ) ); - str.append( String.format( " - search radius: %.1f\n", maxSearchRadius ) ); - str.append( " Linking conditions:\n" ); - str.append( LAPUtils.echoFeaturePenalties( ( Map< String, Double > ) sm.get( KEY_LINKING_FEATURE_PENALTIES ) ) ); - - str.append( super.toString( sm ) ); - return str.toString(); - } - - @Override - public ConfigurationPanel getTrackerConfigurationPanel( final Model model ) - { - final String spaceUnits = model.getSpaceUnits(); - final Collection< String > features = model.getFeatureModel().getSpotFeatures(); - final Map< String, String > featureNames = model.getFeatureModel().getSpotFeatureNames(); - return new AdvancedKalmanTrackerSettingsPanel( getName(), spaceUnits, features, featureNames ); - } + public static final String THIS_TRACKER_KEY = "ADVANCED_KALMAN_TRACKER"; + + public static final String THIS_NAME = "Advanced Kalman Tracker - LP"; + + public static final String THIS_INFO_TEXT = "" + + "This tracker is an extended version of the Kalman tracker, that adds " + + "the possibility to customize linking costs and detect track fusion " + + "(segments merging) and track division (segments splitting). " + + "

" + + "This tracker is especially well suited to objects that move following " + + "a nearly constant velocity vector. The velocity vectors of each object " + + "can be completely different from one another. But for the velocity " + + "vector of one object need not to change too much from one frame to another. " + + "

" + + "In the frame-to-frame linking step, the classic Kalman tracker " + + "infers the most likely spot positions in the target frame from growing " + + "tracks and links all extrapolated positions against all spots in the " + + "target frame, based on the square distance. " + + "This advanced version of the tracker allows for penalizing " + + "links to spots with different feature values " + + "using the same framework as that of the LAP tracker in TrackMate. " + + "Also, after the frame-to-frame linking step, track segments are " + + "post-processed to detect splitting and merging events, and perform " + + "gap-closing. This is again based on the LAP tracker implementation. " + + ""; + + @Override + public String getInfoText() + { + return THIS_INFO_TEXT; + } + + @Override + public ImageIcon getIcon() + { + return null; + } + + @Override + public String getKey() + { + return THIS_TRACKER_KEY; + } + + @Override + public String getName() + { + return THIS_NAME; + } + + @Override + public SpotTracker create( final SpotCollection spots, final Map< String, Object > settings ) + { + return new AdvancedKalmanTracker( spots, settings ); + } + + @Override + public AdvancedKalmanTrackerFactory copy() + { + return new AdvancedKalmanTrackerFactory(); + } + + @Override + public Map< String, Object > getDefaultSettings() + { + final Map< String, Object > settings = LAPUtils.getDefaultSegmentSettingsMap(); + settings.put( KEY_LINKING_MAX_DISTANCE, DEFAULT_LINKING_MAX_DISTANCE ); + settings.put( KEY_KALMAN_SEARCH_RADIUS, DEFAULT_KALMAN_SEARCH_RADIUS ); + settings.put( KEY_LINKING_FEATURE_PENALTIES, new HashMap<>( DEFAULT_LINKING_FEATURE_PENALTIES ) ); + settings.put( KEY_EXPECTED_MOVEMENT, DEFAULT_EXPECTED_MOVEMENT ); // Add default expected movement + return settings; + } + + /** + * Marshall into the {@link Element} the settings in the {@link Map} that + * relate to segment linking: gap-closing, merging segments, splitting + * segments. + */ + @Override + public boolean marshall( final Map< String, Object > settings, final Element element ) + { + boolean ok = true; + final StringBuilder str = new StringBuilder(); + + ok = ok & writeAttribute( settings, element, KEY_KALMAN_SEARCH_RADIUS, Double.class, str ); + // Linking + final Element linkingElement = new Element( XML_ELEMENT_NAME_LINKING ); + ok = ok & writeAttribute( settings, linkingElement, KEY_LINKING_MAX_DISTANCE, Double.class, str ); + // feature penalties + @SuppressWarnings( "unchecked" ) + final Map< String, Double > lfpm = ( Map< String, Double > ) settings.get( KEY_LINKING_FEATURE_PENALTIES ); + final Element lfpElement = new Element( XML_ELEMENT_NAME_FEATURE_PENALTIES ); + marshallMap( lfpm, lfpElement ); + linkingElement.addContent( lfpElement ); + element.addContent( linkingElement ); + + // Marshall expected movement + ok = ok & writeDoubleArrayAttribute(settings, element, KEY_EXPECTED_MOVEMENT, str); + + return ( ok & super.marshall( settings, element ) ); + } + + @Override + public boolean unmarshall( final Element element, final Map< String, Object > settings ) + { + final StringBuilder errorHolder = new StringBuilder(); + // common parameters + boolean ok = unmarshallSegment( element, settings, errorHolder ); + + ok = ok & readDoubleAttribute( element, settings, KEY_KALMAN_SEARCH_RADIUS, errorHolder ); + ok = ok & readDoubleArrayAttribute(element, settings, KEY_EXPECTED_MOVEMENT, errorHolder); + + // Linking + final Element linkingElement = element.getChild( XML_ELEMENT_NAME_LINKING ); + if ( null == linkingElement ) + { + errorHolder.append( "Could not find the " + XML_ELEMENT_NAME_LINKING + " element in XML.\n" ); + ok = false; + + } + else + { + ok = ok & readDoubleAttribute( linkingElement, settings, KEY_LINKING_MAX_DISTANCE, errorHolder ); + // feature penalties + final Map< String, Double > lfpMap = new HashMap<>(); + final Element lfpElement = linkingElement.getChild( XML_ELEMENT_NAME_FEATURE_PENALTIES ); + if ( null != lfpElement ) + { + ok = ok & unmarshallMap( lfpElement, lfpMap, errorHolder ); + } + settings.put( KEY_LINKING_FEATURE_PENALTIES, lfpMap ); + } + if ( !checkSettingsValidity( settings ) ) + { + ok = false; + errorHolder.append( errorMessage ); // append validity check message + + } + + if ( !ok ) + { + errorMessage = errorHolder.toString(); + } + return ok; + + } + + @Override + public boolean checkSettingsValidity( final Map< String, Object > settings ) + { + if ( null == settings ) + { + errorMessage = "Settings map is null.\n"; + return false; + } + + final StringBuilder str = new StringBuilder(); + final boolean ok = LAPUtils.checkSettingsValidity( settings, str, true ); + if ( !ok ) + { + errorMessage = str.toString(); + } + return ok; + } + + @Override + @SuppressWarnings( "unchecked" ) + public String toString( final Map< String, Object > sm ) + { + if ( !checkSettingsValidity( sm ) ) + { return errorMessage; } + + final StringBuilder str = new StringBuilder(); + final double maxSearchRadius = ( Double ) sm.get( KEY_KALMAN_SEARCH_RADIUS ); + final double initialSearchRadius = ( Double ) sm.get( KEY_LINKING_MAX_DISTANCE ); + final double[] expectedMovement = (double[]) sm.get(KEY_EXPECTED_MOVEMENT); + + str.append( String.format( " - initial search radius: %.1f\n", initialSearchRadius ) ); + str.append( String.format( " - search radius: %.1f\n", maxSearchRadius ) ); + str.append( String.format( " - expected movement: [%.1f, %.1f, %.1f]\n", expectedMovement[0], expectedMovement[1], expectedMovement[2] ) ); + str.append( " Linking conditions:\n" ); + str.append( LAPUtils.echoFeaturePenalties( ( Map< String, Double > ) sm.get( KEY_LINKING_FEATURE_PENALTIES ) ) ); + + str.append( super.toString( sm ) ); + return str.toString(); + } + + @Override + public ConfigurationPanel getTrackerConfigurationPanel( final Model model ) + { + final String spaceUnits = model.getSpaceUnits(); + final Collection< String > features = model.getFeatureModel().getSpotFeatures(); + final Map< String, String > featureNames = model.getFeatureModel().getSpotFeatureNames(); + return new AdvancedKalmanTrackerSettingsPanel( getName(), spaceUnits, features, featureNames ); + } + + private boolean readDoubleArrayAttribute(Element element, Map settings, String key, StringBuilder errorHolder) { + try { + String str = element.getAttributeValue(key); + String[] values = str.split(","); + double[] array = new double[values.length]; + for (int i = 0; i < values.length; i++) { + array[i] = Double.parseDouble(values[i]); + } + settings.put(key, array); + return true; + } catch (Exception e) { + errorHolder.append("Could not read double array for key " + key + "\n"); + return false; + } + } + + private boolean writeDoubleArrayAttribute(Map settings, Element element, String key, StringBuilder errorHolder) { + try { + double[] array = (double[]) settings.get(key); + StringBuilder str = new StringBuilder(); + for (int i = 0; i < array.length; i++) { + str.append(array[i]); + if (i < array.length - 1) { + str.append(","); + } + } + element.setAttribute(key, str.toString()); + return true; + } catch (Exception e) { + errorHolder.append("Could not write double array for key " + key + "\n"); + return false; + } + } } diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kalman/EX_KalmanTracker b/src/main/java/fiji/plugin/trackmate/tracking/kalman/EX_KalmanTracker new file mode 100644 index 000000000..281944e99 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/tracking/kalman/EX_KalmanTracker @@ -0,0 +1,524 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package fiji.plugin.trackmate.tracking.kalman; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NavigableSet; + +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.SimpleWeightedGraph; +import org.scijava.Cancelable; + +import fiji.plugin.trackmate.Logger; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.tracking.SpotTracker; +import fiji.plugin.trackmate.tracking.jaqaman.JaqamanLinker; +import fiji.plugin.trackmate.tracking.jaqaman.costfunction.CostFunction; +import fiji.plugin.trackmate.tracking.jaqaman.costfunction.FeaturePenaltyCostFunction; +import fiji.plugin.trackmate.tracking.jaqaman.costfunction.SquareDistCostFunction; +import fiji.plugin.trackmate.tracking.jaqaman.costmatrix.JaqamanLinkingCostMatrixCreator; +import net.imglib2.algorithm.Benchmark; + +public class KalmanTracker implements SpotTracker, Benchmark, Cancelable +{ + + private static final double ALTERNATIVE_COST_FACTOR = 1.05d; + + private static final double PERCENTILE = 1d; + + private static final String BASE_ERROR_MSG = "[KalmanTracker] "; + + private SimpleWeightedGraph< Spot, DefaultWeightedEdge > graph; + + private String errorMessage; + + private Logger logger = Logger.VOID_LOGGER; + + private final SpotCollection spots; + + private final double maxSearchRadius; + + private final int maxFrameGap; + + private final double initialSearchRadius; + + private final Map< String, Double > featurePenalties; + + private boolean savePredictions = false; + + private SpotCollection predictionsCollection; + + private long processingTime; + + private boolean isCanceled; + + private String cancelReason; + + /* + * CONSTRUCTOR + */ + + /** + * @param spots + * the spots to track. + * @param maxSearchRadius + * @param maxFrameGap + * @param initialSearchRadius + */ + public EX_KalmanTracker( final SpotCollection spots, final double maxSearchRadius, final int maxFrameGap, final double initialSearchRadius, final Map< String, Double > featurePenalties ) + { + this.spots = spots; + this.maxSearchRadius = maxSearchRadius; + this.maxFrameGap = maxFrameGap; + this.initialSearchRadius = initialSearchRadius; + this.featurePenalties = featurePenalties; + } + + /* + * PUBLIC METHODS + */ + + @Override + public SimpleWeightedGraph< Spot, DefaultWeightedEdge > getResult() + { + return graph; + } + + @Override + public boolean checkInput() + { + return true; + } + + @Override + public boolean process() + { + final long start = System.currentTimeMillis(); + + isCanceled = false; + cancelReason = null; + + /* + * Outputs + */ + + graph = new SimpleWeightedGraph<>( DefaultWeightedEdge.class ); + predictionsCollection = new SpotCollection(); + + /* + * Constants. + */ + + // Max KF search cost. + final double maxCost = maxSearchRadius * maxSearchRadius; + // Cost function to nucleate KFs. + // final CostFunction< Spot, Spot > nucleatingCostFunction = new + // SquareDistCostFunction(); + final CostFunction< Spot, Spot > nucleatingCostFunction = getCostFunction( featurePenalties ); + // Max cost to nucleate KFs. + final double maxInitialCost = initialSearchRadius * initialSearchRadius; + + final CostFunction< Spot, Spot > costFunction = getCostFunction( featurePenalties ); + + // Find first and second non-empty frames. + final NavigableSet< Integer > keySet = spots.keySet(); + final Iterator< Integer > frameIterator = keySet.iterator(); + + /* + * Initialize. Find first links just based on square distance. We do + * this via the orphan spots lists. + */ + + // Spots in the PREVIOUS frame that were not part of a link. + Collection< Spot > previousOrphanSpots = new ArrayList<>(); + if ( !frameIterator.hasNext() ) + return true; + + int firstFrame = frameIterator.next(); + while ( true ) + { + previousOrphanSpots = generateSpotList( spots, firstFrame ); + if ( !frameIterator.hasNext() ) + return true; + if ( !previousOrphanSpots.isEmpty() ) + break; + + firstFrame = frameIterator.next(); + } + + /* + * Spots in the current frame that are not part of a new link (no + * parent). + */ + Collection< Spot > orphanSpots = new ArrayList<>(); + int secondFrame = frameIterator.next(); + while ( true ) + { + orphanSpots = generateSpotList( spots, secondFrame ); + if ( !frameIterator.hasNext() ) + return true; + if ( !orphanSpots.isEmpty() ) + break; + + secondFrame = frameIterator.next(); + } + + /* + * Estimate Kalman filter variances. + * + * The search radius is used to derive an estimate of the noise that + * affects position and velocity. The two are linked: if we need a large + * search radius, then the fluctuations over predicted states are large. + */ + final double positionProcessStd = maxSearchRadius / 3d; + final double velocityProcessStd = maxSearchRadius / 3d; + /* + * We assume the detector did a good job and that positions measured are + * accurate up to a fraction of the spot radius + */ + + double meanSpotRadius = 0d; + for ( final Spot spot : orphanSpots ) + meanSpotRadius += spot.getFeature( Spot.RADIUS ).doubleValue(); + + meanSpotRadius /= orphanSpots.size(); + final double positionMeasurementStd = meanSpotRadius / 10d; + + // The master map that contains the currently active KFs. + final Map< CVMKalmanFilter, Spot > kalmanFiltersMap = new HashMap<>( orphanSpots.size() ); + + /* + * Then loop over time, starting from second frame. + */ + int p = 1; + for ( int frame = secondFrame; frame <= keySet.last(); frame++ ) + { + if ( isCanceled() ) + return true; // It's ok to be canceled. + + p++; + + // Use the spot in the next frame has measurements. + final List< Spot > measurements = generateSpotList( spots, frame ); + + /* + * Predict for all Kalman filters, and use it to generate linking + * candidates. + */ + final Map< Spot, CVMKalmanFilter > predictionMap = new HashMap<>( kalmanFiltersMap.size() ); + for ( final CVMKalmanFilter kf : kalmanFiltersMap.keySet() ) + { + final double[] X = kf.predict(); + final Spot s = kalmanFiltersMap.get( kf ); + final Spot predSpot = new Spot( X[ 0 ], X[ 1 ], X[ 2 ], s.getFeature( Spot.RADIUS ), s.getFeature( Spot.QUALITY ) ); + // copy the necessary features of original spot to the predicted + // spot + if ( null != featurePenalties ) + predSpot.copyFeatures( s, featurePenalties ); + + predictionMap.put( predSpot, kf ); + + if ( savePredictions ) + { + final Spot pred = new Spot( X[ 0 ], X[ 1 ], X[ 2 ], s.getFeature( Spot.RADIUS ), s.getFeature( Spot.QUALITY ) ); + pred.setName( "Pred_" + s.getName() ); + pred.putFeature( Spot.RADIUS, s.getFeature( Spot.RADIUS ) ); + predictionsCollection.add( predSpot, frame ); + } + + } + final List< Spot > predictions = new ArrayList<>( predictionMap.keySet() ); + + /* + * The KF for which we could not find a measurement in the target + * frame. Is updated later. + */ + final Collection< CVMKalmanFilter > childlessKFs = new HashSet<>( kalmanFiltersMap.keySet() ); + + /* + * Find the global (in space) optimum for associating a prediction + * to a measurement. + */ + + orphanSpots = new HashSet<>( measurements ); + if ( !predictions.isEmpty() && !measurements.isEmpty() ) + { + // Only link measurements to predictions if we have predictions. + final JaqamanLinkingCostMatrixCreator< Spot, Spot > crm = new JaqamanLinkingCostMatrixCreator<>( + predictions, + measurements, + costFunction, + maxCost, + ALTERNATIVE_COST_FACTOR, + PERCENTILE ); + final JaqamanLinker< Spot, Spot > linker = new JaqamanLinker<>( crm ); + if ( !linker.checkInput() || !linker.process() ) + { + errorMessage = BASE_ERROR_MSG + "Error linking candidates in frame " + frame + ": " + linker.getErrorMessage(); + return false; + } + final Map< Spot, Spot > agnts = linker.getResult(); + final Map< Spot, Double > costs = linker.getAssignmentCosts(); + // Deal with found links. + for ( final Spot spotty : agnts.keySet() ) + { + final CVMKalmanFilter kf = predictionMap.get( spotty ); + + // Create links for found match. + final Spot source = kalmanFiltersMap.get( kf ); + final Spot target = agnts.get( spotty ); + + graph.addVertex( source ); + graph.addVertex( target ); + final DefaultWeightedEdge edge = graph.addEdge( source, target ); + final double cost = costs.get( spotty ); + graph.setEdgeWeight( edge, cost ); + + // Update Kalman filter + kf.update( toMeasurement( target ) ); + + // Update Kalman track spot + kalmanFiltersMap.put( kf, target ); + + // Remove from orphan set + orphanSpots.remove( target ); + + // Remove from childless KF set + childlessKFs.remove( kf ); + } + } + + /* + * Deal with orphans from the previous frame. (We deal with orphans + * from previous frame only now because we want to link in priority + * target spots to predictions. Nucleating new KF from nearest + * neighbor only comes second. + */ + if ( !previousOrphanSpots.isEmpty() && !orphanSpots.isEmpty() ) + { + + /* + * We now deal with orphans of the previous frame. We try to + * find them a target from the list of spots that are not + * already part of a link created via KF. That is: the orphan + * spots of this frame. + */ + + final JaqamanLinkingCostMatrixCreator< Spot, Spot > ic = new JaqamanLinkingCostMatrixCreator<>( + previousOrphanSpots, + orphanSpots, + nucleatingCostFunction, + maxInitialCost, + ALTERNATIVE_COST_FACTOR, + PERCENTILE ); + final JaqamanLinker< Spot, Spot > newLinker = new JaqamanLinker<>( ic ); + if ( !newLinker.checkInput() || !newLinker.process() ) + { + errorMessage = BASE_ERROR_MSG + "Error linking spots from frame " + ( frame - 1 ) + " to frame " + frame + ": " + newLinker.getErrorMessage(); + return false; + } + final Map< Spot, Spot > newAssignments = newLinker.getResult(); + final Map< Spot, Double > assignmentCosts = newLinker.getAssignmentCosts(); + + // Build links and new KFs from these links. + for ( final Spot source : newAssignments.keySet() ) + { + final Spot target = newAssignments.get( source ); + + // Remove from orphan collection. + orphanSpots.remove( target ); + + // Derive initial state and create Kalman filter. + final double[] XP = estimateInitialState( source, target ); + final CVMKalmanFilter kt = new CVMKalmanFilter( XP, Double.MIN_NORMAL, positionProcessStd, velocityProcessStd, positionMeasurementStd ); + // We trust the initial state a lot. + + // Store filter and source + kalmanFiltersMap.put( kt, target ); + + // Add edge to the graph. + graph.addVertex( source ); + graph.addVertex( target ); + final DefaultWeightedEdge edge = graph.addEdge( source, target ); + final double cost = assignmentCosts.get( source ); + graph.setEdgeWeight( edge, cost ); + } + } + previousOrphanSpots = orphanSpots; + + // Deal with childless KFs. + for ( final CVMKalmanFilter kf : childlessKFs ) + { + // Echo we missed a measurement + kf.update( null ); + + /* + * We can bridge a limited number of gaps. If too much, we die. + * If not, we will use predicted state next time. + */ + if ( kf.getNOcclusion() > maxFrameGap ) + kalmanFiltersMap.remove( kf ); + } + + final double progress = ( double ) p / keySet.size(); + logger.setProgress( progress ); + } + + if ( savePredictions ) + predictionsCollection.setVisible( true ); + + final long end = System.currentTimeMillis(); + processingTime = end - start; + + return true; + } + + @Override + public String getErrorMessage() + { + return errorMessage; + } + + /** + * Returns the saved predicted state as a {@link SpotCollection}. + * + * @return the predicted states. + * @see #setSavePredictions(boolean) + */ + public SpotCollection getPredictions() + { + return predictionsCollection; + } + + /** + * Sets whether the tracker saves the predicted states. + * + * @param doSave + * if true, the predicted states will be saved. + * @see #getPredictions() + */ + public void setSavePredictions( final boolean doSave ) + { + this.savePredictions = doSave; + } + + @Override + public void setNumThreads() + {} + + @Override + public void setNumThreads( final int numThreads ) + {} + + @Override + public int getNumThreads() + { + return 1; + } + + @Override + public long getProcessingTime() + { + return processingTime; + } + + @Override + public void setLogger( final Logger logger ) + { + this.logger = logger; + } + + private static final double[] toMeasurement( final Spot spot ) + { + final double[] d = new double[] { + spot.getDoublePosition( 0 ), + spot.getDoublePosition( 1 ), + spot.getDoublePosition( 2 ) + }; + return d; + } + + private static final double[] estimateInitialState( final Spot first, final Spot second ) + { + final double[] xp = new double[] { + second.getDoublePosition( 0 ), + second.getDoublePosition( 1 ), + second.getDoublePosition( 2 ), + second.diffTo( first, Spot.POSITION_X ), + second.diffTo( first, Spot.POSITION_Y ), + second.diffTo( first, Spot.POSITION_Z ) + }; + return xp; + } + + private static final List< Spot > generateSpotList( final SpotCollection spots, final int frame ) + { + final List< Spot > list = new ArrayList<>( spots.getNSpots( frame, true ) ); + for ( final Iterator< Spot > iterator = spots.iterator( frame, true ); iterator.hasNext(); ) + list.add( iterator.next() ); + + return list; + } + + /** + * Creates a suitable cost function. + * + * @param featurePenalties + * feature penalties to base costs on. Can be null. + * @return a new {@link CostFunction} + */ + protected CostFunction< Spot, Spot > getCostFunction( final Map< String, Double > featurePenalties ) + { + if ( null == featurePenalties || featurePenalties.isEmpty() ) + return new SquareDistCostFunction(); + + return new FeaturePenaltyCostFunction( featurePenalties ); + } + + // --- org.scijava.Cancelable methods --- + + @Override + public boolean isCanceled() + { + return isCanceled; + } + + @Override + public void cancel( final String reason ) + { + isCanceled = true; + cancelReason = reason; + } + + @Override + public String getCancelReason() + { + return cancelReason; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTracker.java b/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTracker.java index 7477b9609..f6a1f62b3 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTracker.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTracker.java @@ -1,24 +1,5 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2024 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ +// modified by Lorenzo Pedrolli, 2024 + package fiji.plugin.trackmate.tracking.kalman; import java.util.ArrayList; @@ -80,6 +61,8 @@ public class KalmanTracker implements SpotTracker, Benchmark, Cancelable private String cancelReason; + private final double[] expectedMovement; + /* * CONSTRUCTOR */ @@ -90,14 +73,16 @@ public class KalmanTracker implements SpotTracker, Benchmark, Cancelable * @param maxSearchRadius * @param maxFrameGap * @param initialSearchRadius + * + * @param expectedMovement */ - public KalmanTracker( final SpotCollection spots, final double maxSearchRadius, final int maxFrameGap, final double initialSearchRadius, final Map< String, Double > featurePenalties ) - { + public KalmanTracker(final SpotCollection spots, final double maxSearchRadius, final int maxFrameGap, final double initialSearchRadius, final Map featurePenalties, final double[] expectedMovement) { this.spots = spots; this.maxSearchRadius = maxSearchRadius; this.maxFrameGap = maxFrameGap; this.initialSearchRadius = initialSearchRadius; this.featurePenalties = featurePenalties; + this.expectedMovement = expectedMovement; // Store the expected movement array } /* @@ -320,8 +305,7 @@ public boolean process() * target spots to predictions. Nucleating new KF from nearest * neighbor only comes second. */ - if ( !previousOrphanSpots.isEmpty() && !orphanSpots.isEmpty() ) - { + if (!previousOrphanSpots.isEmpty() && !orphanSpots.isEmpty()) { /* * We now deal with orphans of the previous frame. We try to @@ -329,47 +313,63 @@ public boolean process() * already part of a link created via KF. That is: the orphan * spots of this frame. */ - - final JaqamanLinkingCostMatrixCreator< Spot, Spot > ic = new JaqamanLinkingCostMatrixCreator<>( - previousOrphanSpots, + + // Translate orphan spots by expected movement + Collection translatedOrphanSpots = new ArrayList<>(); + Map originalToTranslatedSpotMap = new HashMap<>(); + for (Spot spot : previousOrphanSpots) { + Spot translatedSpot = new Spot(spot); + // Copy all features + for (String feature : spot.getFeatures().keySet()) { + translatedSpot.putFeature(feature, spot.getFeature(feature)); + } + translatedSpot.putFeature("POSITION_X", spot.getDoublePosition(0) + expectedMovement[0]); + translatedSpot.putFeature("POSITION_Y", spot.getDoublePosition(1) + expectedMovement[1]); + translatedSpot.putFeature("POSITION_Z", spot.getDoublePosition(2) + expectedMovement[2]); + + translatedOrphanSpots.add(translatedSpot); + originalToTranslatedSpotMap.put(translatedSpot, spot); // Keep track of original spots + } + + final JaqamanLinkingCostMatrixCreator ic = new JaqamanLinkingCostMatrixCreator<>( + translatedOrphanSpots, orphanSpots, nucleatingCostFunction, maxInitialCost, ALTERNATIVE_COST_FACTOR, - PERCENTILE ); - final JaqamanLinker< Spot, Spot > newLinker = new JaqamanLinker<>( ic ); - if ( !newLinker.checkInput() || !newLinker.process() ) - { - errorMessage = BASE_ERROR_MSG + "Error linking spots from frame " + ( frame - 1 ) + " to frame " + frame + ": " + newLinker.getErrorMessage(); + PERCENTILE); + final JaqamanLinker newLinker = new JaqamanLinker<>(ic); + if (!newLinker.checkInput() || !newLinker.process()) { + errorMessage = BASE_ERROR_MSG + "Error linking spots from frame " + (frame - 1) + " to frame " + frame + ": " + newLinker.getErrorMessage(); return false; } - final Map< Spot, Spot > newAssignments = newLinker.getResult(); - final Map< Spot, Double > assignmentCosts = newLinker.getAssignmentCosts(); - + final Map newAssignments = newLinker.getResult(); + final Map assignmentCosts = newLinker.getAssignmentCosts(); + // Build links and new KFs from these links. - for ( final Spot source : newAssignments.keySet() ) - { - final Spot target = newAssignments.get( source ); - + for (final Spot translatedSource : newAssignments.keySet()) { + final Spot target = newAssignments.get(translatedSource); + final Spot originalSource = originalToTranslatedSpotMap.get(translatedSource); + // Remove from orphan collection. - orphanSpots.remove( target ); - + orphanSpots.remove(target); + // Derive initial state and create Kalman filter. - final double[] XP = estimateInitialState( source, target ); - final CVMKalmanFilter kt = new CVMKalmanFilter( XP, Double.MIN_NORMAL, positionProcessStd, velocityProcessStd, positionMeasurementStd ); + final double[] XP = estimateInitialState(originalSource, target); + final CVMKalmanFilter kt = new CVMKalmanFilter(XP, Double.MIN_NORMAL, positionProcessStd, velocityProcessStd, positionMeasurementStd); // We trust the initial state a lot. - + // Store filter and source - kalmanFiltersMap.put( kt, target ); - + kalmanFiltersMap.put(kt, target); + // Add edge to the graph. - graph.addVertex( source ); - graph.addVertex( target ); - final DefaultWeightedEdge edge = graph.addEdge( source, target ); - final double cost = assignmentCosts.get( source ); - graph.setEdgeWeight( edge, cost ); + graph.addVertex(originalSource); + graph.addVertex(target); + final DefaultWeightedEdge edge = graph.addEdge(originalSource, target); + final double cost = assignmentCosts.get(translatedSource); + graph.setEdgeWeight(edge, cost); } - } + } previousOrphanSpots = orphanSpots; // Deal with childless KFs. diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerFactory.java b/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerFactory.java index f62c191f0..f87bdaa85 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerFactory.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerFactory.java @@ -1,24 +1,3 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2024 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ package fiji.plugin.trackmate.tracking.kalman; import static fiji.plugin.trackmate.io.IOUtils.readDoubleAttribute; @@ -46,169 +25,183 @@ import fiji.plugin.trackmate.tracking.SpotTrackerFactory; import static fiji.plugin.trackmate.tracking.TrackerKeys.DEFAULT_KALMAN_SEARCH_RADIUS; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_KALMAN_SEARCH_RADIUS; - -@Plugin( type = SpotTrackerFactory.class ) -public class KalmanTrackerFactory implements SpotTrackerFactory -{ - - private static final String INFO_TEXT_PART2 = "This tracker needs two parameters (on top of the maximal frame gap tolerated): " - + "
" - + "\t - the max search radius defines how far from a predicted position it should look " - + "for candidate spots;
" - + "\t - the initial search radius defines how far two spots can be apart when initiating " - + "a new track." - + "
"; - - private static final String INFO_TEXT = "" - + "This tracker is best suited for objects that " - + "move with a roughly constant velocity vector." - + "

" - + "It relies on the Kalman filter to predict the next most likely position of a spot. " - + "The predictions for all current tracks are linked to the spots actually " - + "found in the next frame, thanks to the LAP framework already present in the LAP tracker. " - + "Predictions are continuously refined and the tracker can accommodate moderate " - + "velocity direction and magnitude changes. " - + "

" - + "This tracker can bridge gaps: If a spot is not found close enough to a prediction, " - + "then the Kalman filter will make another prediction in the next frame and re-iterate " - + "the search. " - + "

" - + "The first frames of a track are critical for this tracker to work properly: Tracks" - + "are initiated by looking for close neighbors (again via the LAP tracker). " - + "Spurious spots in the beginning of each track can confuse the tracker." - + "

" - + INFO_TEXT_PART2; - - public static final String KEY = "KALMAN_TRACKER"; - - public static final String NAME = "Kalman tracker"; - - private String errorMessage; - - @Override - public String getInfoText() - { - return INFO_TEXT; - } - - @Override - public ImageIcon getIcon() - { - return null; - } - - @Override - public String getKey() - { - return KEY; - } - - @Override - public String getName() - { - return NAME; - } - - @Override - public SpotTracker create( final SpotCollection spots, final Map< String, Object > settings ) - { - final double maxSearchRadius = ( Double ) settings.get( KEY_KALMAN_SEARCH_RADIUS ); - final int maxFrameGap = ( Integer ) settings.get( KEY_GAP_CLOSING_MAX_FRAME_GAP ); - final double initialSearchRadius = ( Double ) settings.get( KEY_LINKING_MAX_DISTANCE ); - return new KalmanTracker( spots, maxSearchRadius, maxFrameGap, initialSearchRadius, null ); - } - - @Override - public ConfigurationPanel getTrackerConfigurationPanel( final Model model ) - { - final String spaceUnits = model.getSpaceUnits(); - return new KalmanTrackerConfigPanel( getName(), "" + INFO_TEXT_PART2, spaceUnits ); - } - - @Override - public boolean marshall( final Map< String, Object > settings, final Element element ) - { - boolean ok = true; - final StringBuilder str = new StringBuilder(); - - ok = ok & writeAttribute( settings, element, KEY_LINKING_MAX_DISTANCE, Double.class, str ); - ok = ok & writeAttribute( settings, element, KEY_KALMAN_SEARCH_RADIUS, Double.class, str ); - ok = ok & writeAttribute( settings, element, KEY_GAP_CLOSING_MAX_FRAME_GAP, Integer.class, str ); - return ok; - } - - @Override - public boolean unmarshall( final Element element, final Map< String, Object > settings ) - { - settings.clear(); - final StringBuilder errorHolder = new StringBuilder(); - boolean ok = true; - - ok = ok & readDoubleAttribute( element, settings, KEY_LINKING_MAX_DISTANCE, errorHolder ); - ok = ok & readDoubleAttribute( element, settings, KEY_KALMAN_SEARCH_RADIUS, errorHolder ); - ok = ok & readIntegerAttribute( element, settings, KEY_GAP_CLOSING_MAX_FRAME_GAP, errorHolder ); - return ok; - } - - @Override - public String toString( final Map< String, Object > settings ) - { - if ( !checkSettingsValidity( settings ) ) { return errorMessage; } - - final double maxSearchRadius = ( Double ) settings.get( KEY_KALMAN_SEARCH_RADIUS ); - final int maxFrameGap = ( Integer ) settings.get( KEY_GAP_CLOSING_MAX_FRAME_GAP ); - final double initialSearchRadius = ( Double ) settings.get( KEY_LINKING_MAX_DISTANCE ); - final StringBuilder str = new StringBuilder(); - - str.append( String.format( " - initial search radius: %.1f\n", initialSearchRadius)); - str.append( String.format( " - max search radius: %.1f\n", maxSearchRadius ) ); - str.append( String.format( " - max frame gap: %d\n", maxFrameGap ) ); - - return str.toString(); - } - - @Override - public Map< String, Object > getDefaultSettings() - { - final Map< String, Object > sm = new HashMap<>( 3 ); - sm.put( KEY_KALMAN_SEARCH_RADIUS, DEFAULT_KALMAN_SEARCH_RADIUS ); - sm.put( KEY_LINKING_MAX_DISTANCE, DEFAULT_LINKING_MAX_DISTANCE ); - sm.put( KEY_GAP_CLOSING_MAX_FRAME_GAP, DEFAULT_GAP_CLOSING_MAX_FRAME_GAP ); - return sm; - } - - @Override - public boolean checkSettingsValidity( final Map< String, Object > settings ) - { - if ( null == settings ) - { - errorMessage = "Settings map is null.\n"; - return false; - } - - boolean ok = true; - final StringBuilder str = new StringBuilder(); - - ok = ok & checkParameter( settings, KEY_LINKING_MAX_DISTANCE, Double.class, str ); - ok = ok & checkParameter( settings, KEY_KALMAN_SEARCH_RADIUS, Double.class, str ); - ok = ok & checkParameter( settings, KEY_GAP_CLOSING_MAX_FRAME_GAP, Integer.class, str ); - - if ( !ok ) - { - errorMessage = str.toString(); - } - return ok; - } - - @Override - public String getErrorMessage() - { - return errorMessage; - } - - @Override - public KalmanTrackerFactory copy() - { - return new KalmanTrackerFactory(); - } +import static fiji.plugin.trackmate.tracking.TrackerKeys.DEFAULT_EXPECTED_MOVEMENT; +import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_EXPECTED_MOVEMENT; + +@Plugin(type = SpotTrackerFactory.class) +public class KalmanTrackerFactory implements SpotTrackerFactory { + + private static final String INFO_TEXT_PART2 = "This tracker needs FOUR parameters (on top of the maximal frame gap tolerated): " + + "
" + + "\t - the max search radius defines how far from a predicted position it should look " + + "for candidate spots;
" + + "\t - the initial search radius defines how far two spots can be apart when initiating " + + "a new track.
" + + "\t - the expected movement defines how far two spots are expected to move when initiating " + + "a new track in X, Y, and Z directions.
" + + "\t Modified by Lorenzo Pedrolli, 2024" + + "
"; + + private static final String INFO_TEXT = "" + + "This tracker is best suited for objects that " + + "move with a roughly constant velocity vector." + + "

" + + "It relies on the Kalman filter to predict the next most likely position of a spot. " + + "The predictions for all current tracks are linked to the spots actually " + + "found in the next frame, thanks to the LAP framework already present in the LAP tracker. " + + "Predictions are continuously refined and the tracker can accommodate moderate " + + "velocity direction and magnitude changes. " + + "

" + + "This tracker can bridge gaps: If a spot is not found close enough to a prediction, " + + "then the Kalman filter will make another prediction in the next frame and re-iterate " + + "the search. " + + "

" + + "The first frames of a track are critical for this tracker to work properly: Tracks" + + "are initiated by looking for close neighbors (again via the LAP tracker). " + + "Spurious spots in the beginning of each track can confuse the tracker." + + "

" + + INFO_TEXT_PART2; + + public static final String KEY = "KALMAN_TRACKER"; + + public static final String NAME = "Kalman tracker - LP"; + + private String errorMessage; + + @Override + public String getInfoText() { + return INFO_TEXT; + } + + @Override + public ImageIcon getIcon() { + return null; + } + + @Override + public String getKey() { + return KEY; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public SpotTracker create(final SpotCollection spots, final Map settings) { + final double maxSearchRadius = (Double) settings.get(KEY_KALMAN_SEARCH_RADIUS); + final int maxFrameGap = (Integer) settings.get(KEY_GAP_CLOSING_MAX_FRAME_GAP); + final double initialSearchRadius = (Double) settings.get(KEY_LINKING_MAX_DISTANCE); + final double[] expectedMovement = (double[]) settings.get(KEY_EXPECTED_MOVEMENT); + return new KalmanTracker(spots, maxSearchRadius, maxFrameGap, initialSearchRadius, null, expectedMovement); + } + + @Override + public ConfigurationPanel getTrackerConfigurationPanel(final Model model) { + final String spaceUnits = model.getSpaceUnits(); + return new KalmanTrackerConfigPanel(getName(), "" + INFO_TEXT_PART2, spaceUnits); + } + + @Override + public boolean marshall(final Map settings, final Element element) { + boolean ok = true; + final StringBuilder str = new StringBuilder(); + + ok = ok & writeAttribute(settings, element, KEY_LINKING_MAX_DISTANCE, Double.class, str); + ok = ok & writeAttribute(settings, element, KEY_KALMAN_SEARCH_RADIUS, Double.class, str); + ok = ok & writeAttribute(settings, element, KEY_GAP_CLOSING_MAX_FRAME_GAP, Integer.class, str); + ok = ok & writeAttribute(settings, element, KEY_EXPECTED_MOVEMENT, double[].class, str); + return ok; + } + + @Override + public boolean unmarshall(final Element element, final Map settings) { + settings.clear(); + final StringBuilder errorHolder = new StringBuilder(); + boolean ok = true; + + ok = ok & readDoubleAttribute(element, settings, KEY_LINKING_MAX_DISTANCE, errorHolder); + ok = ok & readDoubleAttribute(element, settings, KEY_KALMAN_SEARCH_RADIUS, errorHolder); + ok = ok & readIntegerAttribute(element, settings, KEY_GAP_CLOSING_MAX_FRAME_GAP, errorHolder); + ok = ok & readDoubleArrayAttribute(element, settings, KEY_EXPECTED_MOVEMENT, errorHolder); + return ok; + } + + @Override + public String toString(final Map settings) { + if (!checkSettingsValidity(settings)) { + return errorMessage; + } + + final double maxSearchRadius = (Double) settings.get(KEY_KALMAN_SEARCH_RADIUS); + final int maxFrameGap = (Integer) settings.get(KEY_GAP_CLOSING_MAX_FRAME_GAP); + final double initialSearchRadius = (Double) settings.get(KEY_LINKING_MAX_DISTANCE); + final double[] expectedMovement = (double[]) settings.get(KEY_EXPECTED_MOVEMENT); + final StringBuilder str = new StringBuilder(); + + str.append(String.format(" - initial search radius: %.1f\n", initialSearchRadius)); + str.append(String.format(" - max search radius: %.1f\n", maxSearchRadius)); + str.append(String.format(" - max frame gap: %d\n", maxFrameGap)); + str.append(String.format(" - expected movement: [%.1f, %.1f, %.1f]\n", expectedMovement[0], expectedMovement[1], expectedMovement[2])); + + return str.toString(); + } + + @Override + public Map getDefaultSettings() { + final Map sm = new HashMap<>(4); + sm.put(KEY_KALMAN_SEARCH_RADIUS, DEFAULT_KALMAN_SEARCH_RADIUS); + sm.put(KEY_LINKING_MAX_DISTANCE, DEFAULT_LINKING_MAX_DISTANCE); + sm.put(KEY_GAP_CLOSING_MAX_FRAME_GAP, DEFAULT_GAP_CLOSING_MAX_FRAME_GAP); + sm.put(KEY_EXPECTED_MOVEMENT, DEFAULT_EXPECTED_MOVEMENT); + return sm; + } + + @Override + public boolean checkSettingsValidity(final Map settings) { + if (null == settings) { + errorMessage = "Settings map is null.\n"; + return false; + } + + boolean ok = true; + final StringBuilder str = new StringBuilder(); + + ok = ok & checkParameter(settings, KEY_LINKING_MAX_DISTANCE, Double.class, str); + ok = ok & checkParameter(settings, KEY_KALMAN_SEARCH_RADIUS, Double.class, str); + ok = ok & checkParameter(settings, KEY_GAP_CLOSING_MAX_FRAME_GAP, Integer.class, str); + ok = ok & checkParameter(settings, KEY_EXPECTED_MOVEMENT, double[].class, str); + + if (!ok) { + errorMessage = str.toString(); + } + return ok; + } + + @Override + public String getErrorMessage() { + return errorMessage; + } + + @Override + public KalmanTrackerFactory copy() { + return new KalmanTrackerFactory(); + } + + private boolean readDoubleArrayAttribute(Element element, Map settings, String key, StringBuilder errorHolder) { + try { + String str = element.getAttributeValue(key); + String[] values = str.split(","); + double[] array = new double[values.length]; + for (int i = 0; i < values.length; i++) { + array[i] = Double.parseDouble(values[i]); + } + settings.put(key, array); + return true; + } catch (Exception e) { + errorHolder.append("Could not read double array for key " + key + "\n"); + return false; + } + } } diff --git a/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest.java b/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest.java index d07627e35..e6e74a380 100644 --- a/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest.java +++ b/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest.java @@ -170,7 +170,7 @@ private Model test( final SpotCollection spots ) final double maxSearchRadius = 2 * WIDTH / NFRAMES; // small final int maxFrameGap = 2; final double initialSearchRadius = 2 * WIDTH / ( NFRAMES ); - final KalmanTracker tracker = new KalmanTracker( spots, maxSearchRadius, maxFrameGap, initialSearchRadius, null ); + final KalmanTracker tracker = new KalmanTracker( spots, maxSearchRadius, maxFrameGap, initialSearchRadius, null , null); if ( !tracker.checkInput() || !tracker.process() ) System.err.println( tracker.getErrorMessage() ); diff --git a/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest2.java b/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest2.java index 35ebed9d6..17a5a8129 100644 --- a/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest2.java +++ b/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest2.java @@ -54,7 +54,7 @@ public static void main( final String[] args ) final SpotCollection spots = model.getSpots(); final ImagePlus imp = reader.readImage(); - final KalmanTracker tracker = new KalmanTracker( spots, 15d, 2, 15d, null ); + final KalmanTracker tracker = new KalmanTracker( spots, 15d, 2, 15d, null , null); tracker.setLogger( Logger.DEFAULT_LOGGER ); if ( !tracker.checkInput() || !tracker.process() ) { diff --git a/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest3.java b/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest3.java index ae55af4bd..d62fa0fd3 100755 --- a/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest3.java +++ b/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest3.java @@ -55,7 +55,7 @@ private Model test( final SpotCollection spots ) final double maxSearchRadius = 2 * WIDTH / NFRAMES; // small final int maxFrameGap = 2; final double initialSearchRadius = 2 * WIDTH / ( NFRAMES ); - final KalmanTracker tracker = new KalmanTracker( spots, maxSearchRadius, maxFrameGap, initialSearchRadius, null ); + final KalmanTracker tracker = new KalmanTracker( spots, maxSearchRadius, maxFrameGap, initialSearchRadius, null , null); tracker.setLogger( Logger.DEFAULT_LOGGER ); if ( !tracker.checkInput() || !tracker.process() ) System.err.println( tracker.getErrorMessage() ); From 1e3b589ac4588dcaa88ba781dfe6f64b2e3a8ed6 Mon Sep 17 00:00:00 2001 From: Lollum89 Date: Thu, 16 May 2024 17:03:01 +0200 Subject: [PATCH 2/4] fixed issues with AdvancedKalmanTracker. Maybe still issue with number locale (1.2,2.1 OR 1,2;2,1). it works for int --- ...anelAdvancedKalmanTrackerSettingsMain.java | 161 +++++++++--------- .../kalman/AdvancedKalmanTracker.java | 2 + .../kalman/AdvancedKalmanTrackerFactory.java | 6 +- .../tracking/kalman/KalmanTrackerFactory.java | 6 +- 4 files changed, 92 insertions(+), 83 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelAdvancedKalmanTrackerSettingsMain.java b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelAdvancedKalmanTrackerSettingsMain.java index eb721e124..0f6cf9318 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelAdvancedKalmanTrackerSettingsMain.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelAdvancedKalmanTrackerSettingsMain.java @@ -50,6 +50,8 @@ import java.awt.Insets; import java.awt.event.MouseWheelListener; import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -122,6 +124,7 @@ public class JPanelAdvancedKalmanTrackerSettingsMain extends javax.swing.JPanel // Add these fields in the class private final JLabel lblExpectedMovement; private final JTextField txtfldExpectedMovement; + private final JLabel lblExpectedMovementUnits; public JPanelAdvancedKalmanTrackerSettingsMain( final String trackerName, final String spaceUnits, final Collection< String > features, final Map< String, String > featureNames ) { @@ -194,19 +197,25 @@ public JPanelAdvancedKalmanTrackerSettingsMain( final String trackerName, final lblExpectedMovement.setText("Expected movement (X,Y,Z):"); lblExpectedMovement.setFont(SMALL_FONT); - txtfldExpectedMovement = new JTextField("0,0,0"); + txtfldExpectedMovement = new JTextField(); this.add(txtfldExpectedMovement, new GridBagConstraints(1, ycur, 2, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0)); txtfldExpectedMovement.setFont(SMALL_FONT); - txtfldSearchRadius.setSize( TEXTFIELD_DIMENSION ); + txtfldExpectedMovement.setSize( TEXTFIELD_DIMENSION ); txtfldExpectedMovement.setHorizontalAlignment(JTextField.CENTER); + + lblExpectedMovementUnits = new JLabel(); + this.add( lblExpectedMovementUnits, new GridBagConstraints( 2, ycur, 1, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets( 0, 5, 0, 0 ), 0, 0 ) ); + lblExpectedMovementUnits.setFont( SMALL_FONT ); + lblExpectedMovementUnits.setText( spaceUnits ); - // Adding input validation txtfldExpectedMovement.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { - if (!validateExpectedMovementInput(txtfldExpectedMovement.getText())) { - JOptionPane.showMessageDialog(null, "Invalid input. Please enter a comma-separated list of three numbers (e.g., '1.0,0.0,0.0').", "Invalid Input", JOptionPane.ERROR_MESSAGE); - //txtfldExpectedMovement.setText("0,0,0"); + try { + parseExpectedMovement(txtfldExpectedMovement.getText()); + } catch (IllegalArgumentException ex) { + JOptionPane.showMessageDialog(null, ex.getMessage(), "Invalid Input", JOptionPane.ERROR_MESSAGE); + txtfldExpectedMovement.setText("0.0,0.0,0.0"); // Reset to default or handle as necessary } } }); @@ -368,66 +377,69 @@ public void focusLost(FocusEvent e) { * PUBLIC METHODS */ - @SuppressWarnings("unchecked") - void echoSettings(final Map settings) { - txtfldInitialSearchRadius.setValue(settings.get(KEY_LINKING_MAX_DISTANCE)); - if (settings.get(KEY_KALMAN_SEARCH_RADIUS) == null) - txtfldSearchRadius.setValue(DEFAULT_KALMAN_SEARCH_RADIUS); - else - txtfldSearchRadius.setValue(settings.get(KEY_KALMAN_SEARCH_RADIUS)); - txtfldMaxFrameGap.setValue(settings.get(KEY_GAP_CLOSING_MAX_FRAME_GAP)); - panelKalmanFeatures.setSelectedFeaturePenalties((Map) settings.get(KEY_LINKING_FEATURE_PENALTIES)); - - chkboxAllowSplitting.setSelected((Boolean) settings.get(KEY_ALLOW_TRACK_SPLITTING)); - txtfldSplittingMaxDistance.setValue(settings.get(KEY_SPLITTING_MAX_DISTANCE)); - panelSplittingFeatures.setSelectedFeaturePenalties((Map) settings.get(KEY_SPLITTING_FEATURE_PENALTIES)); - - chkboxAllowMerging.setSelected((Boolean) settings.get(KEY_ALLOW_TRACK_MERGING)); - txtfldMergingMaxDistance.setValue(settings.get(KEY_MERGING_MAX_DISTANCE)); - panelMergingFeatures.setSelectedFeaturePenalties((Map) settings.get(KEY_MERGING_FEATURE_PENALTIES)); - - // Echo expected movement - double[] expectedMovement = (double[]) settings.get(KEY_EXPECTED_MOVEMENT); - txtfldExpectedMovement.setText(String.format("%.1f,%.1f,%.1f", expectedMovement[0], expectedMovement[1], expectedMovement[2])); - - setEnabled(new Component[]{ - lbl10, txtfldSplittingMaxDistance, lblSplittingMaxDistanceUnit, - lbl15, scrpneSplittingFeatures, panelSplittingFeatures}, - chkboxAllowSplitting.isSelected()); - - setEnabled(new Component[]{ - lbl13, txtfldMergingMaxDistance, lblMergingMaxDistanceUnit, - lbl16, scrpneMergingFeatures, panelMergingFeatures}, - chkboxAllowMerging.isSelected()); - } + @SuppressWarnings("unchecked") + void echoSettings(final Map settings) { + txtfldInitialSearchRadius.setValue(settings.get(KEY_LINKING_MAX_DISTANCE)); + if (settings.get(KEY_KALMAN_SEARCH_RADIUS) == null) + txtfldSearchRadius.setValue(DEFAULT_KALMAN_SEARCH_RADIUS); + else + txtfldSearchRadius.setValue(settings.get(KEY_KALMAN_SEARCH_RADIUS)); + txtfldMaxFrameGap.setValue(settings.get(KEY_GAP_CLOSING_MAX_FRAME_GAP)); + panelKalmanFeatures.setSelectedFeaturePenalties((Map) settings.get(KEY_LINKING_FEATURE_PENALTIES)); + + chkboxAllowSplitting.setSelected((Boolean) settings.get(KEY_ALLOW_TRACK_SPLITTING)); + txtfldSplittingMaxDistance.setValue(settings.get(KEY_SPLITTING_MAX_DISTANCE)); + panelSplittingFeatures.setSelectedFeaturePenalties((Map) settings.get(KEY_SPLITTING_FEATURE_PENALTIES)); + + chkboxAllowMerging.setSelected((Boolean) settings.get(KEY_ALLOW_TRACK_MERGING)); + txtfldMergingMaxDistance.setValue(settings.get(KEY_MERGING_MAX_DISTANCE)); + panelMergingFeatures.setSelectedFeaturePenalties((Map) settings.get(KEY_MERGING_FEATURE_PENALTIES)); + + // Echo expected movement + double[] expectedMovement = (double[]) settings.get(KEY_EXPECTED_MOVEMENT); + txtfldExpectedMovement.setText(String.format(Locale.US,"%.1f,%.1f,%.1f", + expectedMovement[0], expectedMovement[1], expectedMovement[2])); + + setEnabled(new Component[]{ + lbl10, txtfldSplittingMaxDistance, lblSplittingMaxDistanceUnit, + lbl15, scrpneSplittingFeatures, panelSplittingFeatures}, + chkboxAllowSplitting.isSelected()); + + setEnabled(new Component[]{ + lbl13, txtfldMergingMaxDistance, lblMergingMaxDistanceUnit, + lbl16, scrpneMergingFeatures, panelMergingFeatures}, + chkboxAllowMerging.isSelected()); + } + + /** * @return a new settings {@link Map} with values taken from this panel. */ - public Map< String, Object > getSettings() - { - final Map< String, Object > settings = getDefaultKalmanSettingsMap(); - - settings.put( KEY_LINKING_MAX_DISTANCE, ( ( Number ) txtfldInitialSearchRadius.getValue() ).doubleValue() ); - settings.put( KEY_KALMAN_SEARCH_RADIUS, ( ( Number ) txtfldSearchRadius.getValue() ).doubleValue() ); - settings.put( KEY_GAP_CLOSING_MAX_FRAME_GAP, ( ( Number ) txtfldMaxFrameGap.getValue() ).intValue() ); - settings.put( KEY_LINKING_FEATURE_PENALTIES, panelKalmanFeatures.getFeaturePenalties() ); - - settings.put( KEY_ALLOW_GAP_CLOSING, false ); - - settings.put( KEY_ALLOW_TRACK_SPLITTING, chkboxAllowSplitting.isSelected() ); - settings.put( KEY_SPLITTING_MAX_DISTANCE, ( ( Number ) txtfldSplittingMaxDistance.getValue() ).doubleValue() ); - settings.put( KEY_SPLITTING_FEATURE_PENALTIES, panelSplittingFeatures.getFeaturePenalties() ); - - settings.put( KEY_ALLOW_TRACK_MERGING, chkboxAllowMerging.isSelected() ); - settings.put( KEY_MERGING_MAX_DISTANCE, ( ( Number ) txtfldMergingMaxDistance.getValue() ).doubleValue() ); - settings.put( KEY_MERGING_FEATURE_PENALTIES, panelMergingFeatures.getFeaturePenalties() ); - + public Map getSettings() { + final Map settings = getDefaultKalmanSettingsMap(); + + settings.put(KEY_LINKING_MAX_DISTANCE, ((Number) txtfldInitialSearchRadius.getValue()).doubleValue()); + settings.put(KEY_KALMAN_SEARCH_RADIUS, ((Number) txtfldSearchRadius.getValue()).doubleValue()); + settings.put(KEY_GAP_CLOSING_MAX_FRAME_GAP, ((Number) txtfldMaxFrameGap.getValue()).intValue()); + settings.put(KEY_LINKING_FEATURE_PENALTIES, panelKalmanFeatures.getFeaturePenalties()); + + settings.put(KEY_ALLOW_GAP_CLOSING, false); + + settings.put(KEY_ALLOW_TRACK_SPLITTING, chkboxAllowSplitting.isSelected()); + settings.put(KEY_SPLITTING_MAX_DISTANCE, ((Number) txtfldSplittingMaxDistance.getValue()).doubleValue()); + settings.put(KEY_SPLITTING_FEATURE_PENALTIES, panelSplittingFeatures.getFeaturePenalties()); + + settings.put(KEY_ALLOW_TRACK_MERGING, chkboxAllowMerging.isSelected()); + settings.put(KEY_MERGING_MAX_DISTANCE, ((Number) txtfldMergingMaxDistance.getValue()).doubleValue()); + settings.put(KEY_MERGING_FEATURE_PENALTIES, panelMergingFeatures.getFeaturePenalties()); + // Save expected movement settings.put(KEY_EXPECTED_MOVEMENT, parseExpectedMovement(txtfldExpectedMovement.getText())); - + return settings; } + public static final Map< String, Object > getDefaultKalmanSettingsMap() { @@ -443,31 +455,26 @@ public static final Map< String, Object > getDefaultKalmanSettingsMap() * PRIVATE METHODS */ - // Add this method in the class for input validation - private boolean validateExpectedMovementInput(String input) { - String[] parts = input.split(","); - if (parts.length != 3) return false; - try { - for (String part : parts) { - Double.parseDouble(part.trim()); - } - } catch (NumberFormatException e) { - return false; - } - return true; - } - // Add this method to parse the expected movement from the text field + // Parse the expected movement from the text field private double[] parseExpectedMovement(String text) { - String[] parts = text.split(","); + String[] parts = text.split("[,;]"); // Allow both comma and semicolon as separators + if (parts.length != 3) { + throw new IllegalArgumentException("Input must be a comma-separated list of three numbers."); + } + double[] result = new double[3]; - for (int i = 0; i < parts.length; i++) { - result[i] = Double.parseDouble(parts[i].trim()); + try { + for (int i = 0; i < parts.length; i++) { + result[i] = Double.parseDouble(parts[i]); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Input must be a comma-separated list of three valid numbers.", e); } return result; } - - + + private void setEnabled( final Component[] components, final boolean enable ) { for ( final Component component : components ) diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kalman/AdvancedKalmanTracker.java b/src/main/java/fiji/plugin/trackmate/tracking/kalman/AdvancedKalmanTracker.java index 0bc789875..08d165246 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/kalman/AdvancedKalmanTracker.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/kalman/AdvancedKalmanTracker.java @@ -124,6 +124,7 @@ public boolean process() kalSettings.put( KEY_LINKING_MAX_DISTANCE, settings.get( KEY_LINKING_MAX_DISTANCE ) ); kalSettings.put( KEY_GAP_CLOSING_MAX_FRAME_GAP, settings.get( KEY_GAP_CLOSING_MAX_FRAME_GAP ) ); kalSettings.put( KEY_LINKING_FEATURE_PENALTIES, settings.get( KEY_LINKING_FEATURE_PENALTIES ) ); + kalSettings.put( KEY_EXPECTED_MOVEMENT, settings.get( KEY_EXPECTED_MOVEMENT ) ); // check these parameters if ( !checkSettingsValidity( kalSettings, errorHolder ) ) @@ -220,6 +221,7 @@ protected boolean checkSettingsValidity( final Map< String, Object > settings, f mandatoryKeys.add( KEY_KALMAN_SEARCH_RADIUS ); mandatoryKeys.add( KEY_LINKING_MAX_DISTANCE ); mandatoryKeys.add( KEY_GAP_CLOSING_MAX_FRAME_GAP ); + mandatoryKeys.add( KEY_EXPECTED_MOVEMENT ); final List< String > optionalKeys = new ArrayList<>(); optionalKeys.add( KEY_LINKING_FEATURE_PENALTIES ); diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kalman/AdvancedKalmanTrackerFactory.java b/src/main/java/fiji/plugin/trackmate/tracking/kalman/AdvancedKalmanTrackerFactory.java index 1edfa6faa..bfdca2113 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/kalman/AdvancedKalmanTrackerFactory.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/kalman/AdvancedKalmanTrackerFactory.java @@ -220,9 +220,9 @@ public String toString( final Map< String, Object > sm ) final double initialSearchRadius = ( Double ) sm.get( KEY_LINKING_MAX_DISTANCE ); final double[] expectedMovement = (double[]) sm.get(KEY_EXPECTED_MOVEMENT); - str.append( String.format( " - initial search radius: %.1f\n", initialSearchRadius ) ); - str.append( String.format( " - search radius: %.1f\n", maxSearchRadius ) ); - str.append( String.format( " - expected movement: [%.1f, %.1f, %.1f]\n", expectedMovement[0], expectedMovement[1], expectedMovement[2] ) ); + str.append( String.format( " - initial search radius: %g\n", initialSearchRadius ) ); + str.append( String.format( " - search radius: %g\n", maxSearchRadius ) ); + str.append( String.format( " - expected movement: [%g, %g, %g]\n", expectedMovement[0], expectedMovement[1], expectedMovement[2] ) ); str.append( " Linking conditions:\n" ); str.append( LAPUtils.echoFeaturePenalties( ( Map< String, Double > ) sm.get( KEY_LINKING_FEATURE_PENALTIES ) ) ); diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerFactory.java b/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerFactory.java index f87bdaa85..60b32fc64 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerFactory.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerFactory.java @@ -140,10 +140,10 @@ public String toString(final Map settings) { final double[] expectedMovement = (double[]) settings.get(KEY_EXPECTED_MOVEMENT); final StringBuilder str = new StringBuilder(); - str.append(String.format(" - initial search radius: %.1f\n", initialSearchRadius)); - str.append(String.format(" - max search radius: %.1f\n", maxSearchRadius)); + str.append(String.format(" - initial search radius: %g\n", initialSearchRadius)); + str.append(String.format(" - max search radius: %g\n", maxSearchRadius)); str.append(String.format(" - max frame gap: %d\n", maxFrameGap)); - str.append(String.format(" - expected movement: [%.1f, %.1f, %.1f]\n", expectedMovement[0], expectedMovement[1], expectedMovement[2])); + str.append(String.format(" - expected movement: [%g, %g, %g]\n", expectedMovement[0], expectedMovement[1], expectedMovement[2])); return str.toString(); } From ff1f08889646c9ef5658feb6eb72614902bb2a4c Mon Sep 17 00:00:00 2001 From: Lollum89 Date: Fri, 17 May 2024 15:26:37 +0200 Subject: [PATCH 3/4] modifying locale for the array and semicolon instead of comma --- ...JPanelAdvancedKalmanTrackerSettingsMain.java | 17 ++++++++--------- .../tracker/KalmanTrackerConfigPanel.java | 12 ++++++------ .../tracking/kalman/KalmanTrackerFactory.java | 4 ++-- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelAdvancedKalmanTrackerSettingsMain.java b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelAdvancedKalmanTrackerSettingsMain.java index 0f6cf9318..2a6c18462 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelAdvancedKalmanTrackerSettingsMain.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelAdvancedKalmanTrackerSettingsMain.java @@ -50,8 +50,6 @@ import java.awt.Insets; import java.awt.event.MouseWheelListener; import java.text.DecimalFormat; -import java.text.DecimalFormatSymbols; -import java.util.Locale; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -194,10 +192,10 @@ public JPanelAdvancedKalmanTrackerSettingsMain( final String trackerName, final ycur++; lblExpectedMovement = new JLabel(); this.add(lblExpectedMovement, new GridBagConstraints(0, ycur, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(0, 10, 0, 10), 0, 0)); - lblExpectedMovement.setText("Expected movement (X,Y,Z):"); + lblExpectedMovement.setText("Expected movement ( X;Y;Z ):"); lblExpectedMovement.setFont(SMALL_FONT); - txtfldExpectedMovement = new JTextField(); + txtfldExpectedMovement = new JTextField("0;0;0"); this.add(txtfldExpectedMovement, new GridBagConstraints(1, ycur, 2, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0)); txtfldExpectedMovement.setFont(SMALL_FONT); txtfldExpectedMovement.setSize( TEXTFIELD_DIMENSION ); @@ -215,7 +213,7 @@ public void focusLost(FocusEvent e) { parseExpectedMovement(txtfldExpectedMovement.getText()); } catch (IllegalArgumentException ex) { JOptionPane.showMessageDialog(null, ex.getMessage(), "Invalid Input", JOptionPane.ERROR_MESSAGE); - txtfldExpectedMovement.setText("0.0,0.0,0.0"); // Reset to default or handle as necessary + txtfldExpectedMovement.setText("0;0;0"); // Reset to default or handle as necessary } } }); @@ -397,7 +395,7 @@ void echoSettings(final Map settings) { // Echo expected movement double[] expectedMovement = (double[]) settings.get(KEY_EXPECTED_MOVEMENT); - txtfldExpectedMovement.setText(String.format(Locale.US,"%.1f,%.1f,%.1f", + txtfldExpectedMovement.setText(String.format("%g;%g;%g", expectedMovement[0], expectedMovement[1], expectedMovement[2])); setEnabled(new Component[]{ @@ -458,9 +456,9 @@ public static final Map< String, Object > getDefaultKalmanSettingsMap() // Parse the expected movement from the text field private double[] parseExpectedMovement(String text) { - String[] parts = text.split("[,;]"); // Allow both comma and semicolon as separators + String[] parts = text.split(";"); // Allow semicolon as separator if (parts.length != 3) { - throw new IllegalArgumentException("Input must be a comma-separated list of three numbers."); + throw new IllegalArgumentException("Input must be a semicolon-separated list of three numbers."); } double[] result = new double[3]; @@ -469,7 +467,7 @@ private double[] parseExpectedMovement(String text) { result[i] = Double.parseDouble(parts[i]); } } catch (NumberFormatException e) { - throw new IllegalArgumentException("Input must be a comma-separated list of three valid numbers.", e); + throw new IllegalArgumentException("Input must be a semicolon-separated list of three valid numbers.", e); } return result; } @@ -481,3 +479,4 @@ private void setEnabled( final Component[] components, final boolean enable ) component.setEnabled( enable ); } } + diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/KalmanTrackerConfigPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/KalmanTrackerConfigPanel.java index f597616c1..0297752c2 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/KalmanTrackerConfigPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/KalmanTrackerConfigPanel.java @@ -70,7 +70,7 @@ public KalmanTrackerConfigPanel(final String trackerName, final String infoText, lblMaxFrameGap.setBounds(6, 404, 173, 16); add(lblMaxFrameGap); - final JLabel lblExpectedMovement = new JLabel("Expected movement X,Y,Z:"); // New label + final JLabel lblExpectedMovement = new JLabel("Expected movement ( X;Y;Z ):"); // New label lblExpectedMovement.setFont(FONT); lblExpectedMovement.setBounds(6, 432, 250, 16); // Adjust position as needed add(lblExpectedMovement); @@ -96,7 +96,7 @@ public KalmanTrackerConfigPanel(final String trackerName, final String infoText, add(tfMaxFrameGap); tfMaxFrameGap.setSize(TEXTFIELD_DIMENSION); - tfExpectedMovement = new JTextField("0,0,0"); // Initialize with default value + tfExpectedMovement = new JTextField("0;0;0"); // Initialize with default value tfExpectedMovement.setHorizontalAlignment(SwingConstants.CENTER); tfExpectedMovement.setFont(FONT); tfExpectedMovement.setBounds(167, 432, 100, 28); // Adjust position as needed @@ -107,8 +107,8 @@ public KalmanTrackerConfigPanel(final String trackerName, final String infoText, @Override public void focusLost(FocusEvent e) { if (!validateExpectedMovementInput(tfExpectedMovement.getText())) { - JOptionPane.showMessageDialog(null, "Invalid input. Please enter a comma-separated list of three numbers (e.g., '1.0,0.0,0.0').", "Invalid Input", JOptionPane.ERROR_MESSAGE); - tfExpectedMovement.setText("0,0,0"); + JOptionPane.showMessageDialog(null, "Invalid input. Please enter a comma-separated list of three numbers (e.g., '123;0;0').", "Invalid Input", JOptionPane.ERROR_MESSAGE); + tfExpectedMovement.setText("0;0;0"); } } }); @@ -136,7 +136,7 @@ public void focusLost(FocusEvent e) { } private boolean validateExpectedMovementInput(String input) { - String[] parts = input.split(","); + String[] parts = input.split(";"); if (parts.length != 3) return false; try { @@ -155,7 +155,7 @@ public void setSettings(final Map settings) { tfSearchRadius.setValue(settings.get(KEY_KALMAN_SEARCH_RADIUS)); tfMaxFrameGap.setValue(settings.get(KEY_GAP_CLOSING_MAX_FRAME_GAP)); double[] expectedMovementArray = (double[]) settings.get(KEY_EXPECTED_MOVEMENT); - tfExpectedMovement.setText(expectedMovementArray[0] + "," + expectedMovementArray[1] + "," + expectedMovementArray[2]); + tfExpectedMovement.setText(expectedMovementArray[0] + ";" + expectedMovementArray[1] + ";" + expectedMovementArray[2]); } @Override diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerFactory.java b/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerFactory.java index 60b32fc64..1c6b5bea5 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerFactory.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerFactory.java @@ -143,7 +143,7 @@ public String toString(final Map settings) { str.append(String.format(" - initial search radius: %g\n", initialSearchRadius)); str.append(String.format(" - max search radius: %g\n", maxSearchRadius)); str.append(String.format(" - max frame gap: %d\n", maxFrameGap)); - str.append(String.format(" - expected movement: [%g, %g, %g]\n", expectedMovement[0], expectedMovement[1], expectedMovement[2])); + str.append(String.format(" - expected movement: [%g; %g; %g]\n", expectedMovement[0], expectedMovement[1], expectedMovement[2])); return str.toString(); } @@ -192,7 +192,7 @@ public KalmanTrackerFactory copy() { private boolean readDoubleArrayAttribute(Element element, Map settings, String key, StringBuilder errorHolder) { try { String str = element.getAttributeValue(key); - String[] values = str.split(","); + String[] values = str.split(";"); double[] array = new double[values.length]; for (int i = 0; i < values.length; i++) { array[i] = Double.parseDouble(values[i]); From 2a97ebb768bf6f609e879ae2efb588a6a8c5241c Mon Sep 17 00:00:00 2001 From: Lollum89 Date: Fri, 17 May 2024 15:43:50 +0200 Subject: [PATCH 4/4] changed echo for expectedMeasurement from %g back to %.1f --- .../tracker/JPanelAdvancedKalmanTrackerSettingsMain.java | 2 +- .../trackmate/tracking/kalman/KalmanTrackerFactory.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelAdvancedKalmanTrackerSettingsMain.java b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelAdvancedKalmanTrackerSettingsMain.java index 2a6c18462..a95d545da 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelAdvancedKalmanTrackerSettingsMain.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelAdvancedKalmanTrackerSettingsMain.java @@ -395,7 +395,7 @@ void echoSettings(final Map settings) { // Echo expected movement double[] expectedMovement = (double[]) settings.get(KEY_EXPECTED_MOVEMENT); - txtfldExpectedMovement.setText(String.format("%g;%g;%g", + txtfldExpectedMovement.setText(String.format("%.1f;%.1f;%.1f", expectedMovement[0], expectedMovement[1], expectedMovement[2])); setEnabled(new Component[]{ diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerFactory.java b/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerFactory.java index 1c6b5bea5..4e34907bc 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerFactory.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerFactory.java @@ -140,10 +140,10 @@ public String toString(final Map settings) { final double[] expectedMovement = (double[]) settings.get(KEY_EXPECTED_MOVEMENT); final StringBuilder str = new StringBuilder(); - str.append(String.format(" - initial search radius: %g\n", initialSearchRadius)); - str.append(String.format(" - max search radius: %g\n", maxSearchRadius)); + str.append(String.format(" - initial search radius: %.1f\n", initialSearchRadius)); + str.append(String.format(" - max search radius: %.1f\n", maxSearchRadius)); str.append(String.format(" - max frame gap: %d\n", maxFrameGap)); - str.append(String.format(" - expected movement: [%g; %g; %g]\n", expectedMovement[0], expectedMovement[1], expectedMovement[2])); + str.append(String.format(" - expected movement: [%.1f; %.1f; %g]\n", expectedMovement[0], expectedMovement[1], expectedMovement[2])); return str.toString(); }