au.com.nicta.ct.solution.tracking.jipda.CtTkDBTracker.java Source code

Java tutorial

Introduction

Here is the source code for au.com.nicta.ct.solution.tracking.jipda.CtTkDBTracker.java

Source

// ====================================================================================================================
// Copyright (c) 2013, National ICT Australia Ltd and The Walter and Eliza Hall Institute of Medical Research.
// All rights reserved.
//
// This software and source code is made available under a GPL v2 licence.
// The terms of the licence can be read here: http://www.gnu.org/licenses/gpl-2.0.txt
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// ====================================================================================================================

package au.com.nicta.ct.solution.tracking.jipda;

import au.com.nicta.ct.orm.patterns.CtObjectDirectory;
import au.com.nicta.ct.db.CtSession;
import au.com.nicta.ct.db.hibernate.CtCoordinates;
import au.com.nicta.ct.db.hibernate.CtCoordinatesTypes;
import au.com.nicta.ct.db.hibernate.CtDetections;
import au.com.nicta.ct.db.hibernate.CtImagesCoordinates;
import au.com.nicta.ct.db.hibernate.CtSolutions;
import au.com.nicta.ct.db.hibernate.CtTracks;
import au.com.nicta.ct.db.hibernate.CtTracksDetections;
import au.com.nicta.ct.experiment.graphics.canvas.microwells.CtMicrowell;
import au.com.nicta.ct.experiment.graphics.canvas.microwells.CtMicrowellsController;
import au.com.nicta.ct.experiment.graphics.canvas.microwells.CtMicrowellsModel;
import au.com.nicta.ct.graphics.canvas.zoom.polygons.CtRegion;
import au.com.nicta.ct.graphics.canvas.zoom.polygons.CtSubPixelResolution;
import au.com.nicta.ct.graphics.canvas.zoom.polygons.CtZoomPolygon;
import au.com.nicta.ct.solution.CtSolutionController;
import au.com.nicta.tk.TkCell;
import au.com.nicta.tk.TkDetection;
import au.com.nicta.tk.TkDetections;
import au.com.nicta.tk.TkDetectionsSeries;
import au.com.nicta.tk.TkHandleDivisions;
import au.com.nicta.tk.TkKalmanFilter;
import au.com.nicta.tk.TkLJIPDAForcedAssociator;
import au.com.nicta.tk.TkLJIPDAState;
import au.com.nicta.tk.TkLJIPDATracker;
import au.com.nicta.tk.TkPruneTracks;
import au.com.nicta.tk.tree.TkStringKey;
import au.com.nicta.tk.TkTrack;
import au.com.nicta.tk.TkTrackElement;
import au.com.nicta.tk.TkTracks;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.hibernate.Query;
import org.hibernate.Session;
import org.ujmp.core.MatrixFactory;
import org.ujmp.core.enums.ValueType;

/**
 *
 * @author Alan
 */
public class CtTkDBTracker {

    static final TkStringKey<String> detectionDescriptionKey = TkStringKey.newInstance("Description");
    static final TkStringKey<Integer> DETECTION_PK = TkStringKey.newInstance("detection-pk");

    HashMap<Integer, String> _stepsMessages = new HashMap<Integer, String>();
    CtTrackerState _ts = new CtTrackerState();
    TkPruneTracks pruneTracks = new TkPruneTracks();

    CtParaTracker _para;
    String microWellName;

    public CtTkDBTracker(CtParaTracker para) {
        _para = para;
        _stepsMessages.put(0, "Loading detections");
        _stepsMessages.put(1, "Forcing associations");
        _stepsMessages.put(2, "Creating tracker");
        _stepsMessages.put(3, "Running tracker");
        _stepsMessages.put(4, "Handling divisions");
        _stepsMessages.put(5, "Saving tracks");
    }

    public void setMicroWellName(String s) {
        microWellName = s;
    }

    public String getMicroWellName() {
        return microWellName;
    }

    public String findMessage(int step) {
        return _stepsMessages.get(step);
    }

    public int steps() {
        return 8;
    }

    //    // progress state {
    //    int _startTimeIdxInclusive = -1;//120;
    //    int   _endTimeIdxInclusive = -1;//130;
    //    TkDetectionsSeries _series;
    //    TkLJIPDATracker _tracker;
    //    TkTracks _tracks;
    //    // }

