-
Notifications
You must be signed in to change notification settings - Fork 16
/
exportTiles.groovy
134 lines (114 loc) · 5.58 KB
/
exportTiles.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/**
* Script to export annotations as labeled tiles for QuPath > 0.2*.
*
* All patches will be exported to the same directory called 'tiles' inside the Project directory
* The patches will be filtered based on tissue content, and finally moved to respective the
* subdirectories: Images and Labels within the 'tiles' folder
*
* Each patch's filename contains the original WSI ID, and images are saved as PNG (by default)
* and ground truth as TIF
*
* The downsampling level can be set by the user, default value is 4.
*
* Code is inspired by the script from the QuPath documentations, written by Pete Bankhead:
* https://qupath.readthedocs.io/en/stable/docs/advanced/exporting_images.html#tile-exporter
*
* @author André Pedersen
*/
import qupath.lib.images.servers.LabeledImageServer
import java.awt.image.Raster
import javax.imageio.ImageIO;
// ----- SET THESE PARAMETERS -----
def classNames = ["Benign", "Malign"] // names of classes of interest (simply add more values to list to add more classes)
double downsample = 4 // which downsampling level to use -> how much to downsample the patches
double glassThreshold = 50 // which threshold to use for tissue detection (lower value => more tissue included in mask)
double percentageThreshold = 0.25 // if a patch contains less than X amount of tissue, it should be neglected (range [0.0, 1.0])
int patchSize = 512 // generated patch size
int pixelOverlap = 128 // stride for which patches are generated
def imageExtension = ".tif"
int nb_channels = 3;
def multiChannel = false;
// --------------------------------
def imageData = getCurrentImageData()
// Define output path (relative to project)
def name = GeneralTools.getNameWithoutExtension(imageData.getServer().getMetadata().getName())
def pathOutput = buildFilePath(PROJECT_BASE_DIR, 'tiles')
mkdirs(pathOutput)
// Create an ImageServer where the pixels are derived from annotations
def tempServer = new LabeledImageServer.Builder(imageData)
.backgroundLabel(0, ColorTools.WHITE) // Specify background label (usually 0 or 255)
.downsample(downsample) // Choose server resolution; this should match the resolution at which tiles are exported
.multichannelOutput(multiChannel) // If true, each label is a different channel (required for multiclass probability)
// assign each class to the server (need to iterate across list array due to multi-class)
def counter = 1
classNames.each { currClassName ->
tempServer.addLabel(currClassName, counter) // Choose output labels (the order matters!)
counter++;
}
// finally, build server
def labelServer = tempServer.build()
// Create an exporter that requests corresponding tiles from the original & labeled image servers
new TileExporter(imageData)
.downsample(downsample) // Define export resolution
.imageExtension(imageExtension) // Define file extension for original pixels (often .tif, .jpg, '.png' or '.ome.tif')
.tileSize(patchSize) // Define size of each tile, in pixels
.labeledServer(labelServer) // Define the labeled image server to use (i.e. the one we just built)
.annotatedTilesOnly(true) // If true, only export tiles if there is a (labeled) annotation present
.overlap(pixelOverlap) // Define overlap, in pixel units at the export resolution
.writeTiles(pathOutput) // Write tiles to the specified directory
// create new folder (IMAGES AND LABELS), but only if they do not exist!
def dir1 = new File(pathOutput + "/Images");
if (!dir1.isDirectory())
dir1.mkdir()
def dir2 = new File(pathOutput + "/Labels");
if (!dir2.isDirectory())
dir2.mkdir()
// attempt to delete unwanted patches, both some formats as well as patches containing mostly glass
// Iterate through all your tiles
File folder = new File(pathOutput)
File[] listOfFiles = folder.listFiles()
// for each patch
listOfFiles.each { tile ->
// skip directories within masks folder, and skip all ground truth patches
if (tile.isDirectory())
return;
def currPath = tile.getPath()
if (!currPath.endsWith(imageExtension))
return;
// load TIFF images back again, estimate patch glass density, and remove patches with lots
// of glass based on user-defined threshold
def image = ImageIO.read(new File(currPath))
Raster raster = image.getRaster();
// estimate amount of tissue in patch
def tissue = 0;
for (int y = 0; y < image.getHeight(); ++y) {
for (int x = 0; x < image.getWidth(); ++x) {
double currDist = 0
for (int z = 0; z < nb_channels; ++z) {
currDist += raster.getSample(x, y, z)
}
currDist = ((currDist / 3) > (255 - glassThreshold)) ? 0 : 1;
if (currDist > 0) {
++tissue
}
}
}
// remove patches containing less tissue, dependent on user-defined threshold, and move accepted patches to respective folders
def amountTissue = tissue / (image.getWidth() * image.getHeight());
def currLabelPatch = new File(pathOutput + "/" + tile.getName().split(imageExtension)[0] + ".png")
if (amountTissue < percentageThreshold) {
tile.delete()
currLabelPatch.delete()
} else {
tile.renameTo(pathOutput + "/Images/" + tile.getName())
currLabelPatch.renameTo(new File(pathOutput + "/Labels/" + tile.getName().split(imageExtension)[0] + ".png"))
}
}
print "Done!"
// reclaim memory - relevant for running this within a RunForProject
Thread.sleep(100);
javafx.application.Platform.runLater {
getCurrentViewer().getImageRegionStore().cache.clear();
System.gc();
}
Thread.sleep(100);