Skip to content

Commit f059bdb

Browse files
authored
Merge pull request #14 from Rylern/refactoring
Refactoring
2 parents 7ba12bb + 34b2eda commit f059bdb

30 files changed

Lines changed: 3281 additions & 1653 deletions

build.gradle.kts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@ qupathExtension {
1111
}
1212

1313
dependencies {
14-
1514
implementation(libs.bundles.qupath)
1615
implementation(libs.bundles.logging)
1716
implementation(libs.qupath.fxtras)
17+
implementation(libs.guava)
1818

19-
// For testing
2019
testImplementation(libs.junit)
21-
2220
}

src/main/java/.gitkeep

Whitespace-only changes.

src/main/java/module-info.java.bkp

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/main/java/qupath/ext/align/AlignExtension.java

Lines changed: 0 additions & 82 deletions
This file was deleted.
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
package qupath.ext.align.core;
2+
3+
import javafx.beans.property.ObjectProperty;
4+
import javafx.beans.property.SimpleObjectProperty;
5+
import javafx.beans.value.ObservableValue;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
import qupath.lib.display.ImageDisplay;
9+
import qupath.lib.geom.Point2;
10+
import qupath.lib.gui.viewer.QuPathViewer;
11+
import qupath.lib.images.ImageData;
12+
import qupath.lib.images.servers.PixelCalibration;
13+
import qupath.lib.roi.ROIs;
14+
import qupath.lib.roi.RoiTools;
15+
import qupath.lib.roi.interfaces.ROI;
16+
17+
import java.awt.*;
18+
import java.awt.geom.AffineTransform;
19+
import java.awt.geom.NoninvertibleTransformException;
20+
import java.awt.geom.Point2D;
21+
import java.awt.image.BufferedImage;
22+
import java.io.IOException;
23+
import java.util.Arrays;
24+
import java.util.Objects;
25+
26+
/**
27+
* A class that represents an affine transformation happening on an image and on a specific QuPath viewer.
28+
* <p>
29+
* This class is not thread-safe.
30+
*/
31+
public class AffineImageTransform {
32+
33+
private static final Logger logger = LoggerFactory.getLogger(AffineImageTransform.class);
34+
private final ObjectProperty<AffineTransform> transform = new SimpleObjectProperty<>(new AffineTransform());
35+
private final ImageData<BufferedImage> imageData;
36+
private final QuPathViewer viewer;
37+
private AffineTransform inverseTransform = new AffineTransform();
38+
private ImageDisplay imageDisplay;
39+
40+
/**
41+
* Create the affine image transform.
42+
* <p>
43+
* The transform will be initialized as the identity matrix. If the provided image data
44+
* and the provided viewer have a defined pixel size in microns, the transform will be scaled to
45+
* the pixel size of the provided viewer divided by the pixel size of the provided image.
46+
* <p>
47+
* It is expected that the image server associated with the provided image data (see {@link ImageData#getServer()}
48+
* is already created.
49+
*
50+
* @param imageData the image data the transform should be applied to
51+
* @param viewer the viewer the image should be transformed to
52+
* @throws NullPointerException if one of the provided parameters is null
53+
*/
54+
public AffineImageTransform(ImageData<BufferedImage> imageData, QuPathViewer viewer) {
55+
this.imageData = Objects.requireNonNull(imageData);
56+
this.viewer = Objects.requireNonNull(viewer);
57+
58+
resetTransform();
59+
}
60+
61+
@Override
62+
public String toString() {
63+
return String.format(
64+
"Image transform of %s and %s with transform %s and inverse transform %s",
65+
imageData,
66+
viewer,
67+
transform.get(),
68+
inverseTransform
69+
);
70+
}
71+
72+
/**
73+
* Get an observable value representing the transform of this object. The observable value will be updated
74+
* each time the transform is modified. Note that you shouldn't modify the returned transform; use functions of
75+
* this class (e.g. {@link #setTransform(double, double, double, double, double, double)} or {@link #invertTransform()})
76+
*
77+
* @return an observable value representing the transform of this object
78+
*/
79+
public ObservableValue<AffineTransform> getTransform() {
80+
return transform;
81+
}
82+
83+
/**
84+
* Call {@link AutoAligner#getAlignTransformation(ImageData, ImageData, AffineTransform, AutoAligner.AlignmentType, AutoAligner.TransformationTypes, double)}
85+
* with the current transform and update the transform with the result.
86+
*/
87+
public void alignTransform(
88+
ImageData<BufferedImage> baseImageData,
89+
ImageData<BufferedImage> imageDataToAlign,
90+
AutoAligner.AlignmentType alignmentType,
91+
AutoAligner.TransformationTypes transformationTypes,
92+
double downsample
93+
) throws Exception {
94+
updateTransform(AutoAligner.getAlignTransformation(
95+
baseImageData,
96+
imageDataToAlign,
97+
this.transform.get(),
98+
alignmentType,
99+
transformationTypes,
100+
downsample
101+
));
102+
}
103+
104+
/**
105+
* Update the transform with the provided matrix value.
106+
*
107+
* @param m00 the X coordinate scaling element of the 3x3 matrix
108+
* @param m10 the Y coordinate shearing element of the 3x3 matrix
109+
* @param m01 the X coordinate shearing element of the 3x3 matrix
110+
* @param m11 the Y coordinate scaling element of the 3x3 matrix
111+
* @param m02 the X coordinate translation element of the 3x3 matrix
112+
* @param m12 the Y coordinate translation element of the 3x3 matrix
113+
*/
114+
public void setTransform(double m00, double m10, double m01, double m11, double m02, double m12) {
115+
AffineTransform transform = new AffineTransform(this.transform.get());
116+
transform.setTransform(m00, m10, m01, m11, m02, m12);
117+
updateTransform(transform);
118+
}
119+
120+
/**
121+
* Concatenates the transform with a translation.
122+
*
123+
* @param x the distance by which coordinates are translated in the X axis direction
124+
* @param y the distance by which coordinates are translated in the Y axis direction
125+
*/
126+
public void translateTransform(double x, double y) {
127+
AffineTransform transform = new AffineTransform(this.transform.get());
128+
transform.translate(x, y);
129+
updateTransform(transform);
130+
}
131+
132+
/**
133+
* Concatenates the transform with a transform that rotates coordinates around an anchor point.
134+
*
135+
* @param theta the angle of rotation (in radians)
136+
* @param anchorX the X coordinate of the rotation anchor point
137+
* @param anchorY the Y coordinate of the rotation anchor point
138+
*/
139+
public void rotateTransform(double theta, double anchorX, double anchorY) {
140+
AffineTransform transform = new AffineTransform(this.transform.get());
141+
transform.rotate(theta, anchorX, anchorY);
142+
updateTransform(transform);
143+
}
144+
145+
/**
146+
* Attempt to set the transform to the inverse of itself.
147+
*
148+
* @throws NoninvertibleTransformException if the matrix cannot be inverted
149+
*/
150+
public void invertTransform() throws NoninvertibleTransformException {
151+
AffineTransform transform = new AffineTransform(this.transform.get());
152+
transform.invert();
153+
updateTransform(transform);
154+
}
155+
156+
/**
157+
* Reset the transform with the value described in {@link #AffineImageTransform(ImageData, QuPathViewer)}.
158+
*/
159+
public void resetTransform() {
160+
AffineTransform transform = new AffineTransform();
161+
162+
if (viewer.getImageData() != null) {
163+
PixelCalibration viewerImageCalibration = viewer.getImageData().getServer().getPixelCalibration();
164+
PixelCalibration overlayImageCalibration = imageData.getServer().getPixelCalibration();
165+
166+
if (viewerImageCalibration.hasPixelSizeMicrons() && overlayImageCalibration.hasPixelSizeMicrons()) {
167+
transform.scale(
168+
viewerImageCalibration.getPixelWidthMicrons() / overlayImageCalibration.getPixelWidthMicrons(),
169+
viewerImageCalibration.getPixelHeightMicrons() / overlayImageCalibration.getPixelHeightMicrons()
170+
);
171+
}
172+
}
173+
174+
updateTransform(transform);
175+
}
176+
177+
/**
178+
* Get the inverse of the transform. Note that if the transform is currently not invertible, the returned value of this
179+
* function might not be the inverse of the transform, but rather the inverse of the last value of the transform
180+
* when it was still invertible.
181+
*
182+
* @return the inverse of the transform
183+
*/
184+
public AffineTransform getInverseTransform() {
185+
return inverseTransform;
186+
}
187+
188+
/**
189+
* Transform the provided ROI with the current transform.
190+
*
191+
* @param roi the ROI to transform
192+
* @return a new ROI that represents the provided ROI transformed with the current transform
193+
* @throws NullPointerException if the provided parameter is null
194+
*/
195+
public ROI transformROI(ROI roi) {
196+
logger.debug("Transforming {} with {}", roi, this);
197+
198+
if (roi.isPoint()) {
199+
int nPoints = roi.getAllPoints().size();
200+
Point2D[] transformedPoints = new Point2D[nPoints];
201+
202+
transform.get().transform(
203+
roi.getAllPoints().stream()
204+
.map(point -> new Point2D.Double(point.getX(), point.getY()))
205+
.toArray(Point2D[]::new),
206+
0,
207+
transformedPoints,
208+
0,
209+
nPoints
210+
);
211+
logger.debug("{} is a ROI of points. Its points were transformed from {} to {}", roi, roi.getAllPoints(), Arrays.toString(transformedPoints));
212+
213+
return ROIs.createPointsROI(
214+
Arrays.stream(transformedPoints)
215+
.map(point -> new Point2(point.getX(), point.getY()))
216+
.toList(),
217+
roi.getImagePlane()
218+
);
219+
} else {
220+
Shape transformedShape = transform.get().createTransformedShape(roi.getShape());
221+
logger.debug("{} is not a ROI of points. Its shape was transformed from {} to {}", roi, roi.getShape(), transformedShape);
222+
223+
return RoiTools.getShapeROI(transformedShape, roi.getImagePlane(), 0.5);
224+
}
225+
}
226+
227+
/**
228+
* @return the image data to which the transform should be applied
229+
*/
230+
public ImageData<BufferedImage> getImageData() {
231+
return imageData;
232+
}
233+
234+
/**
235+
* Get an image display that can be used when painting the image returned by {@link #getImageData()}.
236+
* The first call to this function may take some time as it will generate the image display. Further calls
237+
* will use the previously generated image display so will be faster.
238+
*
239+
* @return an image display that can be used when painting the image
240+
* @throws IOException if an error occurs while creating the image display
241+
*/
242+
public ImageDisplay getImageDisplay() throws IOException {
243+
if (imageDisplay == null) {
244+
imageDisplay = ImageDisplay.create(imageData);
245+
}
246+
return imageDisplay;
247+
}
248+
249+
private void updateTransform(AffineTransform transform) {
250+
this.transform.set(transform); // this is used to trigger any listener to this.transform
251+
252+
try {
253+
inverseTransform = transform.createInverse();
254+
logger.trace("Transform updated to {} and inverse transform updated to {}. Repainting {}", transform, inverseTransform, viewer);
255+
viewer.repaint();
256+
} catch (NoninvertibleTransformException e) {
257+
logger.warn(
258+
"Cannot create inverse transform of {}. Inverse transform not updated and still set to {}. {} not repainted",
259+
transform,
260+
inverseTransform,
261+
viewer,
262+
e
263+
);
264+
}
265+
}
266+
}

0 commit comments

Comments
 (0)