    public void doAllSteps() {
        int steps = steps();

        for (int n = 0; n < steps; ++n) {
            doStep(n);
        }
    }

    public void doStep(int step) {
        System.out.println("DOING STEP #" + step);
        //  try {
        //          Thread.sleep( 800 );
        //        }catch ( Exception e ){
        //            e.printStackTrace();
        //            System.exit( -1 );
        //        }
        //        return;

        // *** 1 ***
        if (step == 0) {
            _ts._startTimeIdxInclusive = -1;//120;
            //        int   endTimeIdxInclusive = Integer.MAX_VALUE;
            _ts._endTimeIdxInclusive = -1;//130;
            _ts._series = loadDetections(_ts._startTimeIdxInclusive, _ts._endTimeIdxInclusive);
            return;
        }

        // find the first and last time idx
        // *** 2 ***
        if (step == 1) {
            TkDetections first = _ts._series.get(0);
            _ts._startTimeIdxInclusive = first.get(0).timeIdx;

            TkDetections last = _ts._series.get(_ts._series.size() - 1);
            _ts._endTimeIdxInclusive = last.get(last.size() - 1).timeIdx;

            // force any manual associations
            //            forceAssociate( _ts._series );
            return;
        }

        // run tracker
        // *** 3 ***
        if (step == 2) {
            _ts._tracker = createTracker();
            return;
        }

        // *** 4 ***
        if (step == 3) {
            _ts._tracks = runTracker(_ts._tracker, _ts._series);
            return;
        }

        // post process tracks to remove some sperious tracks
        // *** 5 ***
        if (step == 4) {
            //            pruneTracks.minNumDetectionsPerFinalTrack = 0; // want to not filter anything on this criteria
            _ts._tracks = pruneTracks.process(_ts._tracks);
            return;
        }

        // *** 6 ***
        if (step == 5) {
            TkHandleDivisions handleDivisions = new TkHandleDivisions(_ts._tracker.stateKey);
            handleDivisions.process(_ts._tracks);

            for (int i = 0; i < _ts._tracks.size(); ++i) {
                if (_ts._tracks.get(i).elements.get(0).det == null) {
                    throw new Error("Null detection not allowed as first detection of track.");
                }
            }
            return;
        }

        // post process tracks to remove some sperious tracks
        // *** 7 ***
        if (step == 6) {
            //            pruneTracks.minNumDetectionsPerFinalTrack = 0;
            _ts._tracks = pruneTracks.process(_ts._tracks);
            return;
        }

        // *** 8 ***
        if (step == 7) {
            saveTracks(_ts._tracks, _ts._startTimeIdxInclusive, _ts._endTimeIdxInclusive);
        }
    }

    /*    public static void forceAssociate( TkDetectionsSeries series ) {
    // force associate detections between time 0 and 1
        
    //        int cnt = Math.min( series.get(0).size(), series.get(1).size() );
        
    // Force cross tracks
    // --------------------------------------------------------------------
    //        for( int i = 0; i < cnt; ++i ) {
    //            TkDetection t0 = series.get(0).get( (i+0)%cnt );
    //            TkDetection t1 = series.get(1).get( (i+1)%cnt );
    //
    //            TkDetectionAssocInfo info;
    //
    //            info = new TkDetectionAssocInfo();
    //            info.prev = null;
    //            info.next = t1;
    //            t0.add( TkDetectionAssocInfo.name, info );
    //
    //            info = new TkDetectionAssocInfo();
    //            info.prev = t0;
    //            info.next = null;
    //            t1.add( TkDetectionAssocInfo.name, info );
    //        }
        
    // Force track termination
    // --------------------------------------------------------------------
    //        {
    //            TkDetection d = series.get(2).get(0);
    //
    //            TkDetectionAssocInfo info;
    //
    //            info = new TkDetectionAssocInfo();
    //            info.terminateTrack = true;
    //            d.add( TkDetectionAssocInfo.name, info );
    //        }
        
    // Force missed detection, not supported.
    // --------------------------------------------------------------------
    //        {
    //            TkDetection t0 = series.get(1).get(0);
    //            TkDetection t2 = series.get(3).get(0);
    //
    //            TkDetectionAssocInfo info;
    //
    //            info = new TkDetectionAssocInfo();
    //            info.prev = null;
    //            info.next = t2;
    //            t0.add( TkDetectionAssocInfo.name, info );
    //
    //            info = new TkDetectionAssocInfo();
    //            info.prev = t0;
    //            info.next = null;
    //            t2.add( TkDetectionAssocInfo.name, info );
    //        }
        }*/

