1212import org .slf4j .Logger ;
1313import org .slf4j .LoggerFactory ;
1414import qupath .lib .analysis .features .ObjectMeasurements ;
15- import qupath .lib .common .GeneralTools ;
16- import qupath .lib .common .Version ;
1715import qupath .lib .gui .QuPathApp ;
1816import qupath .lib .images .ImageData ;
1917import qupath .lib .images .servers .ImageServer ;
18+ import qupath .lib .images .servers .TransformedServerBuilder ;
2019import qupath .lib .objects .*;
2120import qupath .lib .objects .hierarchy .TMAGrid ;
2221import qupath .lib .projects .Project ;
2322import qupath .lib .projects .ProjectImageEntry ;
2423import qupath .lib .roi .GeometryTools ;
2524import qupath .lib .roi .ROIs ;
2625import qupath .lib .roi .interfaces .ROI ;
27- import qupath .lib .scripting .QP ;
28-
2926import java .awt .image .BufferedImage ;
3027import java .io .File ;
3128import java .io .FileNotFoundException ;
3229import java .io .FileReader ;
3330import java .io .IOException ;
3431import java .nio .file .Path ;
35- import java .util .ArrayList ;
36- import java .util .Arrays ;
37- import java .util .Collection ;
38- import java .util .List ;
32+ import java .util .*;
3933import java .util .function .Predicate ;
4034import java .util .regex .Matcher ;
4135import java .util .regex .Pattern ;
4741 * Utility class which bridges real transformation of the ImgLib2 world and
4842 * makes it easily usable into JTS world, mainly used by QuPath
4943 * <p>
50- * See initial forum thread : https://forum.image.sc/t/qupath-arbitrarily-transform-detections-and-annotations/49674
51- * For documentation regarding this tool, see https://c4science.ch/w/bioimaging_and_optics_platform_biop/image-processing/wsi_registration_fjii_qupath/
44+ * See initial forum thread : <a href=" https://forum.image.sc/t/qupath-arbitrarily-transform-detections-and-annotations/49674">...</a>
45+ * For documentation regarding this tool, see <a href=" https://c4science.ch/w/bioimaging_and_optics_platform_biop/image-processing/wsi_registration_fjii_qupath/">...</a>
5246 * <p>
5347 * Extra dependencies required for QuPath:
5448 * <p>
@@ -69,47 +63,47 @@ public class Warpy {
6963 final private static Logger logger = LoggerFactory .getLogger (Warpy .class );
7064
7165 // Pattern to match the transform file
72- private static Pattern transformFilePattern = Pattern .compile ("transform \\ _ (?<target>\\ d+)\\ _(?<source>\\ d+)\\ .json" );
66+ private static final Pattern transformFilePattern = Pattern .compile ("transform_ (?<target>\\ d+)_(?<source>\\ d+)\\ .json" );
7367
7468 /**
7569 * Recovers a list of candidate entries in this project that have a RealTransform file that matches the pattern in
7670 * 'transformFilePattern'
7771 * @param targetEntry the entry which should *receive* transformed objects. Typically the active entry.
7872 * @return a collection of project entries that have a useable and valid RealTransform (forward or valid inverse)
7973 */
80- public static Collection <ProjectImageEntry > getCandidateSourceEntries (ProjectImageEntry targetEntry ) {
74+ public static Collection <ProjectImageEntry <?>> getCandidateSourceEntries (ProjectImageEntry <?> targetEntry ) {
8175
82- Project project = getProject ();
76+ Project <?> project = getProject ();
8377 // Find in the targetfolder, the source entries that have a Serialized RealTransform file
84- List <ProjectImageEntry <BufferedImage >> entries = project .getImageList ();
78+ List <? extends ProjectImageEntry <? >> entries = project .getImageList ();
8579
8680 String targetID = targetEntry .getID ();
8781
8882 // Find if there is a forward transform file and return the entry source
8983 Path targetEntryPath = targetEntry .getEntryPath ();
90- Collection <ProjectImageEntry > candidateTransformableEntries = new ArrayList <>();
91- for (File currentFile : targetEntryPath .toFile ().listFiles ()) {
84+ Collection <ProjectImageEntry <?> > candidateTransformableEntries = new ArrayList <>();
85+ for (File currentFile : Objects . requireNonNull ( targetEntryPath .toFile ().listFiles () )) {
9286 Matcher matcher = transformFilePattern .matcher (currentFile .getName ());
9387 if (matcher .matches ()) {
9488 if (matcher .group ("target" ).equals (targetID )) {
9589 // Check the source ID and return if
9690 String sourceID = matcher .group ("source" );
97- ProjectImageEntry sourceEntry = getEntryFromID (sourceID );
91+ ProjectImageEntry <?> sourceEntry = getEntryFromID (sourceID );
9892 if (sourceEntry != null ) candidateTransformableEntries .add (sourceEntry );
9993 }
10094 }
10195 }
10296
10397 // Find is there are inverse transforms available by going through all entries
104- for (ProjectImageEntry entry : entries ) {
98+ for (ProjectImageEntry <?> entry : entries ) {
10599 if (!entry .equals (targetEntry )) {
106- for (File currentFile : entry .getEntryPath ().toFile ().listFiles ()) {
100+ for (File currentFile : Objects . requireNonNull ( entry .getEntryPath ().toFile ().listFiles () )) {
107101 Matcher matcher = transformFilePattern .matcher (currentFile .getName ());
108102 if (matcher .matches ()) {
109103 if (matcher .group ("source" ).equals (targetID )) {
110104 // Check the source ID and return it
111105 String inverseSourceID = matcher .group ("target" );
112- ProjectImageEntry inverseSourceEntry = getEntryFromID (inverseSourceID );
106+ ProjectImageEntry <?> inverseSourceEntry = getEntryFromID (inverseSourceID );
113107 if (inverseSourceEntry != null ) {
114108 // Try to get the transform and see if it works
115109 RealTransform rt = getRealTransform (currentFile );
@@ -138,7 +132,7 @@ public static Collection<ProjectImageEntry> getCandidateSourceEntries(ProjectIma
138132 * @param sourceEntry the entry from which we want to extract the objects
139133 * @return a collection of PathObjects, in hierarchical form
140134 */
141- public static Collection <PathObject > getPathObjectsFromEntry (ProjectImageEntry sourceEntry ) {
135+ public static Collection <PathObject > getPathObjectsFromEntry (ProjectImageEntry <?> sourceEntry ) {
142136 try {
143137 // Do not return the TMA cores
144138 return sourceEntry .readHierarchy ().getRootObject ().getChildObjects ().stream ().filter (Predicate .not (PathObject ::isTMACore )).collect (Collectors .toList ());
@@ -148,7 +142,7 @@ public static Collection<PathObject> getPathObjectsFromEntry(ProjectImageEntry s
148142 return null ;
149143 }
150144
151- public static TMAGrid getTMAGridFromEntry (ProjectImageEntry sourceEntry ) {
145+ public static TMAGrid getTMAGridFromEntry (ProjectImageEntry <?> sourceEntry ) {
152146 try {
153147 return sourceEntry .readHierarchy ().getTMAGrid ();
154148 } catch (IOException e ) {
@@ -214,16 +208,9 @@ public static Collection<PathObject> transformPathObjects(Collection<PathObject>
214208 CoordinateSequenceFilter transformer = getJTSFilter (transform );
215209
216210 // Transforms all objects and add them to a new list
217- List <PathObject > transformedObjects = new ArrayList <>();
218-
219- for (PathObject o : objects ) {
220- try {
221- transformedObjects .add (transformPathObjectAndChildren (o , transformer , true , true ));
222- } catch (Exception e ) {
223- logger .info ("Could not transform object " + o , e );
224- }
225- }
226- return transformedObjects ;
211+ return objects .stream ().map (o -> transformPathObjectAndChildren (o , transformer , true , true ) )
212+ .filter (Objects ::nonNull )
213+ .collect (Collectors .toList ());
227214 }
228215
229216 /**
@@ -232,8 +219,8 @@ public static Collection<PathObject> transformPathObjects(Collection<PathObject>
232219 * @param id the ID to get the ImageEntry from
233220 * @return the corresponding ProjectImageEntry or null if none is found
234221 */
235- private static ProjectImageEntry getEntryFromID (String id ) {
236- for (ProjectImageEntry entry : QP . getProject ().getImageList ()) {
222+ private static ProjectImageEntry <?> getEntryFromID (String id ) {
223+ for (ProjectImageEntry <?> entry : getProject ().getImageList ()) {
237224 if (entry .getID ().equals (id )) return entry ;
238225 }
239226 return null ;
@@ -247,13 +234,13 @@ private static ProjectImageEntry getEntryFromID(String id) {
247234 * Warpy can work out if the serialized transform is a forward one or an inverse transform.
248235 * @return the RealTransform to use for warping pathObjects
249236 */
250- public static RealTransform getRealTransform (ProjectImageEntry sourceEntry , ProjectImageEntry targetEntry ) {
237+ public static RealTransform getRealTransform (ProjectImageEntry <?> sourceEntry , ProjectImageEntry <?> targetEntry ) {
251238
252239 // Search Forward
253240 String targetID = targetEntry .getID ();
254241 String sourceID = sourceEntry .getID ();
255242 Path targetEntryPath = targetEntry .getEntryPath ();
256- for (File currentFile : targetEntryPath .toFile ().listFiles ()) {
243+ for (File currentFile : Objects . requireNonNull ( targetEntryPath .toFile ().listFiles () )) {
257244 Matcher matcher = transformFilePattern .matcher (currentFile .getName ());
258245 if (matcher .matches ()) {
259246 if (matcher .group ("target" ).equals (targetID )) {
@@ -268,7 +255,7 @@ public static RealTransform getRealTransform(ProjectImageEntry sourceEntry, Proj
268255
269256 // Search Backwards
270257 Path sourceEntryPath = sourceEntry .getEntryPath ();
271- for (File currentFile : sourceEntryPath .toFile ().listFiles ()) {
258+ for (File currentFile : Objects . requireNonNull ( sourceEntryPath .toFile ().listFiles () )) {
272259 Matcher matcher = transformFilePattern .matcher (currentFile .getName ());
273260 if (matcher .matches ()) {
274261 if (matcher .group ("source" ).equals (targetID )) {
@@ -279,7 +266,7 @@ public static RealTransform getRealTransform(ProjectImageEntry sourceEntry, Proj
279266 if (rt instanceof InvertibleRealTransform ) {
280267 return ((InvertibleRealTransform ) rt ).inverse ();
281268 } else {
282- logger .error ("Could not invert transform from file {}. This error should not exist." );
269+ logger .error ("Could not invert transform from file {}. This error should not exist." , currentFile . getAbsolutePath () );
283270 return null ;
284271 }
285272 }
@@ -299,17 +286,16 @@ public static RealTransform getRealTransform(ProjectImageEntry sourceEntry, Proj
299286 * @throws Exception an error in case that the objects could not be measured
300287 */
301288 public static void addIntensityMeasurements (Collection <PathObject > objects , double downsample ) throws Exception {
302- ImageServer server = getCurrentServer ();
303-
304289
290+ ImageServer <BufferedImage > server = (ImageServer <BufferedImage >) getCurrentServer ();
305291 //If the image is RGB, this line can be added to import the correct measurements (DAB, etc.):
306292 //cf https://forum.image.sc/t/transferring-segmentation-predictions-from-custom-masks-to-qupath/43408/15
307293 ImageData .ImageType type = getProjectEntry ().readImageData ().getImageType ();
308294
309295 if (type .equals (ImageData .ImageType .BRIGHTFIELD_H_DAB ) ||
310- type .equals (ImageData .ImageType .BRIGHTFIELD_H_DAB ) ||
296+ type .equals (ImageData .ImageType .BRIGHTFIELD_H_E ) ||
311297 type .equals (ImageData .ImageType .BRIGHTFIELD_OTHER )) {
312- server = new qupath . lib . images . servers . TransformedServerBuilder (server )
298+ server = new TransformedServerBuilder (server )
313299 .deconvolveStains (getCurrentImageData ().getColorDeconvolutionStains (), 1 , 2 )
314300 .build ();
315301 }
@@ -344,20 +330,25 @@ public static void addIntensityMeasurements(Collection<PathObject> objects, Imag
344330 *
345331 * @param object qupath annotation or detection object
346332 * @param transform jts free form transformation
347- * @param copyMeasurements whether or not to transfer all the source PathObject Measurements to the resulting PathObject
333+ * @param copyMeasurements whether to transfer all the source PathObject Measurements to the resulting PathObject
348334 */
349- private static PathObject transformPathObjectAndChildren (PathObject object , CoordinateSequenceFilter transform , boolean checkGeometryValidity , boolean copyMeasurements ) throws Exception {
335+ private static PathObject transformPathObjectAndChildren (PathObject object , CoordinateSequenceFilter transform , boolean checkGeometryValidity , boolean copyMeasurements ) {
350336
351- PathObject transformedObject = transformPathObject (object , transform , checkGeometryValidity , copyMeasurements );
352-
353- if (object .hasChildObjects ()) {
354- for (PathObject child : object .getChildObjects ()) {
355- transformedObject .addChildObject (transformPathObjectAndChildren (child , transform , checkGeometryValidity , copyMeasurements ));
356- }
337+ PathObject transformedObject = null ;
338+ try {
339+ transformedObject = transformPathObject (object , transform , checkGeometryValidity , copyMeasurements );
340+ } catch (Exception e ) {
341+ logger .error ("Could not transform object {}, error is {}" , object , e .getLocalizedMessage ());
357342 }
358343
359- // Re-add name if it exists
360- transformedObject .setName (object .getName ());
344+ if (object .hasChildObjects () && transformedObject != null ) {
345+ logger .info ("Transforming {}" , object );
346+ List <PathObject > children = object .getChildObjects ().stream ()
347+ .map (child -> transformPathObjectAndChildren (child , transform , checkGeometryValidity , copyMeasurements ))
348+ .filter (Objects ::nonNull )
349+ .collect (Collectors .toList ());
350+ transformedObject .addChildObjects (children );
351+ }
361352
362353 return transformedObject ;
363354 }
@@ -391,33 +382,36 @@ private static PathObject transformPathObject(PathObject object, CoordinateSeque
391382 ROI transformed_roi = GeometryTools .geometryToROI (geometry , original_roi .getImagePlane ());
392383
393384 PathObject transformedObject ;
394- if (object instanceof PathAnnotationObject ) {
395- transformedObject = PathObjects .createAnnotationObject (transformed_roi , object .getPathClass (), copyMeasurements ? object .getMeasurementList () : null );
396- } else if (object instanceof PathCellObject ) {
397- // Need to transform the nucleus as well
398- ROI original_nuc = ((PathCellObject ) object ).getNucleusROI ();
399- ROI transformed_nuc_roi = null ;
400- if (original_nuc != null ) {
401-
402- Geometry nuc_geometry = original_nuc .getGeometry ();
403-
404- GeometryTools .attemptOperation (nuc_geometry , (g ) -> {
405- g .apply (transform );
406- return g ;
407- });
408- transformed_nuc_roi = GeometryTools .geometryToROI (nuc_geometry , original_roi .getImagePlane ());
385+ switch (object ) {
386+ case PathAnnotationObject pathAnnotationObject ->
387+ transformedObject = PathObjects .createAnnotationObject (transformed_roi , pathAnnotationObject .getPathClass (), copyMeasurements ? object .getMeasurementList () : null );
388+ case PathCellObject pathCellObject -> {
389+ // Need to transform the nucleus as well
390+ ROI original_nuc = pathCellObject .getNucleusROI ();
391+ ROI transformed_nuc_roi = null ;
392+ if (original_nuc != null ) {
393+
394+ Geometry nuc_geometry = original_nuc .getGeometry ();
395+
396+ GeometryTools .attemptOperation (nuc_geometry , (g ) -> {
397+ g .apply (transform );
398+ return g ;
399+ });
400+ transformed_nuc_roi = GeometryTools .geometryToROI (nuc_geometry , original_roi .getImagePlane ());
401+ }
402+ transformedObject = PathObjects .createCellObject (transformed_roi , transformed_nuc_roi , object .getPathClass (), copyMeasurements ? object .getMeasurementList () : null );
409403 }
410- transformedObject = PathObjects .createCellObject (transformed_roi , transformed_nuc_roi , object .getPathClass (), copyMeasurements ? object .getMeasurementList () : null );
411-
412- } else if (object instanceof PathDetectionObject ) {
413- transformedObject = PathObjects .createDetectionObject (transformed_roi , object .getPathClass (), copyMeasurements ? object .getMeasurementList () : null );
414- } else {
415- throw new Exception ("Unknown PathObject class for class " + object .getClass ().getSimpleName ());
404+ case PathDetectionObject pathDetectionObject ->
405+ transformedObject = PathObjects .createDetectionObject (transformed_roi , pathDetectionObject .getPathClass (), copyMeasurements ? object .getMeasurementList () : null );
406+ default -> throw new Exception ("Unknown PathObject class for class " + object .getClass ().getSimpleName ());
416407 }
417408
418409 // Return the same ID as the original object
419- transformedObject . setID ( object . getID ());
410+ // Add the name and ID here
420411 transformedObject .setName (object .getName ());
412+ transformedObject .setID (object .getID ());
413+ transformedObject .setLocked (object .isLocked ());
414+
421415 return transformedObject ;
422416 }
423417
@@ -434,8 +428,7 @@ public static RealTransform getRealTransform(File f) {
434428 JsonObject element = new Gson ().fromJson (fileReader , JsonObject .class );
435429 fileReader .close ();
436430 element = (JsonObject ) RealTransformSerializer .fixAffineTransform (element ); // Fix missing type element in old versions
437- RealTransform rt = RealTransformSerializer .getRealTransformAdapter ().fromJson (element , RealTransform .class );
438- return rt ;
431+ return RealTransformSerializer .getRealTransformAdapter ().fromJson (element , RealTransform .class );
439432 } catch (FileNotFoundException e ) {
440433 logger .error ("Transform file " + f .getName () + " not found" , e );
441434 } catch (IOException e ) {
@@ -482,10 +475,9 @@ public boolean isGeometryChanged() {
482475 /**
483476 * Main class for debugging
484477 *
485- * @param args
486- * @throws Exception
478+ * @param args some inputs we do not need
487479 */
488- public static void main (String ... args ) throws Exception {
480+ public static void main (String ... args ) {
489481 //String projectPath = "\\\\svfas6.epfl.ch\\biop\\public\\luisa.spisak_UPHUELSKEN\\Overlay\\qp\\project.qpproj";
490482 QuPathApp .launch (QuPathApp .class );//, projectPath);
491483 }
0 commit comments