response = client.send(request, HttpResponse.BodyHandlers.ofString());
170 | if (response.statusCode() == HttpURLConnection.HTTP_OK) {
171 | Dialogs.showMessageDialog("Model reset", "Model is reset");
172 | }
173 | else {
174 | Dialogs.showErrorMessage("Http error: " + response.statusCode(), response.body());
175 | }
176 | } catch (IOException | InterruptedException e) {
177 | e.printStackTrace();
178 | Dialogs.showErrorMessage(getClass().getName(), e);
179 | }
180 | }
181 |
182 | }
183 |
--------------------------------------------------------------------------------
/src/main/java/org/elephant/cellsparse/CellsparseBody.java:
--------------------------------------------------------------------------------
1 | package org.elephant.cellsparse;
2 |
3 |
4 | public class CellsparseBody {
5 |
6 | @SuppressWarnings("unused")
7 | private String modelname;
8 | @SuppressWarnings("unused")
9 | private String b64img;
10 | @SuppressWarnings("unused")
11 | private String b64lbl;
12 | @SuppressWarnings("unused")
13 | private boolean train;
14 | @SuppressWarnings("unused")
15 | private boolean eval;
16 | @SuppressWarnings("unused")
17 | private int epochs;
18 | @SuppressWarnings("unused")
19 | private int batchsize;
20 | @SuppressWarnings("unused")
21 | private int steps;
22 |
23 | public CellsparseBody(final Builder builder) {
24 | this.modelname = builder.modelname;
25 | this.b64img = builder.b64img;
26 | this.b64lbl = builder.b64lbl;
27 | this.train = builder.train;
28 | this.eval = builder.eval;
29 | this.epochs = builder.epochs;
30 | this.batchsize = builder.batchsize;
31 | this.steps = builder.steps;
32 | }
33 |
34 | static class Builder {
35 | private String modelname;
36 | private String b64img;
37 | private String b64lbl = null;
38 | private boolean train = false;
39 | private boolean eval = false;
40 | private int epochs = 10;
41 | private int batchsize = 8;
42 | private int steps = 10;
43 |
44 | public Builder(final String modelname) {
45 | this.modelname = modelname;
46 | };
47 |
48 | public Builder b64img(final String b64img) {
49 | this.b64img = b64img;
50 | return this;
51 | }
52 |
53 | public Builder b64lbl(final String b64lbl) {
54 | this.b64lbl = b64lbl;
55 | return this;
56 | }
57 |
58 | public Builder train(final boolean train) {
59 | this.train = train;
60 | return this;
61 | }
62 |
63 | public Builder eval(final boolean eval) {
64 | this.eval = eval;
65 | return this;
66 | }
67 |
68 | public Builder epochs(final int epochs) {
69 | this.epochs = epochs;
70 | return this;
71 | }
72 |
73 | public Builder batchsize(final int batchsize) {
74 | this.batchsize = batchsize;
75 | return this;
76 | }
77 |
78 | public Builder steps(final int steps) {
79 | this.steps = steps;
80 | return this;
81 | }
82 |
83 | public CellsparseBody build() {
84 | return new CellsparseBody(this);
85 | }
86 | }
87 |
88 | public static CellsparseBody.Builder newBuilder(final String modelname) {
89 | return new Builder(modelname);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/main/java/org/elephant/cellsparse/CellsparseCellposeExtension.java:
--------------------------------------------------------------------------------
1 | package org.elephant.cellsparse;
2 |
3 | import org.controlsfx.control.action.Action;
4 | import qupath.lib.gui.ActionTools;
5 | import qupath.lib.gui.QuPathGUI;
6 | import qupath.lib.gui.ActionTools.ActionDescription;
7 | import qupath.lib.gui.ActionTools.ActionMenu;
8 | import qupath.lib.gui.dialogs.Dialogs;
9 | import qupath.lib.gui.extensions.QuPathExtension;
10 |
11 | public class CellsparseCellposeExtension implements QuPathExtension {
12 |
13 | @Override
14 | public void installExtension(QuPathGUI qupath) {
15 | qupath.installActions(ActionTools.getAnnotatedActions(new CellsparseCellposeCommands(qupath)));
16 | }
17 |
18 | @Override
19 | public String getName() {
20 | return "Cellsparse Cellpose";
21 | }
22 |
23 | @Override
24 | public String getDescription() {
25 | return "Cellpose with sparse annotation";
26 | }
27 |
28 | @ActionMenu("Extensions>Cellsparse")
29 | public class CellsparseCellposeCommands extends AbstractCellsparseCommands {
30 |
31 | @ActionMenu("Cellpose>Training")
32 | @ActionDescription("Cellpose training with sparse annotation.")
33 | public final Action actionTraining;
34 |
35 | @ActionMenu("Cellpose>Inference")
36 | @ActionDescription("Cellpose inference.")
37 | public final Action actionInference;
38 |
39 | @ActionMenu("Cellpose>Reset")
40 | @ActionDescription("Reset Cellpose model.")
41 | public final Action actionReset;
42 |
43 | @ActionMenu("Cellpose>Server URL")
44 | @ActionDescription("Set API server URL.")
45 | public final Action actionSetServerURL;
46 |
47 | private String serverURL = "http://localhost:8000/cellpose/";
48 |
49 | private CellsparseCellposeCommands(QuPathGUI qupath) {
50 | actionTraining = qupath.createImageDataAction(imageData -> {
51 | CellsparseCommand(imageData, serverURL, true, 5, 8, 200);
52 | });
53 |
54 | actionInference = qupath.createImageDataAction(imageData -> {
55 | CellsparseCommand(imageData, serverURL, false);
56 | });
57 |
58 | actionReset = new Action(e -> CellsparseResetCommand(serverURL + "reset/"));
59 |
60 | actionSetServerURL = new Action(event -> {
61 | String newURL = Dialogs.showInputDialog("Server URL", "Set API server URL", serverURL);
62 | if (newURL != null) {
63 | serverURL = newURL;
64 | }
65 | });
66 | }
67 |
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/src/main/java/org/elephant/cellsparse/CellsparseElephantExtension.java:
--------------------------------------------------------------------------------
1 | package org.elephant.cellsparse;
2 |
3 | import org.controlsfx.control.action.Action;
4 | import qupath.lib.gui.ActionTools;
5 | import qupath.lib.gui.QuPathGUI;
6 | import qupath.lib.gui.ActionTools.ActionDescription;
7 | import qupath.lib.gui.ActionTools.ActionMenu;
8 | import qupath.lib.gui.dialogs.Dialogs;
9 | import qupath.lib.gui.extensions.QuPathExtension;
10 |
11 | public class CellsparseElephantExtension implements QuPathExtension {
12 |
13 | @Override
14 | public void installExtension(QuPathGUI qupath) {
15 | qupath.installActions(ActionTools.getAnnotatedActions(new CellsparseElephantCommands(qupath)));
16 | }
17 |
18 | @Override
19 | public String getName() {
20 | return "Cellsparse ELEPHANT";
21 | }
22 |
23 | @Override
24 | public String getDescription() {
25 | return "ELEPHANT with sparse annotation";
26 | }
27 |
28 | @ActionMenu("Extensions>Cellsparse")
29 | public class CellsparseElephantCommands extends AbstractCellsparseCommands {
30 |
31 | @ActionMenu("ELEPHANT>Training")
32 | @ActionDescription("ELEPHANT training with sparse annotation.")
33 | public final Action actionTraining;
34 |
35 | @ActionMenu("ELEPHANT>Inference")
36 | @ActionDescription("ELEPHANT inference.")
37 | public final Action actionInference;
38 |
39 | @ActionMenu("ELEPHANT>Reset")
40 | @ActionDescription("Reset ELEPHANT model.")
41 | public final Action actionReset;
42 |
43 | @ActionMenu("ELEPHANT>Server URL")
44 | @ActionDescription("Set API server URL.")
45 | public final Action actionSetServerURL;
46 |
47 | private String serverURL = "http://localhost:8000/elephant/";
48 |
49 | private CellsparseElephantCommands(QuPathGUI qupath) {
50 | actionTraining = qupath.createImageDataAction(imageData -> {
51 | CellsparseCommand(imageData, serverURL, true, 1, 8, 200);
52 | });
53 |
54 | actionInference = qupath.createImageDataAction(imageData -> {
55 | CellsparseCommand(imageData, serverURL, false);
56 | });
57 |
58 | actionReset = new Action(event -> CellsparseResetCommand(serverURL + "reset/"));
59 |
60 | actionSetServerURL = new Action(event -> {
61 | String newURL = Dialogs.showInputDialog("Server URL", "Set API server URL", serverURL);
62 | if (newURL != null) {
63 | serverURL = newURL;
64 | }
65 | });
66 | }
67 |
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/src/main/java/org/elephant/cellsparse/CellsparseResetBody.java:
--------------------------------------------------------------------------------
1 | package org.elephant.cellsparse;
2 |
3 |
4 | public class CellsparseResetBody {
5 |
6 | @SuppressWarnings("unused")
7 | private String modelname;
8 |
9 | public CellsparseResetBody(final Builder builder) {
10 | this.modelname = builder.modelname;
11 | }
12 |
13 | static class Builder {
14 | private String modelname;
15 |
16 | public Builder(final String modelname) {
17 | this.modelname = modelname;
18 | };
19 |
20 | public CellsparseResetBody build() {
21 | return new CellsparseResetBody(this);
22 | }
23 | }
24 |
25 | public static CellsparseResetBody.Builder newBuilder(final String modelname) {
26 | return new Builder(modelname);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/org/elephant/cellsparse/CellsparseStarDistExtension.java:
--------------------------------------------------------------------------------
1 | package org.elephant.cellsparse;
2 |
3 | import org.controlsfx.control.action.Action;
4 | import qupath.lib.gui.ActionTools;
5 | import qupath.lib.gui.QuPathGUI;
6 | import qupath.lib.gui.ActionTools.ActionDescription;
7 | import qupath.lib.gui.ActionTools.ActionMenu;
8 | import qupath.lib.gui.dialogs.Dialogs;
9 | import qupath.lib.gui.extensions.QuPathExtension;
10 |
11 | public class CellsparseStarDistExtension implements QuPathExtension {
12 |
13 | @Override
14 | public void installExtension(QuPathGUI qupath) {
15 | qupath.installActions(ActionTools.getAnnotatedActions(new CellsparseStarDistCommands(qupath)));
16 | }
17 |
18 | @Override
19 | public String getName() {
20 | return "Cellsparse StarDist";
21 | }
22 |
23 | @Override
24 | public String getDescription() {
25 | return "StarDist with sparse annotation";
26 | }
27 |
28 | @ActionMenu("Extensions>Cellsparse")
29 | public class CellsparseStarDistCommands extends AbstractCellsparseCommands {
30 |
31 | @ActionMenu("StarDist>Training")
32 | @ActionDescription("StarDist training with sparse annotation.")
33 | public final Action actionTraining;
34 |
35 | @ActionMenu("StarDist>Inference")
36 | @ActionDescription("StarDist inference.")
37 | public final Action actionInference;
38 |
39 | @ActionMenu("StarDist>Reset")
40 | @ActionDescription("Reset StarDist model.")
41 | public final Action actionReset;
42 |
43 | @ActionMenu("StarDist>Server URL")
44 | @ActionDescription("Set API server URL.")
45 | public final Action actionSetServerURL;
46 |
47 | private String serverURL = "http://localhost:8000/stardist/";
48 |
49 | private CellsparseStarDistCommands(QuPathGUI qupath) {
50 | actionTraining = qupath.createImageDataAction(imageData -> {
51 | CellsparseCommand(imageData, serverURL, true, 1, 8, 200);
52 | });
53 |
54 | actionInference = qupath.createImageDataAction(imageData -> {
55 | CellsparseCommand(imageData, serverURL, false);
56 | });
57 |
58 | actionReset = new Action(e -> CellsparseResetCommand(serverURL + "reset/"));
59 |
60 | actionSetServerURL = new Action(event -> {
61 | String newURL = Dialogs.showInputDialog("Server URL", "Set API server URL", serverURL);
62 | if (newURL != null) {
63 | serverURL = newURL;
64 | }
65 | });
66 | }
67 |
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/src/main/java/qupath/lib/images/servers/LabeledOffsetImageServer.java:
--------------------------------------------------------------------------------
1 | /*-
2 | * #%L
3 | * This file is part of QuPath.
4 | * %%
5 | * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh
6 | * %%
7 | * QuPath is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as
9 | * published by the Free Software Foundation, either version 3 of the
10 | * License, or (at your option) any later version.
11 | *
12 | * QuPath is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with QuPath. If not, see .
19 | * #L%
20 | */
21 |
22 | package qupath.lib.images.servers;
23 |
24 | import java.awt.BasicStroke;
25 | import java.awt.Color;
26 | import java.awt.Graphics2D;
27 | import java.awt.image.BandedSampleModel;
28 | import java.awt.image.BufferedImage;
29 | import java.awt.image.ColorModel;
30 | import java.awt.image.DataBuffer;
31 | import java.awt.image.DataBufferByte;
32 | import java.awt.image.WritableRaster;
33 | import java.io.IOException;
34 | import java.net.URI;
35 | import java.util.ArrayList;
36 | import java.util.Collection;
37 | import java.util.Collections;
38 | import java.util.HashMap;
39 | import java.util.LinkedHashMap;
40 | import java.util.List;
41 | import java.util.Map;
42 | import java.util.Random;
43 | import java.util.TreeMap;
44 | import java.util.UUID;
45 | import java.util.function.Function;
46 | import java.util.function.Predicate;
47 | import java.util.stream.Collectors;
48 |
49 | import org.slf4j.Logger;
50 | import org.slf4j.LoggerFactory;
51 |
52 | import qupath.lib.color.ColorMaps;
53 | import qupath.lib.color.ColorModelFactory;
54 | import qupath.lib.color.ColorToolsAwt;
55 | import qupath.lib.common.ColorTools;
56 | import qupath.lib.images.ImageData;
57 | import qupath.lib.images.servers.ImageServerMetadata.ChannelType;
58 | import qupath.lib.images.servers.ImageServerBuilder.ServerBuilder;
59 | import qupath.lib.objects.PathObject;
60 | import qupath.lib.objects.PathObjectFilter;
61 | import qupath.lib.objects.PathObjectTools;
62 | import qupath.lib.objects.classes.PathClass;
63 | import qupath.lib.objects.hierarchy.PathObjectHierarchy;
64 | import qupath.lib.regions.ImageRegion;
65 | import qupath.lib.regions.RegionRequest;
66 | import qupath.lib.roi.RoiTools;
67 | import qupath.lib.roi.interfaces.ROI;
68 |
69 |
70 | /**
71 | * A special ImageServer implementation that doesn't have a backing image, but rather
72 | * constructs tiles from a {@link PathObjectHierarchy} where pixel values are integer labels corresponding
73 | * stored and classified annotations.
74 | *
75 | * Warning! This is intend for temporary use when exporting labelled images. No attempt is made to
76 | * respond to changes within the hierarchy. For consistent results, the hierarchy must remain static for the
77 | * time in which this server is being used.
78 | *
79 | * @author Pete Bankhead
80 | *
81 | */
82 | public class LabeledOffsetImageServer extends AbstractTileableImageServer implements GeneratingImageServer {
83 |
84 | private static final Logger logger = LoggerFactory.getLogger(LabeledOffsetImageServer.class);
85 |
86 | private ImageServerMetadata originalMetadata;
87 |
88 | // Easy way to get the default color models...
89 | private static final ColorModel COLOR_MODEL_GRAY_UINT8 = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY).getColorModel();
90 | private static final ColorModel COLOR_MODEL_GRAY_UINT16 = new BufferedImage(1, 1, BufferedImage.TYPE_USHORT_GRAY).getColorModel();
91 |
92 | private PathObjectHierarchy hierarchy;
93 |
94 | private ColorModel colorModel;
95 | private boolean multichannelOutput;
96 |
97 | private LabeledServerParameters params;
98 |
99 | /**
100 | * The maximum requested label; this is used to determine the output depth for indexed images.
101 | */
102 | private int maxLabel;
103 |
104 | private Map instanceClassMap = null;
105 | private Map instanceClassMapInverse = null;
106 |
107 | private LabeledOffsetImageServer(final ImageData imageData, double downsample, int tileWidth, int tileHeight, LabeledServerParameters params, boolean multichannelOutput, int offset) {
108 | super();
109 |
110 | this.multichannelOutput = multichannelOutput;
111 | this.hierarchy = imageData.getHierarchy();
112 |
113 | this.params = params;
114 |
115 | var server = imageData.getServer();
116 |
117 | // Generate mapping for labels; it is permissible to have multiple classes for the same labels, in which case a derived class will be used
118 | Map classificationLabels = new TreeMap<>();
119 | if (params.createInstanceLabels) {
120 | var pathObjects = imageData.getHierarchy().getObjects(null, null).stream()
121 | .filter(params.objectFilter)
122 | .collect(Collectors.toCollection(ArrayList::new));
123 | // Shuffle the objects, this helps when using grayscale lookup tables, since labels for neighboring objects are otherwise very similar
124 | if (params.shuffleInstanceLabels)
125 | Collections.shuffle(pathObjects, new Random(100L));
126 | Integer count = multichannelOutput ? 0 : offset + 1;
127 | instanceClassMap = new HashMap<>();
128 | instanceClassMapInverse = new HashMap<>();
129 | for (var pathObject : pathObjects) {
130 | var pathClass = instanceLabelToClass(count);
131 | instanceClassMap.put(pathObject, count);
132 | instanceClassMapInverse.put(count, pathObject);
133 | classificationLabels.put(count, pathClass);
134 | params.labelColors.put(count, pathClass.getColor());
135 | params.labels.put(pathClass, count);
136 | count++;
137 | }
138 | } else {
139 | for (var entry : params.labels.entrySet()) {
140 | var pathClass = getPathClass(entry.getKey());
141 | var label = entry.getValue();
142 | var previousClass = classificationLabels.put(label, pathClass);
143 | if (previousClass != null && previousClass != PathClass.NULL_CLASS) {
144 | classificationLabels.put(label, PathClass.getInstance(previousClass, pathClass.getName(), null));
145 | }
146 | }
147 | }
148 |
149 | for (var entry : params.boundaryLabels.entrySet()) {
150 | var pathClass = getPathClass(entry.getKey());
151 | var label = entry.getValue();
152 | var previousClass = classificationLabels.put(label, pathClass);
153 | if (previousClass != null && previousClass != PathClass.NULL_CLASS) {
154 | classificationLabels.put(label, PathClass.getInstance(previousClass, pathClass.getName(), null));
155 | }
156 | }
157 |
158 | if (tileWidth <= 0)
159 | tileWidth = 512;
160 | if (tileHeight <= 0)
161 | tileHeight = tileWidth;
162 |
163 | var metadataBuilder = new ImageServerMetadata.Builder(server.getMetadata())
164 | .preferredTileSize(tileWidth, tileHeight)
165 | .levelsFromDownsamples(downsample)
166 | .pixelType(PixelType.UINT8)
167 | .rgb(false);
168 |
169 | // Check the labels are valid
170 | var labelStats = classificationLabels.keySet().stream().mapToInt(i -> i).summaryStatistics();
171 | int minLabel = labelStats.getMin();
172 | maxLabel = labelStats.getMax();
173 | if (minLabel < 0) {
174 | throw new IllegalArgumentException("Minimum possible label value is 0! Requested minimum was " + maxLabel);
175 | }
176 | if (multichannelOutput) {
177 | int nChannels = maxLabel + 1;
178 | if (params.maxOutputChannelLimit > 0 && nChannels > params.maxOutputChannelLimit)
179 | throw new IllegalArgumentException("You've requested " + nChannels + " output channels, but the maximum supported number is " + params.maxOutputChannelLimit);
180 | }
181 |
182 | if (multichannelOutput) {
183 | int nLabels = maxLabel - minLabel + 1;
184 | if (minLabel != 0 || nLabels != classificationLabels.size()) {
185 | throw new IllegalArgumentException("Labels for multichannel output must be consecutive integers starting from 0! Requested labels " + classificationLabels.keySet());
186 | }
187 | var channels = ServerTools.classificationLabelsToChannels(classificationLabels, false);
188 | // It's a bit sad... but if we want grayscale output, we need to set the channels here
189 | if (params.grayscaleLut)
190 | channels = channels.stream().map(c -> ImageChannel.getInstance(c.getName(), ColorTools.WHITE)).collect(Collectors.toList());
191 | metadataBuilder = metadataBuilder
192 | .channelType(ChannelType.MULTICLASS_PROBABILITY)
193 | .channels(channels)
194 | .classificationLabels(classificationLabels);
195 | colorModel = ColorModelFactory.createColorModel(PixelType.UINT8, channels);
196 | } else {
197 | metadataBuilder = metadataBuilder
198 | .channelType(ChannelType.CLASSIFICATION)
199 | .classificationLabels(classificationLabels);
200 |
201 | // Update the color map, ensuring we don't have null
202 | var colors = new LinkedHashMap();
203 | for (var entry : params.labelColors.entrySet()) {
204 | var key = entry.getKey();
205 | var value = entry.getValue();
206 | if (key == null) {
207 | logger.debug("Missing key in label map! Will be skipped.");
208 | continue;
209 | }
210 | if (value == null) {
211 | // Flip the bits of the background color, if needed
212 | logger.debug("Missing color in label map! Will be derived from the background color.");
213 | var backgroundColor = params.labelColors.get(params.labels.get(params.unannotatedClass));
214 | value = backgroundColor == null ? 0 : ~backgroundColor.intValue();
215 | }
216 | colors.put(key, value);
217 | }
218 |
219 | if (params.grayscaleLut) {
220 | if (maxLabel < 255)
221 | colorModel = COLOR_MODEL_GRAY_UINT8;
222 | else if (maxLabel < 65536){
223 | colorModel = COLOR_MODEL_GRAY_UINT16;
224 | metadataBuilder.pixelType(PixelType.UINT16);
225 | } else {
226 | colorModel = ColorModelFactory.createColorModel(PixelType.FLOAT32,
227 | ColorMaps.createColorMap("labels", 255, 255, 255),
228 | 0,
229 | 0,
230 | maxLabel,
231 | -1,
232 | null);
233 | metadataBuilder.pixelType(PixelType.FLOAT32);
234 | }
235 | } else {
236 | if (maxLabel < 65536) {
237 | colorModel = ColorModelFactory.createIndexedColorModel(colors, false);
238 | if (maxLabel > 255)
239 | metadataBuilder.pixelType(PixelType.UINT16);
240 | } else {
241 | colorModel = ColorModelFactory.getDummyColorModel(32);
242 | metadataBuilder.channels(ImageChannel.getDefaultRGBChannels());
243 | }
244 | }
245 | }
246 |
247 | // Set metadata, using the underlying server as a basis
248 | this.originalMetadata = metadataBuilder.build();
249 | }
250 |
251 | /**
252 | * @param pathClass
253 | * @return the input classification, or the unclassified classification if the input is null
254 | */
255 | private static PathClass getPathClass(PathClass pathClass) {
256 | return pathClass == null ? PathClass.NULL_CLASS : pathClass;
257 | }
258 |
259 | /**
260 | * Get a standardized classification for an object.
261 | * If unique labels are requested, this will return the unique classification associated with this object
262 | * or null if no unique classification is available (i.e. the object should not be included).
263 | * Otherwise, it will return either the objects's classification or the unclassified class (not null).
264 | * @param pathObject
265 | * @return
266 | */
267 | private PathClass getPathClass(PathObject pathObject) {
268 | if (instanceClassMap != null)
269 | return instanceLabelToClass(instanceClassMap.get(pathObject));
270 | return getPathClass(pathObject.getPathClass());
271 | }
272 |
273 |
274 | private static PathClass instanceLabelToClass(Integer label) {
275 | if (label == null)
276 | return null;
277 | return PathClass.getInstance("Label " + label);
278 | }
279 |
280 | // /**
281 | // * Get the label associated with a specific {@link PathObject}.
282 | // * This will be based on the instance if {@link Builder#useInstanceLabels()} is selected,
283 | // * or the classification.
284 | // * @param pathObject
285 | // * @return the label if available, or null if no label is associated with the object
286 | // */
287 | // public Integer getLabel(PathObject pathObject) {
288 | // if (!this.params.objectFilter.test(pathObject))
289 | // return null;
290 | // if (params.createInstanceLabels)
291 | // return instanceClassMap.get(pathObject);
292 | // return params.labels.get(getPathClass(pathObject));
293 | // }
294 |
295 | /**
296 | * Get a mapping between objects and instance labels.
297 | * @return the instance label map, or an empty map if no objects are available or
298 | * {@link Builder#useInstanceLabels()} was not selected.
299 | */
300 | public Map getInstanceLabels() {
301 | if (instanceClassMap == null)
302 | return Collections.emptyMap();
303 | return Collections.unmodifiableMap(instanceClassMap);
304 | }
305 |
306 | /**
307 | * Get an unmodifiable map of classifications and their corresponding labels.
308 | * Note that multiple classifications may use the same integer label.
309 | * @return a map of labels, or empty map if none are available or {@code useInstanceLabels()} was selected.
310 | */
311 | public Map getLabels() {
312 | if (params.createInstanceLabels)
313 | return Collections.emptyMap();
314 | return Collections.unmodifiableMap(params.labels);
315 | }
316 |
317 | /**
318 | * Get an unmodifiable map of classifications and their corresponding boundary labels, if available.
319 | * Note that multiple classifications may use the same integer label.
320 | * @return a map of boundary labels, or empty map if none are available or {@code useInstanceLabels()} was selected.
321 | */
322 | public Map getBoundaryLabels() {
323 | if (params.createInstanceLabels)
324 | return Collections.emptyMap();
325 | return Collections.unmodifiableMap(params.boundaryLabels);
326 | }
327 |
328 |
329 |
330 | private static class LabeledServerParameters {
331 |
332 | /**
333 | * Background class (name must not clash with any 'real' class)
334 | * Previously, this was achieved with a UUID - although this looks strange if exporting classes.
335 | */
336 | // private PathClass unannotatedClass = PathClassFactory.getPathClass("Unannotated " + UUID.randomUUID().toString());
337 | private PathClass unannotatedClass = PathClass.getInstance("*Background*");
338 |
339 | private Predicate objectFilter = PathObjectFilter.ANNOTATIONS;
340 | private Function roiFunction = p -> p.getROI();
341 |
342 | private boolean createInstanceLabels = false;
343 | private boolean shuffleInstanceLabels = true; // Only if using instance labels
344 |
345 | private int maxOutputChannelLimit = 256;
346 |
347 | private boolean grayscaleLut = false;
348 |
349 | private float lineThickness = 1.0f;
350 | private Map labels = new LinkedHashMap<>();
351 | private Map boundaryLabels = new LinkedHashMap<>();
352 | private Map labelColors = new LinkedHashMap<>();
353 |
354 | LabeledServerParameters() {
355 | labels.put(unannotatedClass, 0);
356 | labelColors.put(0, ColorTools.WHITE);
357 | }
358 |
359 | LabeledServerParameters(LabeledServerParameters params) {
360 | this.unannotatedClass = params.unannotatedClass;
361 | this.lineThickness = params.lineThickness;
362 | this.objectFilter = params.objectFilter;
363 | this.labels = new LinkedHashMap<>(params.labels);
364 | this.boundaryLabels = new LinkedHashMap<>(params.boundaryLabels);
365 | this.labelColors = new LinkedHashMap<>(params.labelColors);
366 | this.createInstanceLabels = params.createInstanceLabels;
367 | this.maxOutputChannelLimit = params.maxOutputChannelLimit;
368 | this.roiFunction = params.roiFunction;
369 | this.grayscaleLut = params.grayscaleLut;
370 | this.shuffleInstanceLabels = params.shuffleInstanceLabels;
371 | }
372 |
373 | }
374 |
375 | /**
376 | * Helper class for building a {@link LabeledImageServer}.
377 | */
378 | public static class Builder {
379 |
380 | private ImageData imageData;
381 | private double downsample = 1.0;
382 | private int tileWidth, tileHeight;
383 |
384 | private boolean multichannelOutput = false;
385 |
386 | private int offset = 0;
387 |
388 | private LabeledServerParameters params = new LabeledServerParameters();
389 |
390 | /**
391 | * Create a Builder for a {@link LabeledImageServer} for the specified {@link ImageData}.
392 | * @param imageData
393 | */
394 | public Builder(ImageData imageData) {
395 | this.imageData = imageData;
396 | }
397 |
398 | /**
399 | * Use detections rather than annotations for labels.
400 | * The default is to use annotations.
401 | * @return
402 | * @see #useAnnotations()
403 | */
404 | public Builder useDetections() {
405 | params.objectFilter = PathObjectFilter.DETECTIONS_ALL;
406 | return this;
407 | }
408 |
409 | /**
410 | * Use cells rather than annotations for labels.
411 | * The default is to use annotations.
412 | * @return
413 | * @see #useAnnotations()
414 | */
415 | public Builder useCells() {
416 | params.objectFilter = PathObjectFilter.CELLS;
417 | return this;
418 | }
419 |
420 | /**
421 | * Use cells rather than annotations for labels, requesting the nucleus ROI where available.
422 | * The default is to use annotations.
423 | * @return
424 | * @see #useAnnotations()
425 | */
426 | public Builder useCellNuclei() {
427 | params.objectFilter = PathObjectFilter.CELLS;
428 | params.roiFunction = p -> PathObjectTools.getROI(p, true);
429 | return this;
430 | }
431 |
432 | /**
433 | * Use annotations for labels. This is the default.
434 | * @return
435 | * @see #useDetections()
436 | */
437 | public Builder useAnnotations() {
438 | params.objectFilter = PathObjectFilter.ANNOTATIONS;
439 | return this;
440 | }
441 |
442 | /**
443 | * Use a custom method of selecting objects for inclusion.
444 | * The default is to use annotations.
445 | * @param filter the filter that determines whether an object will be included or not
446 | * @return
447 | * @see #useAnnotations()
448 | */
449 | public Builder useFilter(Predicate filter) {
450 | params.objectFilter = filter;
451 | return this;
452 | }
453 |
454 | /**
455 | * Use grayscale LUT, rather than deriving colors from classifications.
456 | * This can streamline import in software that automatically converts paletted images to RGB.
457 | * @return
458 | * @since v0.4.0
459 | * @see #grayscale(boolean)
460 | */
461 | public Builder grayscale() {
462 | return grayscale(true);
463 | }
464 |
465 | /**
466 | * Optionally use grayscale LUT, rather than deriving colors from classifications.
467 | * This can streamline import in software that automatically converts paletted images to RGB.
468 | * @param grayscaleLut
469 | * @return
470 | * @since v0.4.0
471 | * @see #grayscale()
472 | */
473 | public Builder grayscale(boolean grayscaleLut) {
474 | params.grayscaleLut = grayscaleLut;
475 | return this;
476 | }
477 |
478 | /**
479 | * Specify downsample factor. This is very important because it defines
480 | * the resolution at which shapes will be drawn and the line thickness is determined.
481 | * @param downsample
482 | * @return
483 | */
484 | public Builder downsample(double downsample) {
485 | this.downsample = downsample;
486 | return this;
487 | }
488 |
489 | /**
490 | * Set tile width and height (square tiles).
491 | * @param tileSize
492 | * @return
493 | */
494 | public Builder tileSize(int tileSize) {
495 | return tileSize(tileSize, tileSize);
496 | }
497 |
498 | /**
499 | * Set tile width and height.
500 | * @param tileWidth
501 | * @param tileHeight
502 | * @return
503 | */
504 | public Builder tileSize(int tileWidth, int tileHeight) {
505 | this.tileWidth = tileWidth;
506 | this.tileHeight = tileHeight;
507 | return this;
508 | }
509 |
510 | /**
511 | * Thickness of boundary lines and line annotations, defined in terms of pixels at the
512 | * resolution specified by the downsample value of the server.
513 | * @param thickness
514 | * @return
515 | */
516 | public Builder lineThickness(float thickness) {
517 | params.lineThickness = thickness;
518 | return this;
519 | }
520 |
521 |
522 | /**
523 | * @return
524 | * @deprecated in favor of {@link #useInstanceLabels()}
525 | */
526 | @Deprecated
527 | public Builder useUniqueLabels() {
528 | logger.warn("useUniqueLabels() is deprecated; please switch to useInstanceLabels() instead.");
529 | return useInstanceLabels();
530 | }
531 |
532 | /**
533 | * Request that unique labels are used for all objects, rather than classifications.
534 | * If this flag is set, all other label requests are ignored.
535 | * @return
536 | * @see #useInstanceLabels(boolean)
537 | * @see #shuffleInstanceLabels(boolean)
538 | */
539 | public Builder useInstanceLabels() {
540 | return useInstanceLabels(true);
541 | }
542 |
543 | /**
544 | * Optionally request that unique labels are used for all objects, rather than classifications.
545 | * If this flag is set, all other label requests are ignored.
546 | * @param instanceLabels
547 | * @return
548 | * @since v0.4.0
549 | * @see #useInstanceLabels()
550 | * @see #shuffleInstanceLabels(boolean)
551 | */
552 | public Builder useInstanceLabels(boolean instanceLabels) {
553 | params.createInstanceLabels = instanceLabels;
554 | return this;
555 | }
556 |
557 |
558 | /**
559 | * Optionally request that instance labels are shuffled.
560 | * Default is true.
561 | * Only has an effect if {@link #useInstanceLabels(boolean)} is called with {@code true}.
562 | * @param doShuffle
563 | * @return
564 | * @since v0.4.0
565 | * @see #useInstanceLabels()
566 | * @see #useInstanceLabels(boolean)
567 | */
568 | public Builder shuffleInstanceLabels(boolean doShuffle) {
569 | params.shuffleInstanceLabels = doShuffle;
570 | return this;
571 | }
572 |
573 |
574 | /**
575 | * If true, the output image consists of multiple binary images concatenated as different channels,
576 | * so that the channel number relates to a classification.
577 | * If false, the output image is a single-channel indexed image so that each pixel value relates to
578 | * a classification.
579 | * Indexed images are much more efficient, but are unable to support more than one classification per pixel.
580 | * @param doMultichannel
581 | * @return
582 | */
583 | public Builder multichannelOutput(boolean doMultichannel) {
584 | this.multichannelOutput = doMultichannel;
585 | return this;
586 | }
587 |
588 | public Builder offset(int offset) {
589 | this.offset = offset;
590 | return this;
591 | }
592 |
593 | /**
594 | * Specify the background label (0 by default).
595 | * @param label
596 | * @return
597 | */
598 | public Builder backgroundLabel(int label) {
599 | return backgroundLabel(label, ColorTools.packRGB(255, 255, 255));
600 | }
601 |
602 | /**
603 | * Specify the background label (0 by default) and color.
604 | * @param label
605 | * @param color
606 | * @return
607 | */
608 | public Builder backgroundLabel(int label, Integer color) {
609 | addLabel(params.unannotatedClass, label, color);
610 | return this;
611 | }
612 |
613 | /**
614 | * Add multiple labels by classname, where the key represents a classname and the value
615 | * represents the integer label that should be used for annotations of the given class.
616 | * @param labelMap
617 | * @return
618 | */
619 | public Builder addLabelsByName(Map labelMap) {
620 | for (var entry : labelMap.entrySet())
621 | addLabel(entry.getKey(), entry.getValue());
622 | return this;
623 | }
624 |
625 | /**
626 | * Add multiple labels by PathClass, where the key represents a PathClass and the value
627 | * represents the integer label that should be used for annotations of the given class.
628 | * @param labelMap
629 | * @return
630 | */
631 | public Builder addLabels(Map labelMap) {
632 | for (var entry : labelMap.entrySet())
633 | addLabel(entry.getKey(), entry.getValue());
634 | return this;
635 | }
636 |
637 | /**
638 | * Add a single label by classname, where the label represents the integer label used for
639 | * annotations with the given classname.
640 | * @param pathClassName
641 | * @param label
642 | * @return
643 | */
644 | public Builder addLabel(String pathClassName, int label) {
645 | return addLabel(pathClassName, label, null);
646 | }
647 |
648 | /**
649 | * Add a single label by classname, where the label represents the integer label used for
650 | * annotations with the given classname.
651 | * @param pathClassName
652 | * @param label the indexed image pixel value or channel number for the given classification
653 | * @param color the color of the lookup table used with any indexed image
654 | * @return
655 | */
656 | public Builder addLabel(String pathClassName, int label, Integer color) {
657 | return addLabel(PathClass.fromString(pathClassName), label, color);
658 | }
659 |
660 | /**
661 | * Add a single label by {@link PathClass}, where the label represents the integer label used for
662 | * annotations with the given classification.
663 | * @param pathClass
664 | * @param label the indexed image pixel value or channel number for the given classification
665 | * @return
666 | */
667 | public Builder addLabel(PathClass pathClass, int label) {
668 | return addLabel(pathClass, label, null);
669 | }
670 |
671 | /**
672 | * Add a single label by {@link PathClass}, where the label represents the integer label used for
673 | * annotations with the given classification.
674 | * @param pathClass
675 | * @param label the indexed image pixel value or channel number for the given classification
676 | * @param color the color of the lookup table used with any indexed image
677 | * @return
678 | */
679 | public Builder addLabel(PathClass pathClass, int label, Integer color) {
680 | return addLabel(params.labels, pathClass, label, color);
681 | }
682 |
683 | /**
684 | * Add a single label for objects that are unclassified, where the label represents the integer label used for
685 | * annotations that have no classification set.
686 | * @param label the indexed image pixel value or channel number without a classification
687 | * @param color the color of the lookup table used with any indexed image
688 | * @return
689 | */
690 | public Builder addUnclassifiedLabel(int label, Integer color) {
691 | return addLabel(params.labels, PathClass.NULL_CLASS, label, color);
692 | }
693 |
694 | /**
695 | * Add a single label for objects that are unclassified, where the label represents the integer label used for
696 | * annotations that have no classification set.
697 | * @param label the indexed image pixel value or channel number without a classification
698 | * @return
699 | */
700 | public Builder addUnclassifiedLabel(int label) {
701 | return addLabel(params.labels, PathClass.NULL_CLASS, label, null);
702 | }
703 |
704 |
705 | /**
706 | * Set the classification and label to use for boundaries for classified areas.
707 | * @param pathClass
708 | * @param label the indexed image pixel value or channel number for the given classification
709 | * @return
710 | */
711 | public Builder setBoundaryLabel(PathClass pathClass, int label) {
712 | return setBoundaryLabel(pathClass, label, null);
713 | }
714 |
715 | /**
716 | * Set the classification and label to use for boundaries for classified areas.
717 | * @param pathClass
718 | * @param label the indexed image pixel value or channel number for the given classification
719 | * @param color the color of the lookup table used with any indexed image
720 | * @return
721 | */
722 | public Builder setBoundaryLabel(PathClass pathClass, int label, Integer color) {
723 | params.boundaryLabels.clear();
724 | return addLabel(params.boundaryLabels, pathClass, label, color);
725 | }
726 |
727 | /**
728 | * Set the classification and label to use for boundaries for classified areas.
729 | * @param pathClassName
730 | * @param label the indexed image pixel value or channel number for the given classification
731 | * @return
732 | */
733 | public Builder setBoundaryLabel(String pathClassName, int label) {
734 | return setBoundaryLabel(pathClassName, label, null);
735 | }
736 |
737 | /**
738 | * Set the classification and label to use for boundaries for classified areas.
739 | * @param pathClassName
740 | * @param label the indexed image pixel value or channel number for the given classification
741 | * @param color the color of the lookup table used with any indexed image
742 | * @return
743 | */
744 | public Builder setBoundaryLabel(String pathClassName, int label, Integer color) {
745 | return setBoundaryLabel(PathClass.fromString(pathClassName), label, color);
746 | }
747 |
748 | private Builder addLabel(Map map, PathClass pathClass, int label, Integer color) {
749 | pathClass = getPathClass(pathClass);
750 | map.put(pathClass, label);
751 | if (color != null)
752 | params.labelColors.put(label, color);
753 | else if (!params.labelColors.containsKey(label))
754 | params.labelColors.put(label, pathClass.getColor());
755 | return this;
756 | }
757 |
758 | /**
759 | * Specify the maximum number of output channels allowed before QuPath will throw an exception.
760 | * This is used to guard against inadvertently requesting a labelled image that would have an infeasibly
761 | * large number of output channels, most commonly with {@link #useInstanceLabels()}.
762 | * @param maxChannels the maximum supported channels; set (cautiously!) ≤ 0 to ignore the limit entirely.
763 | * @return
764 | */
765 | public Builder maxOutputChannelLimit(int maxChannels) {
766 | params.maxOutputChannelLimit = maxChannels;
767 | return this;
768 | }
769 |
770 | /**
771 | * Build the {@link ImageServer} with the requested parameters.
772 | * @return
773 | */
774 | public LabeledOffsetImageServer build() {
775 | if (params.createInstanceLabels) {
776 | if (!(params.labels.isEmpty() || (params.labels.size() == 1 && params.labels.containsKey(params.unannotatedClass))))
777 | throw new IllegalArgumentException("You cannot use both useInstanceLabels() and addLabel() - please choose one or the other!");
778 | if (params.objectFilter == null)
779 | throw new IllegalArgumentException("Please specify an object filter with useInstanceLabels(), for example useDetections(), useCells(), useAnnotations(), useFilter()");
780 | }
781 |
782 | return new LabeledOffsetImageServer(
783 | imageData, downsample, tileWidth, tileHeight,
784 | new LabeledServerParameters(params),
785 | multichannelOutput,
786 | offset);
787 | }
788 |
789 | }
790 |
791 |
792 | /**
793 | * Returns null (does not support ServerBuilders).
794 | */
795 | @Override
796 | protected ServerBuilder createServerBuilder() {
797 | return null;
798 | }
799 |
800 | @Override
801 | public Collection getURIs() {
802 | return Collections.emptyList();
803 | }
804 |
805 | /**
806 | * Returns a UUID.
807 | */
808 | @Override
809 | protected String createID() {
810 | return UUID.randomUUID().toString();
811 | }
812 |
813 | /**
814 | * Returns true if there are no objects to be painted within the requested region.
815 | *
816 | * @apiNote In v0.2 this performed a fast bounding box check only. In v0.3 it was updated to test ROIs fully for
817 | * an intersection.
818 | * @implNote Since v0.3 the request is expanded by the line thickness before testing intersection. In some edge cases, this might result
819 | * in returning true even if nothing is drawn within the region. There remains a balance between returning quickly and
820 | * giving an exact result.
821 | */
822 | @Override
823 | public boolean isEmptyRegion(RegionRequest request) {
824 | double thicknessScale = request.getDownsample() / getDownsampleForResolution(0);
825 | int pad = (int)Math.ceil(params.lineThickness * thicknessScale);
826 | var request2 = pad > 0 ? request.pad2D(pad, pad) : request;
827 | return !getObjectsForRegion(request2)
828 | .stream()
829 | .anyMatch(p -> RoiTools.intersectsRegion(p.getROI(), request2));
830 | }
831 |
832 | /**
833 | * Get the objects to be painted that fall within a specified region.
834 | * Note that this does not take into consideration line thickness, and therefore results are not guaranteed
835 | * to match {@link #isEmptyRegion(RegionRequest)}; in other worse, an object might fall outside the region
836 | * but still influence an image type because of thick lines being drawn.
837 | * If thicker lines should influence the result, the region should be padded accordingly.
838 | *
839 | * @param region
840 | *
841 | * @return a list of objects with ROIs that intersect the specified region
842 | */
843 | public List getObjectsForRegion(ImageRegion region) {
844 | return hierarchy.getObjectsForRegion(null, region, null).stream()
845 | .filter(params.objectFilter)
846 | .filter(p -> params.createInstanceLabels || params.labels.containsKey(p.getPathClass()) || params.boundaryLabels.containsKey(p.getPathClass()))
847 | .collect(Collectors.toList());
848 | }
849 |
850 | @Override
851 | public void close() {}
852 |
853 | @Override
854 | public String getServerType() {
855 | return "Labelled image";
856 | }
857 |
858 | @Override
859 | public ImageServerMetadata getOriginalMetadata() {
860 | return originalMetadata;
861 | }
862 |
863 | /**
864 | * Throws an exception - metadata should not be set for a hierarchy image server directly. Any changes should be made to the underlying
865 | * image server for which this server represents an object hierarchy.
866 | */
867 | @Override
868 | public void setMetadata(ImageServerMetadata metadata) {
869 | throw new IllegalArgumentException("Metadata cannot be set for a labelled image server!");
870 | }
871 |
872 | @Override
873 | protected BufferedImage createDefaultRGBImage(int width, int height) {
874 | // GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
875 | // return gc.createCompatibleImage(width, height, Transparency.TRANSLUCENT);
876 | return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
877 | }
878 |
879 | @Override
880 | protected BufferedImage readTile(TileRequest tileRequest) throws IOException {
881 | long startTime = System.currentTimeMillis();
882 |
883 | var pathObjects = hierarchy.getObjectsForRegion(null, tileRequest.getRegionRequest(), null)
884 | .stream()
885 | .filter(params.objectFilter)
886 | .collect(Collectors.toList());
887 | BufferedImage img;
888 | if (multichannelOutput) {
889 | img = createMultichannelTile(tileRequest, pathObjects);
890 |
891 | } else {
892 | img = createIndexedColorTile(tileRequest, pathObjects);
893 | }
894 |
895 | long endTime = System.currentTimeMillis();
896 | logger.trace("Labelled tile rendered in {} ms", endTime - startTime);
897 | return img;
898 | }
899 |
900 |
901 | private BufferedImage createMultichannelTile(TileRequest tileRequest, Collection pathObjects) {
902 |
903 | int nChannels = nChannels();
904 | if (nChannels == 1)
905 | return createBinaryTile(tileRequest, pathObjects, 0);
906 |
907 | int tileWidth = tileRequest.getTileWidth();
908 | int tileHeight = tileRequest.getTileHeight();
909 | byte[][] dataArray = new byte[nChannels][];
910 | for (int i = 0; i < nChannels; i++) {
911 | var tile = createBinaryTile(tileRequest, pathObjects, i);
912 | dataArray[i] = ((DataBufferByte)tile.getRaster().getDataBuffer()).getData();
913 | }
914 | DataBuffer buffer = new DataBufferByte(dataArray, tileWidth * tileHeight);
915 |
916 | int[] offsets = new int[nChannels];
917 | for (int b = 0; b < nChannels; b++)
918 | offsets[b] = b * tileWidth * tileHeight;
919 |
920 | var sampleModel = new BandedSampleModel(buffer.getDataType(), tileWidth, tileHeight, nChannels);
921 | // var sampleModel = new ComponentSampleModel(buffer.getDataType(), tileWidth, tileHeight, 1, tileWidth, offsets);
922 |
923 | var raster = WritableRaster.createWritableRaster(sampleModel, buffer, null);
924 |
925 | return new BufferedImage(colorModel, raster, false, null);
926 | }
927 |
928 | private BufferedImage createBinaryTile(TileRequest tileRequest, Collection pathObjects, int label) {
929 | int width = tileRequest.getTileWidth();
930 | int height = tileRequest.getTileHeight();
931 | BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
932 | WritableRaster raster = img.getRaster();
933 | Graphics2D g2d = img.createGraphics();
934 |
935 | if (!pathObjects.isEmpty()) {
936 |
937 | RegionRequest request = tileRequest.getRegionRequest();
938 | double downsampleFactor = request.getDownsample();
939 |
940 | g2d.setClip(0, 0, width, height);
941 | double scale = 1.0/downsampleFactor;
942 | g2d.scale(scale, scale);
943 | g2d.translate(-request.getX(), -request.getY());
944 | g2d.setColor(Color.WHITE);
945 |
946 | BasicStroke stroke = new BasicStroke((float)(params.lineThickness * tileRequest.getDownsample()));
947 | g2d.setStroke(stroke);
948 |
949 | // We want to order consistently to avoid confusing overlaps
950 | for (var entry : params.labels.entrySet()) {
951 | if (entry.getValue() != label)
952 | continue;
953 | var pathClass = getPathClass(entry.getKey());
954 | for (var pathObject : pathObjects) {
955 | if (getPathClass(pathObject) == pathClass) {
956 | var roi = params.roiFunction.apply(pathObject);
957 | if (roi.isArea())
958 | g2d.fill(roi.getShape());
959 | else if (roi.isLine())
960 | g2d.draw(roi.getShape());
961 | else if (roi.isPoint()) {
962 | for (var p : roi.getAllPoints()) {
963 | int x = (int)((p.getX() - request.getX()) / downsampleFactor);
964 | int y = (int)((p.getY() - request.getY()) / downsampleFactor);
965 | if (x >= 0 && x < width && y >= 0 && y < height) {
966 | raster.setSample(x, y, 0, 255);
967 | }
968 | }
969 | }
970 | }
971 | }
972 | }
973 | for (var entry : params.boundaryLabels.entrySet()) {
974 | if (entry.getValue() != label)
975 | continue;
976 | for (var pathObject : pathObjects) {
977 | var pathClass = getPathClass(pathObject);
978 | if (params.labels.containsKey(pathClass)) { // && !PathClassTools.isIgnoredClass(pathObject.getPathClass())) {
979 | var roi = params.roiFunction.apply(pathObject);
980 | if (roi.isArea()) {
981 | var shape = roi.getShape();
982 | g2d.draw(shape);
983 | }
984 | }
985 | }
986 | }
987 | }
988 |
989 | g2d.dispose();
990 | return img;
991 | }
992 |
993 |
994 | private static Color getColorForLabel(int label, boolean doRGB) {
995 | if (doRGB)
996 | return new Color(label, false);
997 | return ColorToolsAwt.getCachedColor(label, label, label);
998 | }
999 |
1000 |
1001 | private BufferedImage createIndexedColorTile(TileRequest tileRequest, Collection pathObjects) {
1002 |
1003 | RegionRequest request = tileRequest.getRegionRequest();
1004 |
1005 | double downsampleFactor = request.getDownsample();
1006 |
1007 | // Fill in the background color
1008 | int width = tileRequest.getTileWidth();
1009 | int height = tileRequest.getTileHeight();
1010 | boolean doRGB = maxLabel > 255;
1011 | // If we have > 255 labels, we can only use Graphics2D if we pretend to have an RGB image
1012 | BufferedImage img = doRGB ? new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) : new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
1013 | WritableRaster raster = img.getRaster();
1014 |
1015 | Graphics2D g2d = img.createGraphics();
1016 | int bgLabel = params.labels.get(params.unannotatedClass);
1017 | Color color = getColorForLabel(bgLabel, doRGB);
1018 | g2d.setColor(color);
1019 | g2d.fillRect(0, 0, width, height);
1020 |
1021 | if (!pathObjects.isEmpty()) {
1022 | g2d.setClip(0, 0, width, height);
1023 | double scale = 1.0/downsampleFactor;
1024 | g2d.scale(scale, scale);
1025 | g2d.translate(-request.getX(), -request.getY());
1026 |
1027 | BasicStroke stroke = new BasicStroke((float)(params.lineThickness * tileRequest.getDownsample()));
1028 | g2d.setStroke(stroke);
1029 |
1030 | // We want to order consistently to avoid confusing overlaps
1031 | for (var entry : params.labels.entrySet()) {
1032 | var pathClass = getPathClass(entry.getKey());
1033 | int c = entry.getValue();
1034 | color = getColorForLabel(c, doRGB);
1035 | List toDraw;
1036 | if (instanceClassMapInverse != null) {
1037 | var temp = instanceClassMapInverse.get(c);
1038 | if (temp == null)
1039 | continue;
1040 | toDraw = Collections.singletonList(temp);
1041 | } else
1042 | toDraw = pathObjects
1043 | .stream()
1044 | .filter(p -> getPathClass(p) == pathClass)
1045 | .collect(Collectors.toList());
1046 |
1047 | for (var pathObject : toDraw) {
1048 | var roi = params.roiFunction.apply(pathObject);
1049 | g2d.setColor(color);
1050 | if (roi.isArea())
1051 | g2d.fill(roi.getShape());
1052 | else if (roi.isLine())
1053 | g2d.draw(roi.getShape());
1054 | else if (roi.isPoint()) {
1055 | for (var p : roi.getAllPoints()) {
1056 | int x = (int)((p.getX() - request.getX()) / downsampleFactor);
1057 | int y = (int)((p.getY() - request.getY()) / downsampleFactor);
1058 | if (x >= 0 && x < width && y >= 0 && y < height) {
1059 | if (doRGB)
1060 | img.setRGB(x, y, color.getRGB());
1061 | else
1062 | raster.setSample(x, y, 0, c);
1063 | }
1064 | }
1065 | }
1066 | }
1067 | }
1068 | for (var entry : params.boundaryLabels.entrySet()) {
1069 | int c = entry.getValue();
1070 | color = getColorForLabel(c, doRGB);
1071 | for (var pathObject : pathObjects) {
1072 | // if (pathObject.getPathClass() == pathClass) {
1073 | var pathClass = getPathClass(pathObject);
1074 | if (params.labels.containsKey(pathClass)) {// && !PathClassTools.isIgnoredClass(pathObject.getPathClass())) {
1075 | var roi = params.roiFunction.apply(pathObject);
1076 | if (roi.isArea()) {
1077 | g2d.setColor(color);
1078 | g2d.draw(roi.getShape());
1079 | }
1080 | }
1081 | }
1082 | }
1083 | }
1084 | g2d.dispose();
1085 | if (doRGB) {
1086 | // Resort to RGB if we have to
1087 | WritableRaster shortRaster = null;
1088 | int w = img.getWidth();
1089 | int h = img.getHeight();
1090 | switch (getPixelType()) {
1091 | case UINT8:
1092 | return img;
1093 | case FLOAT32:
1094 | shortRaster = WritableRaster.createWritableRaster(
1095 | new BandedSampleModel(DataBuffer.TYPE_FLOAT, w, h, 1),
1096 | null);
1097 | break;
1098 | case FLOAT64:
1099 | shortRaster = WritableRaster.createWritableRaster(
1100 | new BandedSampleModel(DataBuffer.TYPE_DOUBLE, w, h, 1),
1101 | null);
1102 | break;
1103 | case INT16:
1104 | shortRaster = WritableRaster.createWritableRaster(
1105 | new BandedSampleModel(DataBuffer.TYPE_SHORT, w, h, 1),
1106 | null);
1107 | break;
1108 | case INT8:
1109 | case UINT16:
1110 | shortRaster = WritableRaster.createWritableRaster(
1111 | new BandedSampleModel(DataBuffer.TYPE_USHORT, w, h, 1),
1112 | null);
1113 | break;
1114 | case INT32:
1115 | case UINT32:
1116 | shortRaster = WritableRaster.createWritableRaster(
1117 | new BandedSampleModel(DataBuffer.TYPE_INT, w, h, 1),
1118 | null);
1119 | break;
1120 | default:
1121 | break;
1122 | }
1123 | if (maxLabel >= 65536 || shortRaster == null) {
1124 | return img;
1125 | }
1126 | // Transfer RGB values as labels to the new raster
1127 | int[] samples = img.getRGB(0, 0, width, height, null, 0, width);
1128 | shortRaster.setSamples(0, 0, width, height, 0, samples);
1129 | // System.err.println("Before: " + Arrays.stream(samples).summaryStatistics());
1130 | raster = shortRaster;
1131 | // samples = raster.getSamples(0, 0, width, height, 0, (int[])null);
1132 | // System.err.println("After: " + Arrays.stream(samples).summaryStatistics());
1133 | }
1134 | return new BufferedImage(colorModel, raster, false, null);
1135 | // return new BufferedImage((IndexColorModel)colorModel, raster, false, null);
1136 | }
1137 |
1138 |
1139 | }
--------------------------------------------------------------------------------
/src/main/resources/META-INF/services/qupath.lib.gui.extensions.QuPathExtension:
--------------------------------------------------------------------------------
1 | org.elephant.cellsparse.CellsparseCellposeExtension
2 | org.elephant.cellsparse.CellsparseElephantExtension
3 | org.elephant.cellsparse.CellsparseStarDistExtension
--------------------------------------------------------------------------------