    public TkLJIPDATracker createTracker() {
        TkStringKey<TkLJIPDAState> stateKey = TkStringKey.newInstance("state");
        TkLJIPDATracker tracker = new TkLJIPDATracker(2, 2, stateKey);

        //        // sensor parameters
        //        TkKalmanFilter kf = (TkKalmanFilter) tracker.getKalmanFilter();
        //        kf.msurMatrix      = MatrixFactory.eye( ValueType.DOUBLE, tracker.msurDims, tracker.stateDims );
        //        kf.msurNoiseCov    = MatrixFactory.eye( ValueType.DOUBLE, tracker.msurDims, tracker.msurDims ).times( 5.0 );
        //        kf.processMatrix   = MatrixFactory.eye( ValueType.DOUBLE, tracker.stateDims, tracker.stateDims );
        ////        kf.processNoiseCov = MatrixFactory.eye( ValueType.DOUBLE, tracker.stateDims, tracker.stateDims ).times( 125 );
        ////        kf.processNoiseCov = MatrixFactory.eye( ValueType.DOUBLE, tracker.stateDims, tracker.stateDims ).times( 900 );
        //        kf.processNoiseCov = MatrixFactory.eye( ValueType.DOUBLE, tracker.stateDims, tracker.stateDims ).times( 2700 );
        //
        //        TkLJIPDAForcedAssociator assoc = new TkLJIPDAForcedAssociator( stateKey );
        //        tracker.setAssociator( assoc );
        //        assoc.gateThreshold = 10;
        ////        assoc.PD = 0.75;
        ////        assoc.PG = 0.95;
        //        assoc.PD = 0.95;
        //        assoc.PG = 0.95;
        //
        //        tracker.existenceGamma = 0.95;
        //        tracker.initExistenceProb = 0.1;
        //        tracker.terminateExistenceThresh = 1e-3;

        // sensor parameters
        TkKalmanFilter kf = (TkKalmanFilter) tracker.getKalmanFilter();
        kf.msurMatrix = MatrixFactory.eye(ValueType.DOUBLE, tracker.msurDims, tracker.stateDims);
        kf.msurNoiseCov = MatrixFactory.eye(ValueType.DOUBLE, tracker.msurDims, tracker.msurDims).times(1);
        kf.processMatrix = MatrixFactory.eye(ValueType.DOUBLE, tracker.stateDims, tracker.stateDims);
        kf.processNoiseCov = MatrixFactory.eye(ValueType.DOUBLE, tracker.stateDims, tracker.stateDims)
                .times(_para.getProcessCov());
        ;
        //        TkLJIPDAForcedAssociator assoc = new TkLJIPDAForcedAssociator( stateKey );
        TkLJIPDAForcedAssociator assoc = new CtLJIPDAAssociator(stateKey);
        tracker.setAssociator(assoc);
        assoc.gateThreshold = 10;
        assoc.PD = 0.95;
        assoc.PG = 0.95;
        tracker.existenceGamma = _para.getGamma();
        tracker.initExistenceProb = _para.getPTS();
        tracker.terminateExistenceThresh = _para.getPTE();
        pruneTracks.maxMissingDetectionRatePerTrack = _para.getMaxMissingDetectionRate();
        pruneTracks.minNumDetectionsPerTrack = _para.getMinDetectionPerTrack();

        return tracker;
    }

