Skip to content

Commit 7848e9e

Browse files
authored
Merge pull request #22 from ksugar/dev
0.6.0 -> 0.7.0
2 parents 9d40bec + deff51b commit 7848e9e

15 files changed

Lines changed: 1425 additions & 20 deletions

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# QuPath extension SAM
22

3+
### New release v0.7: SAM2-based 2D+T tracking and 3D segmentation are supported now!
4+
<img src="https://github.com/ksugar/qupath-extension-sam/releases/download/assets/sam2-sequence-demo.gif" width="768">
5+
36
<img src="https://github.com/ksugar/samapi/releases/download/assets/qupath-samapi.gif" width="768">
47

58
<img src="https://github.com/ksugar/samapi/releases/download/assets/qupath-sam-multipoint-live.gif" width="768">
@@ -12,7 +15,7 @@ This is a part of the following paper. Please [cite](#citation) it when you use
1215

1316
## Install
1417

15-
Drag and drop [the extension file](https://github.com/ksugar/qupath-extension-sam/releases/download/v0.6.0/qupath-extension-sam-0.6.0.jar) to [QuPath](https://qupath.github.io) and restart it.
18+
Drag and drop [the extension file](https://github.com/ksugar/qupath-extension-sam/releases/download/v0.7.0/qupath-extension-sam-0.7.0.jar) to [QuPath](https://qupath.github.io) and restart it.
1619

1720
Since QuPath v0.5.0, you can install the extension from the extension manager dialog by specifying `Owner` and `Repository` as shown below.
1821

@@ -30,7 +33,7 @@ To update the `qupath-extension-sam`, follow the following instructions.
3033

3134
1. Open extensions directory from `Extensions` > `Installed extensions` > `Open extensions directory`
3235
![](https://github.com/ksugar/qupath-extension-sam/releases/download/assets/open-extensions-directory.png)
33-
2. Replace `qupath-extension-sam-x.y.z.jar` with [the latest version of the extension file](https://github.com/ksugar/qupath-extension-sam/releases/download/v0.6.0/qupath-extension-sam-0.6.0.jar). If you are using QuPath v0.4.x, you need to install [the extension file for QuPath v0.4.x](https://github.com/ksugar/qupath-extension-sam/releases/download/v0.4.1/qupath-extension-sam-0.4.1.jar), which is now deprecated.
36+
2. Replace `qupath-extension-sam-x.y.z.jar` with [the latest version of the extension file](https://github.com/ksugar/qupath-extension-sam/releases/download/v0.7.0/qupath-extension-sam-0.7.0.jar). If you are using QuPath v0.4.x, you need to install [the extension file for QuPath v0.4.x](https://github.com/ksugar/qupath-extension-sam/releases/download/v0.4.1/qupath-extension-sam-0.4.1.jar), which is now deprecated.
3437
3. Restart QuPath application.
3538

3639
Please note that you need to also update the [samapi](https://github.com/ksugar/samapi/tree/v0.5.0) server.
@@ -173,8 +176,11 @@ If you select a class in `Auto set` in the Annotations tab, it is used for a new
173176

174177
## Updates
175178

179+
### v0.7.0
180+
- Support 2D+T tracking and 3D segmentation with [SAM2](https://ai.meta.com/sam2/) models, available with the [samapi](https://github.com/ksugar/samapi) server `v0.6.0` and above.
181+
176182
### v0.6.0
177-
- Support [SAM2](https://ai.meta.com/sam2/) models. The SAM2 models are available from the [samapi](https://github.com/ksugar/samapi) server `v0.5.0` and above.
183+
- Support [SAM2](https://ai.meta.com/sam2/) models, available with the [samapi](https://github.com/ksugar/samapi) server `v0.5.0` and above.
178184
- Use the current view for the encoder input in the rectangle mode.
179185

180186
### v0.5.0

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ plugins {
1313
ext.moduleName = 'org.elephant.sam.qupath'
1414

1515
// TODO: Define the extension version & provide a short description
16-
version = "0.6.0"
16+
version = "0.7.0"
1717
description = 'QuPath extension for Segment Anything Model (SAM)'
1818

1919
// TODO: Specify the QuPath version, compatible with the extension.

src/main/java/org/elephant/sam/SAMExtension.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public GitHubRepo getRepository() {
6161

6262
@Override
6363
public Version getQuPathVersion() {
64-
return Version.parse("0.6.0");
64+
return Version.parse("0.7.0");
6565
}
6666

6767
}

src/main/java/org/elephant/sam/Utils.java

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
import com.google.gson.JsonObject;
66

77
import org.elephant.sam.entities.SAMOutput;
8+
import org.elephant.sam.http.MultipartBodyBuilder;
89
import org.locationtech.jts.geom.Coordinate;
910
import org.slf4j.Logger;
1011
import org.slf4j.LoggerFactory;
12+
13+
import qupath.lib.awt.common.AwtTools;
1114
import qupath.lib.common.GeneralTools;
1215
import qupath.lib.geom.Point2;
1316
import qupath.lib.gui.images.servers.RenderedImageServer;
@@ -18,6 +21,7 @@
1821
import qupath.lib.objects.PathObjectTools;
1922
import qupath.lib.objects.classes.PathClass;
2023
import qupath.lib.regions.ImagePlane;
24+
import qupath.lib.regions.ImageRegion;
2125
import qupath.lib.regions.RegionRequest;
2226
import qupath.lib.roi.RectangleROI;
2327
import qupath.lib.roi.interfaces.ROI;
@@ -92,10 +96,14 @@ private static PathObject parsePathObject(Gson gson, JsonElement element) {
9296
if (jsonObj.has("properties")) {
9397
// Set quality from properties if we can
9498
JsonObject properties = jsonObj.get("properties").getAsJsonObject();
95-
JsonElement quality = properties.has("quality") ? properties.get("quality") : null;
99+
JsonElement quality = properties.get("quality");
96100
if (quality != null && quality.isJsonPrimitive()) {
97101
pathObject.getMeasurementList().put(SAM_QUALITY_MEASUREMENT, quality.getAsDouble());
98102
}
103+
JsonElement objectId = properties.get("object_idx");
104+
if (objectId != null && objectId.isJsonPrimitive()) {
105+
pathObject.setPathClass(PathClass.getInstance(objectId.getAsString()));
106+
}
99107
}
100108
return pathObject;
101109
}
@@ -154,7 +162,11 @@ public static PathObject applyTransformAndClassification(PathObject pathObject,
154162
}
155163
if (plane != null && !Objects.equals(plane, pathObject.getROI().getImagePlane()))
156164
pathObject = PathObjectTools.updatePlane(pathObject, plane, true, false);
157-
pathObject.setPathClass(pathClass);
165+
if (pathClass == PathClass.NULL_CLASS) {
166+
pathObject.resetPathClass();
167+
} else if (pathClass != null) {
168+
pathObject.setPathClass(pathClass);
169+
}
158170
return pathObject;
159171
}
160172

@@ -214,6 +226,43 @@ public static ImageServer<BufferedImage> createRenderedServer(QuPathViewer viewe
214226
.build();
215227
}
216228

229+
/**
230+
* Get a region request for the viewer, intersected with the rendered server.
231+
*
232+
* @param viewer
233+
* @param renderedServer
234+
* @return the region request
235+
*/
236+
public static RegionRequest getViewerRegion(QuPathViewer viewer, ImageServer<BufferedImage> renderedServer) {
237+
return getViewerRegion(viewer, renderedServer, viewer.getZPosition(), viewer.getTPosition());
238+
}
239+
240+
/**
241+
* Get a region request for the viewer, intersected with the rendered server.
242+
*
243+
* @param viewer
244+
* @param renderedServer
245+
* @param z
246+
* @param t
247+
* @return the region request
248+
*/
249+
public static RegionRequest getViewerRegion(QuPathViewer viewer, ImageServer<BufferedImage> renderedServer, int z,
250+
int t) {
251+
if (renderedServer == null) {
252+
try {
253+
renderedServer = Utils.createRenderedServer(viewer);
254+
} catch (IOException e) {
255+
logger.error("Failed to create rendered server", e);
256+
}
257+
}
258+
// Find the region and downsample currently used within the viewer
259+
ImageRegion region = AwtTools.getImageRegion(viewer.getDisplayedRegionShape(), z, t);
260+
RegionRequest viewerRegion = RegionRequest.createInstance(renderedServer.getPath(),
261+
viewer.getDownsampleFactor(), region);
262+
viewerRegion = viewerRegion.intersect2D(0, 0, renderedServer.getWidth(), renderedServer.getHeight());
263+
return viewerRegion;
264+
}
265+
217266
/**
218267
* Encode a BufferedImage as a base64-encoded PNG.
219268
*
@@ -333,4 +382,30 @@ public static boolean isPotentialPromptObject(PathObject pathObject) {
333382
return pathObject.isAnnotation() && pathObject.hasROI() && !pathObject.getROI().isEmpty();
334383
}
335384

385+
/**
386+
* Converts a BufferedImage to a JPEG byte array.
387+
*
388+
* @param image
389+
* The BufferedImage to convert.
390+
* @return A byte array containing the JPEG data.
391+
* @throws IOException
392+
* If an error occurs during writing.
393+
*/
394+
public static byte[] bufferedImageToJpegBytes(BufferedImage image) throws IOException {
395+
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
396+
ImageIO.write(image, "jpg", baos);
397+
return baos.toByteArray();
398+
}
399+
}
400+
401+
public static byte[] createImageUploadMultipartBody(String boundary, String dirname, String filename,
402+
BufferedImage image)
403+
throws IOException {
404+
byte[] imageBytes = bufferedImageToJpegBytes(image);
405+
final MultipartBodyBuilder builder = new MultipartBodyBuilder(boundary);
406+
builder.addFormField("dirname", dirname);
407+
builder.addFilePart("file", filename, "image/jpeg", imageBytes);
408+
return builder.build();
409+
}
410+
336411
}

0 commit comments

Comments
 (0)