Skip to content

Commit 0f0229f

Browse files
committed
Added TypeAdapters, switched core resizing from ImageJ to OpenCV
Added further TypeAdapters for PathObjects and more. Changed resize method in BufferedImageTools from using ImageJ to using OpenCV. This means that ImageJ is no longer a dependency of the core module, whereas OpenCV now is. This facilitated bringing the OpenCVTypeAdapterFactory into the core.
1 parent 39b44ac commit 0f0229f

File tree

13 files changed

+697
-147
lines changed

13 files changed

+697
-147
lines changed

qupath-core-processing/src/test/java/qupath/opencv/processing/TypeAdaptersCVTest.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414

1515
import com.google.gson.GsonBuilder;
1616

17+
import qupath.lib.io.OpenCVTypeAdapters;
18+
1719
public class TypeAdaptersCVTest {
1820

1921
@Test
2022
public void testGetOpenCVTypeAdaptorFactory() {
2123

2224
var gson = new GsonBuilder()
23-
.registerTypeAdapterFactory(TypeAdaptersCV.getOpenCVTypeAdaptorFactory())
25+
.registerTypeAdapterFactory(OpenCVTypeAdapters.getOpenCVTypeAdaptorFactory())
2426
.create();
2527

2628

@@ -86,8 +88,8 @@ static boolean matEquals(SparseMat mat1, SparseMat mat2) {
8688
@Test
8789
public void testGetTypeAdaptor() {
8890
var gson = new GsonBuilder()
89-
.registerTypeAdapter(Mat.class, TypeAdaptersCV.getTypeAdaptor(Mat.class))
90-
.registerTypeAdapter(SparseMat.class, TypeAdaptersCV.getTypeAdaptor(SparseMat.class))
91+
.registerTypeAdapter(Mat.class, OpenCVTypeAdapters.getTypeAdaptor(Mat.class))
92+
.registerTypeAdapter(SparseMat.class, OpenCVTypeAdapters.getTypeAdaptor(SparseMat.class))
9193
.create();
9294

9395

qupath-core/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ configurations {
77
implementation.extendsFrom gson
88
implementation.extendsFrom jts
99
implementation.extendsFrom guava
10-
implementation.extendsFrom imagej
10+
implementation.extendsFrom opencv
11+
// implementation.extendsFrom imagej
1112
}

qupath-core/src/main/java/qupath/lib/awt/common/BufferedImageTools.java

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,16 @@
2727
import java.awt.Graphics2D;
2828
import java.awt.Shape;
2929
import java.awt.image.BufferedImage;
30+
import java.awt.image.WritableRaster;
31+
32+
import org.bytedeco.javacpp.indexer.FloatIndexer;
33+
import org.bytedeco.opencv.global.opencv_core;
34+
import org.bytedeco.opencv.global.opencv_imgproc;
35+
import org.bytedeco.opencv.opencv_core.Mat;
36+
import org.bytedeco.opencv.opencv_core.Size;
3037
import org.slf4j.Logger;
3138
import org.slf4j.LoggerFactory;
3239

33-
import ij.process.FloatProcessor;
34-
import ij.process.ImageProcessor;
3540
import qupath.lib.common.GeneralTools;
3641
import qupath.lib.images.servers.AbstractTileableImageServer;
3742
import qupath.lib.regions.RegionRequest;
@@ -119,7 +124,6 @@ public static BufferedImage ensureIntRGB(final BufferedImage img) {
119124
}
120125

121126

122-
123127
/**
124128
* Resize the image to have the requested width/height, using area averaging and bilinear interpolation.
125129
*
@@ -130,11 +134,6 @@ public static BufferedImage ensureIntRGB(final BufferedImage img) {
130134
*/
131135
public static BufferedImage resize(final BufferedImage img, final int finalWidth, final int finalHeight) {
132136

133-
// boolean useLegacyResizing = false;
134-
// if (useLegacyResizing) {
135-
// return resize(img, finalWidth, finalHeight, false);
136-
// }
137-
138137
if (img.getWidth() == finalWidth && img.getHeight() == finalHeight)
139138
return img;
140139

@@ -149,24 +148,81 @@ public static BufferedImage resize(final BufferedImage img, final int finalWidth
149148
logger.warn("Slight difference in aspect ratio for resized image: {}x{} -> {}x{} ({}, {})", img.getWidth(), img.getHeight(), finalWidth, finalHeight, aspectRatio, finalAspectRatio);
150149
}
151150

152-
boolean areaAveraging = true;
153-
154-
var raster = img.getRaster();
155-
var raster2 = raster.createCompatibleWritableRaster(finalWidth, finalHeight);
151+
WritableRaster raster = img.getRaster();
152+
WritableRaster raster2 = raster.createCompatibleWritableRaster(finalWidth, finalHeight);
156153

157154
int w = img.getWidth();
158155
int h = img.getHeight();
159-
160-
var fp = new FloatProcessor(w, h);
161-
fp.setInterpolationMethod(ImageProcessor.BILINEAR);
156+
157+
Mat matInput = new Mat(h, w, opencv_core.CV_32FC1);
158+
Size sizeOutput = new Size(finalWidth, finalHeight);
159+
Mat matOutput = new Mat(sizeOutput, opencv_core.CV_32FC1);
160+
FloatIndexer idxInput = matInput.createIndexer(true);
161+
FloatIndexer idxOutput = matOutput.createIndexer(true);
162+
float[] pixels = new float[w*h];
163+
float[] pixelsOut = new float[finalWidth*finalHeight];
164+
162165
for (int b = 0; b < raster.getNumBands(); b++) {
163-
float[] pixels = (float[])fp.getPixels();
164166
raster.getSamples(0, 0, w, h, b, pixels);
165-
var fp2 = fp.resize(finalWidth, finalHeight, areaAveraging);
166-
raster2.setSamples(0, 0, finalWidth, finalHeight, b, (float[])fp2.getPixels());
167+
idxInput.put(0L, pixels);
168+
opencv_imgproc.resize(matInput, matOutput, sizeOutput, 0, 0, opencv_imgproc.INTER_AREA);
169+
idxOutput.get(0, pixelsOut);
170+
raster2.setSamples(0, 0, finalWidth, finalHeight, b, pixelsOut);
167171
}
172+
173+
idxInput.release();
174+
idxOutput.release();
175+
matInput.close();
176+
matOutput.close();
177+
sizeOutput.close();
178+
179+
// System.err.println(String.format("Resizing from %d x %d to %d x %d", w, h, finalWidth, finalHeight));
168180

169181
return new BufferedImage(img.getColorModel(), raster2, img.isAlphaPremultiplied(), null);
170182
}
171183

184+
// /**
185+
// * Resize the image to have the requested width/height, using area averaging and bilinear interpolation.
186+
// *
187+
// * @param img input image to be resized
188+
// * @param finalWidth target output width
189+
// * @param finalHeight target output height
190+
// * @return resized image
191+
// */
192+
// public static BufferedImage resize(final BufferedImage img, final int finalWidth, final int finalHeight) {
193+
//
194+
// if (img.getWidth() == finalWidth && img.getHeight() == finalHeight)
195+
// return img;
196+
//
197+
// logger.trace(String.format("Resizing %d x %d -> %d x %d", img.getWidth(), img.getHeight(), finalWidth, finalHeight));
198+
//
199+
// double aspectRatio = (double)img.getWidth()/img.getHeight();
200+
// double finalAspectRatio = (double)finalWidth/finalHeight;
201+
// if (!GeneralTools.almostTheSame(aspectRatio, finalAspectRatio, 0.01)) {
202+
// if (!GeneralTools.almostTheSame(aspectRatio, finalAspectRatio, 0.05))
203+
// logger.warn("Substantial difference in aspect ratio for resized image: {}x{} -> {}x{} ({}, {})", img.getWidth(), img.getHeight(), finalWidth, finalHeight, aspectRatio, finalAspectRatio);
204+
// else
205+
// logger.warn("Slight difference in aspect ratio for resized image: {}x{} -> {}x{} ({}, {})", img.getWidth(), img.getHeight(), finalWidth, finalHeight, aspectRatio, finalAspectRatio);
206+
// }
207+
//
208+
// boolean areaAveraging = true;
209+
//
210+
// var raster = img.getRaster();
211+
// var raster2 = raster.createCompatibleWritableRaster(finalWidth, finalHeight);
212+
//
213+
// int w = img.getWidth();
214+
// int h = img.getHeight();
215+
//
216+
// var fp = new FloatProcessor(w, h);
217+
// fp.setInterpolationMethod(ImageProcessor.BILINEAR);
218+
// for (int b = 0; b < raster.getNumBands(); b++) {
219+
// float[] pixels = (float[])fp.getPixels();
220+
// raster.getSamples(0, 0, w, h, b, pixels);
221+
// var fp2 = fp.resize(finalWidth, finalHeight, areaAveraging);
222+
// raster2.setSamples(0, 0, finalWidth, finalHeight, b, (float[])fp2.getPixels());
223+
// }
224+
//
225+
// return new BufferedImage(img.getColorModel(), raster2, img.isAlphaPremultiplied(), null);
226+
// }
227+
172228
}

qupath-core/src/main/java/qupath/lib/io/GsonTools.java

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
package qupath.lib.io;
22

3+
import java.io.IOException;
4+
import java.util.Collection;
5+
6+
import org.locationtech.jts.geom.Geometry;
7+
38
import com.google.gson.Gson;
49
import com.google.gson.GsonBuilder;
10+
import com.google.gson.TypeAdapter;
11+
import com.google.gson.TypeAdapterFactory;
12+
import com.google.gson.reflect.TypeToken;
13+
import com.google.gson.stream.JsonReader;
14+
import com.google.gson.stream.JsonWriter;
515

16+
import qupath.lib.io.PathObjectTypeAdapters.FeatureCollection;
17+
import qupath.lib.measurements.MeasurementList;
18+
import qupath.lib.objects.PathObject;
19+
import qupath.lib.objects.classes.PathClass;
20+
import qupath.lib.objects.classes.PathClassFactory;
21+
import qupath.lib.regions.ImagePlane;
622
import qupath.lib.roi.interfaces.ROI;
723

824
/**
@@ -11,7 +27,11 @@
1127
* <p>
1228
* These include:
1329
* <ul>
30+
* <li>{@link PathObject}</li>
31+
* <li>{@link PathClass}</li>
1432
* <li>{@link ROI}</li>
33+
* <li>{@link ImagePlane}</li>
34+
* <li>Java Topology Suite Geometry objects</li>
1535
* </ul>
1636
*
1737
* @author Pete Bankhead
@@ -20,11 +40,67 @@
2040
public class GsonTools {
2141

2242
private static Gson gson = new GsonBuilder()
23-
.registerTypeHierarchyAdapter(ROI.class, new ROITypeAdapter())
43+
.registerTypeAdapterFactory(new QuPathTypeAdapterFactory())
44+
.registerTypeAdapterFactory(OpenCVTypeAdapters.getOpenCVTypeAdaptorFactory())
45+
// .registerTypeHierarchyAdapter(PathObject.class, PathObjectTypeAdapters.PathObjectTypeAdapter.INSTANCE)
46+
// .registerTypeHierarchyAdapter(MeasurementList.class, PathObjectTypeAdapters.MeasurementListTypeAdapter.INSTANCE)
47+
// .registerTypeHierarchyAdapter(FeatureCollection.class, PathObjectTypeAdapters.PathObjectCollectionTypeAdapter.INSTANCE)
48+
// .registerTypeHierarchyAdapter(ROI.class, ROITypeAdapters.ROI_ADAPTER_INSTANCE)
49+
// .registerTypeHierarchyAdapter(Geometry.class, ROITypeAdapters.GEOMETRY_ADAPTER_INSTANCE)
50+
// .registerTypeAdapter(PathClass.class, PathClassTypeAdapter.INSTANCE)
51+
// .registerTypeAdapter(ImagePlane.class, ImagePlaneTypeAdapter.INSTANCE)
52+
// .registerTypeAdapterFactory(OpenCVTypeAdapters.getOpenCVTypeAdaptorFactory())
2453
.serializeSpecialFloatingPointValues()
2554
.setLenient()
2655
.create();
2756

57+
58+
static class QuPathTypeAdapterFactory implements TypeAdapterFactory {
59+
60+
@Override
61+
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
62+
return getTypeAdaptor((Class<T>)type.getRawType());
63+
}
64+
65+
@SuppressWarnings("unchecked")
66+
static <T> TypeAdapter<T> getTypeAdaptor(Class<T> cls) {
67+
if (PathObject.class.isAssignableFrom(cls))
68+
return (TypeAdapter<T>)PathObjectTypeAdapters.PathObjectTypeAdapter.INSTANCE;
69+
70+
if (MeasurementList.class.isAssignableFrom(cls))
71+
return (TypeAdapter<T>)PathObjectTypeAdapters.MeasurementListTypeAdapter.INSTANCE;
72+
73+
if (FeatureCollection.class.isAssignableFrom(cls))
74+
return (TypeAdapter<T>)PathObjectTypeAdapters.PathObjectCollectionTypeAdapter.INSTANCE;
75+
76+
if (ROI.class.isAssignableFrom(cls))
77+
return (TypeAdapter<T>)ROITypeAdapters.ROI_ADAPTER_INSTANCE;
78+
79+
if (Geometry.class.isAssignableFrom(cls))
80+
return (TypeAdapter<T>)ROITypeAdapters.GEOMETRY_ADAPTER_INSTANCE;
81+
82+
if (PathClass.class.isAssignableFrom(cls))
83+
return (TypeAdapter<T>)PathClassTypeAdapter.INSTANCE;
84+
85+
if (ImagePlane.class.isAssignableFrom(cls))
86+
return (TypeAdapter<T>)ImagePlaneTypeAdapter.INSTANCE;
87+
88+
return null;
89+
}
90+
91+
}
92+
93+
94+
/**
95+
* Wrap a collection of PathObjects as a FeatureCollection. The purpose of this is to enable
96+
* exporting a GeoJSON FeatureCollection that may be reused in other software.
97+
* @param pathObjects
98+
* @return
99+
*/
100+
public static FeatureCollection wrapFeatureCollection(Collection<? extends PathObject> pathObjects) {
101+
return new FeatureCollection(pathObjects);
102+
}
103+
28104
/**
29105
* Get default Gson, capable of serializing/deserializing some key QuPath classes.
30106
* @return
@@ -45,4 +121,77 @@ public static Gson getGsonPretty() {
45121
return getGsonDefault().newBuilder().setPrettyPrinting().create();
46122
}
47123

124+
/**
125+
* TypeAdapter for PathClass objects, ensuring each is a singleton.
126+
*/
127+
static class PathClassTypeAdapter extends TypeAdapter<PathClass> {
128+
129+
static PathClassTypeAdapter INSTANCE = new PathClassTypeAdapter();
130+
131+
private static Gson gson = new Gson();
132+
133+
// TODO: Consider writing just the toString() representation & ensure recreate-able from that (but lacking color?)
134+
@Override
135+
public void write(JsonWriter out, PathClass value) throws IOException {
136+
// Write in the default way
137+
gson.toJson(value, PathClass.class, out);
138+
// Streams.write(gson.toJsonTree(value), out);
139+
}
140+
141+
@Override
142+
public PathClass read(JsonReader in) throws IOException {
143+
// Read in the default way, then replace with a singleton instance
144+
PathClass pathClass = gson.fromJson(in, PathClass.class);
145+
return PathClassFactory.getSingletonPathClass(pathClass);
146+
}
147+
148+
}
149+
150+
/**
151+
* TypeAdapter for ImagePlane objects, ensuring each is a singleton.
152+
*/
153+
static class ImagePlaneTypeAdapter extends TypeAdapter<ImagePlane> {
154+
155+
static ImagePlaneTypeAdapter INSTANCE = new ImagePlaneTypeAdapter();
156+
157+
@Override
158+
public void write(JsonWriter out, ImagePlane plane) throws IOException {
159+
out.beginObject();
160+
out.name("c");
161+
out.value(plane.getC());
162+
out.name("z");
163+
out.value(plane.getZ());
164+
out.name("t");
165+
out.value(plane.getT());
166+
out.endObject();
167+
}
168+
169+
@Override
170+
public ImagePlane read(JsonReader in) throws IOException {
171+
in.beginObject();
172+
173+
ImagePlane plane = ImagePlane.getDefaultPlane();
174+
int c = plane.getC();
175+
int z = plane.getZ();
176+
int t = plane.getT();
177+
178+
while (in.hasNext()) {
179+
switch (in.nextName()) {
180+
case "c":
181+
c = in.nextInt();
182+
break;
183+
case "z":
184+
z = in.nextInt();
185+
break;
186+
case "t":
187+
t = in.nextInt();
188+
break;
189+
}
190+
}
191+
in.endObject();
192+
return ImagePlane.getPlaneWithChannel(c, z, t);
193+
}
194+
195+
}
196+
48197
}

0 commit comments

Comments
 (0)