    static List<TkDetection> findDetectionsInRange(int startTimeIdxInclusive, int endTimeIdxInclusive) {
        CtSolutions solution = (CtSolutions) CtObjectDirectory.get("solution");

        // Compile a list of detections that are within the time index range
        List<TkDetection> tkDetections = new ArrayList<TkDetection>();

        boolean checkTime = true;

        if ((startTimeIdxInclusive < 0) && (endTimeIdxInclusive < 0)) {
            checkTime = false;
        }

        // for each detection in solution
        for (CtDetections d : (Set<CtDetections>) solution.getCtDetectionses()) {

            // for each coordinate of detection
            for (CtImagesCoordinates ic : (Set<CtImagesCoordinates>) d.getCtImages().getCtImagesCoordinateses()) {

                CtCoordinates c = ic.getCtCoordinates();
                CtCoordinatesTypes ct = c.getCtCoordinatesTypes();

                // find the time coordinate
                if (!ct.getName().equals("time")) {
                    continue;
                }

                // make sure it's in range
                Integer time = c.getValue();
                System.out.println("time: " + (time));

                if (checkTime) {
                    if (time < startTimeIdxInclusive || time > endTimeIdxInclusive) {
                        continue;
                    }
                }

                CtZoomPolygon zp = new CtZoomPolygon(d.getBoundary());
                Point2D centre = zp.getCenter();
                Rectangle2D bb = zp.getBoundingBox();

                TkCell cell = new TkCell();
                cell.cx = centre.getX();
                cell.cy = centre.getY();
                cell.radius = Math.min(bb.getWidth(), bb.getHeight()) / 2;

                TkDetection tkDetection = new TkDetection();
                tkDetection.timeIdx = time;
                tkDetection.add(TkCell.name, cell);
                tkDetection.add(DETECTION_PK, d.getPkDetection());

                tkDetections.add(tkDetection);
            }
        }

        return tkDetections;
    }

    //    static TkDetectionsSeries loadDetections() {
    //        return loadDetections( 0, Integer.MAX_VALUE );
    //    }

    TkDetectionsSeries loadDetections(int startTimeIdxInclusive, int endTimeIdxInclusive) {

        if ((startTimeIdxInclusive > 0) && (endTimeIdxInclusive > 0)) { // if valid params selected
            if (startTimeIdxInclusive >= endTimeIdxInclusive) { // check ordering
                throw new IllegalArgumentException("failed: startTimeIdxInclusive < endTimeIdxInclusive");
            }
        }

        Session session = CtSession.Current();
        session.beginTransaction();

        List<TkDetection> allDetections = findDetectionsInRange(startTimeIdxInclusive, endTimeIdxInclusive);

        // filter out detections not in the uwell
        CtRegion roi = getMicroWellRoi(microWellName);
        if (roi != null) {
            filterDetectionsByRoi(allDetections, roi);
        }

        //        String hql =
        //                "SELECT ctDetections"
        //              + " FROM CtDetections as ctDetections";
        //
        //        Query q = session.createQuery( hql );
        //        List<Object[]> results = (List<Object[]>) q.list();
        //
        //        for( Object o : results ) {
        //            CtDetections ctDetection = (CtDetections) o;
        //            Set s = ctDetection.getCtImages().getCtImagesCoordinateses();
        //            Iterator i = s.iterator();
        //            while( i.hasNext() ) {
        //                CtImagesCoordinates ic = (CtImagesCoordinates)i.next();
        //                CtCoordinates c = ic.getCtCoordinates();
        //                CtCoordinatesTypes ct = c.getCtCoordinatesTypes();
        //                if( !ct.getName().equals( "time" ) ) {
        //                    continue;
        //                }
        //
        //                Integer time = c.getValue();
        //                System.out.println("time: " + (time) );
        //
        //                if(    time < startTimeIdxInclusive
        //                    || time > endTimeIdxInclusive ) {
        //                    continue;
        //                }
        //
        //                CtZoomPolygon zp = new CtZoomPolygon( ctDetection.getBoundary() );
        //                Point2D centre = zp.getCenter();
        //
        //                TkCell cell = new TkCell();
        //                cell.cx = centre.getX();
        //                cell.cy = centre.getY();
        //
        //                TkDetection tkDetection = new TkDetection();
        //                tkDetection.timeIdx = time;
        //                tkDetection.add( TkCell.name, cell );
        //                tkDetection.add( DETECTION_PK, ctDetection.getPkDetection() );
        //
        //                allDetections.add( tkDetection );
        //            }
        //        }

        // sort according to time
        Collections.sort(allDetections, TkDetection.timeIdxGreaterThan);

        // print detections
        for (TkDetection d : allDetections) {
            System.out.print(" " + d.timeIdx);
        }

        // print detections
        TkDetectionsSeries series = new TkDetectionsSeries();

        // break detections into blocks with the same time idx
        int timeIdx = allDetections.get(0).timeIdx;

        TkDetections detections = new TkDetections();
        series.add(detections);

        for (TkDetection d : allDetections) {
            while (timeIdx != d.timeIdx) {
                ++timeIdx;
                // create new detections
                detections = new TkDetections();
                series.add(detections);
            }

            detections.add(d);
        }

        // print the final results
        for (TkDetections ds : series) {
            System.out.println("----");
            for (TkDetection d : ds) {
                System.out.print(" " + d.timeIdx);
            }
            System.out.println("");
        }

        session.getTransaction().commit();

        return series;
    }

    TkTracks runTracker(TkLJIPDATracker tracker, TkDetectionsSeries series) {
        TkTracks tracks = new TkTracks();

        for (int step = 0; step < series.size(); ++step) {

            TkDetections dets = series.get(step);

            if (dets.isEmpty()) {
                System.out.println("NO DETECTIONS @ step: " + step);
                continue;
            }

            System.out.println("time idx: " + dets.get(0).timeIdx + " step: " + step);
            System.out.println("num detections: " + dets.size());

            tracker.predict(tracks);
            System.out.println("predict done---------------------------------------------");

            int numTracksBeforeUpdate = tracks.size();

            tracker.update(tracks, dets);
            System.out.println("update done---------------------------------------------");

            // Finalise tracks with consecutive non-detections
            for (TkTrack t : tracks) {
                if (t.elements.size() < _para.maxConsecMissingDetections) {
                    continue;
                }
                boolean allMissing = true;
                for (int i = t.elements.size() - _para.maxConsecMissingDetections; i < t.elements.size(); ++i) {
                    if (t.elements.get(i).det != null) {
                        allMissing = false;
                        break;
                    }
                }
                if (allMissing) {
                    t.setState(TkTrack.Status.TERMINATED);
                }
            }

            // Print tracks
            for (int t = 0; t < tracks.size(); ++t) {
                TkTrack track = tracks.get(t);
                TkLJIPDAState stateLJIPDA = track.getLast().find(tracker.getStateKey());
                System.out.print("|  ");
                System.out.print(String.format("Track: %02d  ", (t + 1)));
                System.out.print(String.format("state: %4.3f %4.3f  ", stateLJIPDA.post.state.getAsDouble(0, 0),
                        stateLJIPDA.post.state.getAsDouble(1, 0)));
                System.out.print(String.format("cov: %4.3f %4.3f  ", stateLJIPDA.post.stateCov.getAsDouble(0, 0),
                        stateLJIPDA.post.stateCov.getAsDouble(1, 0)));
                System.out.print(String.format("exist: %1.5f  ", stateLJIPDA.existencePost));
            }

        }

        return tracks;
    }

    // pass in session to keep transaction atomic
    static void dropAllExistingTracks(Session session) {
        // Clear the current solution of all tracks
        String hql = "SELECT ctTracksDetections" + " FROM CtTracksDetections as ctTracksDetections"
                + " JOIN ctTracksDetections.ctTracks ctTracks" + " WHERE ctTracks.ctSolutions = :solutionPk";

        Query q = session.createQuery(hql);
        q.setInteger("solutionPk", CtSolutionController.getSolutions().getPkSolution());
        List<CtTracksDetections> results = q.list();

        for (CtTracksDetections td : results) {
            session.delete(td);
        }

        hql = "SELECT ctTracks" + " FROM CtTracks as ctTracks" + " WHERE ctTracks.ctSolutions = :solutionPk";

        q = session.createQuery(hql);
        q.setInteger("solutionPk", CtSolutionController.getSolutions().getPkSolution());
        List<CtTracks> results2 = q.list();

        for (CtTracks t : results2) {
            session.delete(t);
        }

    }

    // pass in session to keep transaction atomic
    static void dropExistingTracks(Session session, TkTracks tracks, int startTimeIdxInclusive,
            int endTimeIdxInclusive) {

        CtSolutions solution = (CtSolutions) CtObjectDirectory.get("solution");

        // Drop all existing track detection associations
        int trackCnt = 0;
        for (TkTrack t : tracks) {
            ++trackCnt;

            for (TkTrackElement e : t.elements) {
                TkDetection d = e.det;
                if (d == null) {
                    continue;
                }

                System.out.println("d.timeIdx: " + (d.timeIdx));
                System.out.println("trackCnt: " + (trackCnt));
                System.out.println("total no. tracks: " + (trackCnt));
                if (d.timeIdx == startTimeIdxInclusive || d.timeIdx == endTimeIdxInclusive) {
                    continue;
                }

                Integer detectionPk = d.find(DETECTION_PK);

                String hql = "SELECT ctTracksDetections" + " FROM CtTracksDetections as ctTracksDetections"
                        + " JOIN ctTracksDetections.ctDetections ctDetections"
                        + " WHERE ctDetections.pkDetection = :detectionPk";

                Query q = session.createQuery(hql);
                q.setInteger("detectionPk", detectionPk);
                List<CtTracksDetections> results = q.list();

                HashSet<CtTracks> modified = new HashSet<CtTracks>();

                for (CtTracksDetections td : results) {
                    CtTracks t_n = td.getCtTracks();
                    CtDetections d_n = td.getCtDetections();
                    CtSolutions s = t_n.getCtSolutions();

                    if (s.getPkSolution() == solution.getPkSolution()) {
                        //                    if( td.getCtTracks().getCtSolutions().getPkSolution() == solution.getPkSolution() ) {
                        t_n.getCtTracksDetectionses().remove(td);
                        d_n.getCtTracksDetectionses().remove(td);
                        session.delete(td);
                        modified.add(t_n);
                    }
                }

                // now clean up tracks that have no surviving detections:
                for (CtTracks t_n : modified) {
                    Set<CtTracksDetections> tds = t_n.getCtTracksDetectionses();

                    if (tds.isEmpty()) {
                        CtSolutions s = t_n.getCtSolutions();
                        s.getCtTrackses().remove(t_n);
                        session.delete(t);
                        session.update(s);
                    }
                }

                //                String hql =
                //                      "DELETE from CtTracksDetections as ctTracksDetections"
                //                    + " WHERE fk_detection = " + detectionPk;
                //                System.out.println("hql: " + (hql) );
                //
                //                Query query = session.createQuery(hql);
                //                int row = query.executeUpdate();
                //                System.out.println("Rows deleted: " + row);
            }
        }
    }

    static CtTracks findExistingTrackAtStart(Session session, TkTrack t, int startTimeIdxInclusive) {
        //        session.flush();

        TkDetection firstDetection = t.elements.get(0).det;
        if (firstDetection == null) {
            throw new Error("First detection of a track can not be null");
        }

        // join first detection to any existing tracks
        if (firstDetection.timeIdx != startTimeIdxInclusive) {
            return null;
        }

        CtSolutions solution = (CtSolutions) CtObjectDirectory.get("solution");

        Integer detectionPk = firstDetection.find(DETECTION_PK);

        // retrieve the track that contains the detection
        String hql = " SELECT ctTD" + " FROM CtTracksDetections as ctTD" + " JOIN ctTD.ctDetections as ctD"
                + " WHERE ctD.pkDetection = :detectionPk";
        System.out.println("hql: " + (hql));

        //        session.flush();
        Query q = session.createQuery(hql);
        q.setInteger("detectionPk", detectionPk);
        List<CtTracksDetections> results = q.list();

        // make sure there's only one track associated with this detection
        CtTracks track = null;
        for (CtTracksDetections td : results) {
            if (td.getCtTracks().getCtSolutions().getPkSolution() == solution.getPkSolution()) {
                if (track != null) {
                    throw new Error("Detection associated with more than 1 track,"
                            + "this should not happen when all tracks-detections assoc"
                            + "have been cleaned between start and end time idx.");
                }

                track = td.getCtTracks(); // use existing one
            }
        }

        return track; // may be null if detection is not in a track
    }

    static boolean mergeTracksAtEnd(Session session, TkTrack t, int endTimeIdxInclusive, CtTracks tt) {
        //        session.flush();

        TkDetection lastDetection = t.getLast().det;

        if (lastDetection == null) {
            return false; // no detection associated at track's end.
        }

        // join first detection to any existing tracks
        if (lastDetection.timeIdx != endTimeIdxInclusive) {
            return false; // no detection on the boundary
        }

        Integer detectionPk = lastDetection.find(DETECTION_PK);

        // retrieve all tracks that contains the detection
        // note that this last detection may be a place for a fork.
        String hql = "SELECT ctTracksDetections" + " FROM CtTracksDetections as ctTracksDetections"
                + " JOIN ctTracksDetections.ctDetections ctDetections"
                + " WHERE ctDetections.pkDetection = :detectionPk";

        Query q = session.createQuery(hql);
        q.setInteger("detectionPk", detectionPk);
        List results = q.list();

        if (results.isEmpty()) {
            return false; // no existing tracks so no need to remap
        }

        if (results.size() > 1) {
            // this is a forking detection. When gets here:
            // - all tracks-detection assoc before this detection has been cleared
            // And since this is a fork, the track does not continue past this
            // detection. Since this detection will be associated with the new track
            // we don't have to remap anything.
            return false;
        }

        CtTracksDetections td = (CtTracksDetections) results.get(0);

        Set<CtTracksDetections> remap = td.getCtTracks().getCtTracksDetectionses();

        for (CtTracksDetections td2 : remap) {
            System.out.println(
                    "Remapping: " + td2.getCtTracks().getPkTrack() + " " + td2.getCtDetections().getPkDetection()
                            + " to: " + tt.getPkTrack() + " " + td2.getCtDetections().getPkDetection());

            if (td2.getCtTracks().getPkTrack() == tt.getPkTrack()) {
                continue; // no change
            }

            CtTracks tt0 = td2.getCtTracks();
            td2.setCtTracks(tt);
            tt0.getCtTracksDetectionses().remove(td2);
            tt.getCtTracksDetectionses().add(td2);
            session.update(td2);
            session.update(tt);
            session.update(tt0);
        }

        //        session.flush();
        return true;
    }

    static void removeEmptyTracks(Session session) {
        CtSolutions solution = (CtSolutions) CtObjectDirectory.get("solution");
        String hql = "SELECT ctTracks" + " FROM CtTracks as ctTracks" + " JOIN ctTracks.ctSolutions as ctSolutions"
                + " WHERE ctSolutions.pkSolution = :solutionPk";

        Query q = session.createQuery(hql);
        q.setInteger("solutionPk", solution.getPkSolution());
        List<CtTracks> results = q.list();

        for (CtTracks t : results) {
            Set<CtTracksDetections> s = t.getCtTracksDetectionses();
            //            System.out.println("t.getPkTrack(): " + (t.getPkTrack()) );
            //            System.out.println("t.getCtTracksDetectionses().size(): " + (t.getCtTracksDetectionses().size()) );
            //            System.out.println("s.isEmpty(): " + (s.isEmpty()) );;

            if (s.isEmpty()) {
                // empty track, delete it
                solution.getCtTrackses().remove(t);
                session.update(solution);
                session.delete(t);
            }
        }
    }

    static void saveTracks(TkTracks tracks, int startTimeIdxInclusive, int endTimeIdxInclusive) {

        Session session = CtSession.Current();
        session.beginTransaction();

        dropAllExistingTracks(session);
        //        dropExistingTracks( session, tracks, startTimeIdxInclusive, endTimeIdxInclusive );

        //        session.flush();

        CtSolutions solution = (CtSolutions) CtObjectDirectory.get("solution");

        for (int tIdx = 0; tIdx < tracks.size(); ++tIdx) {
            TkTrack t = tracks.get(tIdx);

            // can we merge with existing track at the start
            CtTracks tt = findExistingTrackAtStart(session, t, startTimeIdxInclusive);
            //            session.flush();

            boolean firstDetectionHasTrack = (tt != null);

            // create a new track if ones does not already exist
            if (tt == null) {
                tt = new CtTracks();
                tt.setCtSolutions(solution);
                solution.getCtTrackses().add(tt);
                session.save(tt);
            }
            //            session.flush();

            // re-associate tracks that joins at the end
            boolean mergedAtEnd = mergeTracksAtEnd(session, t, endTimeIdxInclusive, tt);
            //            session.flush();

            System.out.println("tt.getPkTrack(): " + (tt.getPkTrack()));

            // link up with parent tracks
            if (!t.isRoot()) {
                // find parent track's last detection
                TkTrack parent = (TkTrack) t.getParent();

                TkDetection d = parent.getLast().det;
                if (d == null) { // no detection for this time step
                    throw new Error("Parent track's last element can't be null detection");
                }

                int detectionPK = d.find(DETECTION_PK);

                CtDetections dd = (CtDetections) CtSession.getObject(CtDetections.class, detectionPK);

                // associate detection with track
                CtTracksDetections ttdd = new CtTracksDetections();

                ttdd.setCtTracks(tt);
                ttdd.setCtDetections(dd);
                tt.getCtTracksDetectionses().add(ttdd);
                dd.getCtTracksDetectionses().add(ttdd);

                session.save(ttdd);
            }

            // save detections
            for (int dIdx = 0; dIdx < t.elements.size(); ++dIdx) {
                // skip persistence of first detection if it is associated
                // with an existing track
                if (firstDetectionHasTrack && dIdx == 0) {
                    continue;
                }

                // skip persistence of last detection if it has been merged with
                // existing track
                if (mergedAtEnd && dIdx == t.elements.size() - 1) {
                    continue;
                }

                TkDetection d = t.elements.get(dIdx).det;
                if (d == null) { // no detection for this time step
                    continue;
                }
                int detectionPK = d.find(DETECTION_PK);

                System.out.println("detectionPK: " + (detectionPK));

                CtDetections dd = (CtDetections) CtSession.getObject(CtDetections.class, detectionPK);

                // associate detection with track
                CtTracksDetections ttdd = new CtTracksDetections();

                ttdd.setCtTracks(tt);
                ttdd.setCtDetections(dd);
                tt.getCtTracksDetectionses().add(ttdd);
                dd.getCtTracksDetectionses().add(ttdd);

                session.save(ttdd);
            }
            //            session.flush();
        }

        //TODO why can we delete empty tracks?
        //        session.flush();
        //        session.getTransaction().commit();
        //        removeEmptyTracks( session );

        session.flush();
        session.getTransaction().commit();
    }

    //    public static void putTest() {
    //        TkTracks tracks = newTracks();
    //
    //        Session session = CtSession.Current();
    //        session.beginTransaction();
    //
    //        for( int tIdx = 0; tIdx < tracks.size(); ++tIdx ) {
    //            TkTrack t = tracks.find(tIdx);
    //
    //            CtTracks tt = new CtTracks();
    //            session.save(tt);
    //
    //            for( int dIdx = 0; dIdx < t.elements.size(); ++dIdx ) {
    //                TkDetection d = t.elements.find(dIdx).det;
    //                String desc = d.find(detectionDescriptionKey);
    //
    //                CtDetections dd = new CtDetections();
    //                dd.setBoundary( desc ); // dummy
    //
    //                session.save(dd);
    //
    //                // associate detection with track
    //                CtTracksDetections ttdd = new CtTracksDetections();
    //
    //                ttdd.setCtTracks(tt);
    //                ttdd.setCtDetections(dd);
    //
    //                session.save(ttdd);
    //            }
    //
    //        }
    //
    //        session.getTransaction().commit();
    //
    //    }

    //    static TkTracks newTracks() {
    //        TkTracks tracks = new TkTracks();
    //
    //        int detectionCnt = 0;
    //
    //        for( int i = 0; i < 1; ++i ) {
    //            TkTrack t = new TkTrack();
    //
    //            for( int j = 0; j < 2; ++j, ++detectionCnt ) {
    //                TkDetection d = new TkDetection();
    //                d.add( detectionDescriptionKey, "Detection: " + detectionCnt );
    //
    //                TkTrackElement e = new TkTrackElement( d );
    //                t.elements.add( e );
    //            }
    //
    //            tracks.add( t );
    //        }
    //
    //        return tracks;
    //    }

    public static CtRegion getMicroWellRoi(String microWellName) {
        CtMicrowellsController mc = (CtMicrowellsController) CtObjectDirectory.get(CtMicrowellsController.name());
        CtMicrowellsModel mm = mc.getMicrowellsModel();
        CtMicrowell m = mm.find(microWellName);
        return m;
    }

    public static void filterDetectionsByRoi(List<TkDetection> detections, CtRegion roi) {
        Rectangle2D r = roi.getBoundingBox();

        ArrayList<TkDetection> result = new ArrayList<TkDetection>();

        for (TkDetection d : detections) {
            TkCell c = d.find(TkCell.name);
            if (r.contains(c.cx / CtSubPixelResolution.unitsPerNaturalPixel,
                    c.cy / CtSubPixelResolution.unitsPerNaturalPixel)) {
                result.add(d);
            }
        }

        // NOTE: since we don't know that implementation of List the user might want
        // and we don't care what input List implementation we want either, it's best
        // to just use the implementation the user has provided in the input List.
        // Hence we're not returning a new List but modifying the original.
        detections.clear();
        detections.addAll(result);
    }

}