Import coordinates from .csv file

Hi,

is there a way to import coordinates as tiles from a .csv file?

The respective .csv files have this structure:

y,“x”,“cluster_id”
47,479,301
49,487,301
49,488,301
49,489,301
49,490,301

I would like to import every y and x as a tile with all tiles for example belonging to the same cluster also named the same/the same color for all respective tiles etc.

This is the code I already found:


//https://forum.image.sc/t/reading-csv-table-in-qupath/32772/3
//From @melvingelbard
import java.io.BufferedReader;
import java.io.FileReader;
import qupath.lib.objects.PathAnnotationObject;
import qupath.lib.roi.RectangleROI;

def imageData = getCurrentImageData();

// Get location of csv
def file = Dialogs.promptForDirectory(null)


// Create BufferedReader
def csvReader = new BufferedReader(new FileReader('filename'));


int sizePixels = 1000
row = csvReader.readLine() // first row (header)

// Loop through all the rows of the CSV file.
while ((row = csvReader.readLine()) != null) {
    def rowContent = row.split(",")
    double cx = rowContent[1] as double;
    double cy = rowContent[2] as double;

    // Create annotation
    def roi = new RectangleROI(cx-sizePixels/2, cy-sizePixels/2, sizePixels, sizePixels);
    def annotation = new PathAnnotationObject(roi, PathClassFactory.getPathClass("Region"));
    imageData.getHierarchy().addPathObject(annotation, true);
}

I used the import csv script from the coding helper scripts.

I modified there:

// Get location of csv
def file = getQuPath().getDialogHelper().promptForFile(null)

->

 // Get location of csv
def file = Dialogs.promptForDirectory(null)

With this script, I could create annotations, but all the tiles appeared in the left upper corner. So it’s not working yet.

Any ideas/suggestions?

Best,
Sophia

Hi Sophia,

The script that you provided creates PathAnnotationObjects rather than tiles (although they basically look like tiles). I suppose the difference is not important in this case.
It seems like all the tiles are in the upper left corner because the sample coordinates:

y,“x”,“cluster_id”
47,479,301
49,487,301
49,488,301
49,489,301
49,490,301

Are very close to each other. Between y = 47 and y = 49, you probably won’t be able to see much difference when drawn, and the rest are basically at the exact same y position. Also, the script removes the pixelSize from the original coordinates and divides it by 2. I can’t remember why it does this but it is most probably specific to a certain project.
So I believe the script works well for its original purpose, but that your coordinates are wrong (assuming the tiles are not meant to overlap). In your text file, the x column should indicate the upper left x coordinate of a tile. the y column should indicate the upper left y coordinate of a tile. Finally, the int sizePixels = 1000 line in the provided script indicates the width and height of all tiles.

Also, the script assumes that the second column is x, and third is y. So you should probably change this in the script to reflect how your csv shows the coordinates (see sample script at the end).

Finally, you can change the PathClassFactory.getPathClass("Region") line in the script to make it belong to a specific class (which I suppose in this case would be the cluster?)

Sample script:

import java.io.BufferedReader;
import java.io.FileReader;
import qupath.lib.objects.PathAnnotationObject;
import qupath.lib.roi.RectangleROI;

def imageData = getCurrentImageData();

// Create BufferedReader
def csvReader = new BufferedReader(new FileReader('C:/Users/Mel/Desktop/qupath_forum.txt'));

int sizePixels = 1000
row = csvReader.readLine() // first row (header)

// Loop through all the rows of the CSV file.
while ((row = csvReader.readLine()) != null) {
    def rowContent = row.split(",")
    double x = rowContent[1] as double;
    double y = rowContent[0] as double;
    
    print("x: " + x)
    print("y: " + y)
    print("________")

    // Create annotation
    def roi = new RectangleROI(x, y, sizePixels, sizePixels);
    def annotation = new PathAnnotationObject(roi, PathClassFactory.getPathClass("Cluster " + rowContent[2]));
    imageData.getHierarchy().addPathObject(annotation, true);
}
1 Like

Hi, I have a similar question again:

So let’s assume I have exported tiles from a whole slide image as tif images and used them in a deep learning model and predicted now that some tiles are malignant for example.

I saved the X and Y centroid position of every tile in the wsi in the respective tilename when I exported it as an image.
E.g. “TumorTile 85 x 125272.0 y 16183.0 .tif”.

Now with a little modified version of your recommended script, I can also import those coordinates from the tilenames saved in a csv file again into the whole slide image. (Assuming here for example that original tiles are 256 pixels each).

My only problem this way: X centroid and Y centroid get rounded in each tilename when I save those tiles as images, so they don’t overlap for 100% with the tiles from before, if I use a csv file where I insert the X and Y coordinates from the tilenames.

image

Is there a way I can use

    x = annotation.getROI().getCentroidX()
    y = annotation.getROI().getCentroidY()

without getting rounded in each tilename to not have this problem?

I mean maybe using something with the tile number would be another option too.
Nonetheless, using x and y would maybe be a little cleaner: if I should not save the tiling in the wsi before or in case I would still make a change in the annotation, then the tile number may be changed as well and I couldnt identify the respective position without x and y position again.

Modified script:

import java.io.BufferedReader;
import java.io.FileReader;
import qupath.lib.objects.PathAnnotationObject;
import qupath.lib.roi.RectangleROI;

import qupath.lib.regions.RegionRequest
import qupath.lib.gui.scripting.QPEx

server = getCurrentImageData().getServer()
pixelfactor = server.getPixelCalibration().getPixelHeightMicrons()
tile_px = 256
tile_mic = tile_px * pixelfactor 

def imageData = getCurrentImageData();

// Create BufferedReader
def csvReader = new BufferedReader(new FileReader('filepath.csv'));

int sizePixels = 256
row = csvReader.readLine() // first row (header)

// Loop through all the rows of the CSV file.
while ((row = csvReader.readLine()) != null) {
    def rowContent = row.split(",")
    double x = rowContent[1] as double;
    double y = rowContent[0] as double;
    
    print("x: " + x)
    print("y: " + y)
    print("________")

    // Create annotation
    def roi = new RectangleROI((x*1/pixelfactor-sizePixels/2), (y*1/pixelfactor-sizePixels/2), sizePixels, sizePixels);
    def annotation = new PathAnnotationObject(roi, PathClassFactory.getPathClass("Cluster " + rowContent[2]));
    imageData.getHierarchy().addPathObject(annotation, true);
}

I realize this is not what you asked, but if you are importing a classified tile, do you truly need to put the tile back in the image? Or could you access the tile and change it’s class based on the classifier result? That might be easier if you are not importing a mask, but only adjusting the class of the resulting tile.

In that case, you might be able to access “Tile 3447” instead, or use approximate XY coordinates to find the appropriate tile.

1 Like

Thanks for the reply!
That also might have been an option.

But I just prefer to run my scripts for the whole project without save. So my annotations get not changed with tiles.

I just put some own and other already existing code from the forum together and got everything working now.

So after tiling and patch extraction, I also write a csv with the exact position of every tile (decimal places are included).

Afterwards I can just import tiles from that csv file again.

Edit: The only thing that annoyed me a little, was that I didn’t fix it to change the csv file to the respective name of the image. So I instead created an additional folder for every image. I would have preferred to have the csv the same name as the image directly.

Both scripts:

  1. extracting exact positions to csv (after annotations were tiled):
path = buildFilePath(PROJECT_BASE_DIR, 'centroid result/' + filename)
mkdirs(path)

path = buildFilePath(path, 'centroids.csv')

def file = new File(path) 

server = getCurrentImageData().getServer()
pixelfactor = server.getPixelCalibration().getPixelHeightMicrons()

filename = server.getMetadata().getName()

file.text = 'x\ty\ttile_number\ttile type'

// Write centroids with tab separator
    
for (p in getAnnotationObjects())
    file << System.lineSeparator() << p.getROI().getCentroidX()*pixelfactor << "\t" << p.getROI().getCentroidY()*pixelfactor << "\t" << p.getName() << "\t" << p.getParent().getPathClass()
print 'Done!'
  1. import tiles:
import java.io.BufferedReader;
import java.io.FileReader;
import qupath.lib.objects.PathAnnotationObject;
import qupath.lib.roi.RectangleROI;

import qupath.lib.regions.RegionRequest
import qupath.lib.gui.scripting.QPEx

server = getCurrentImageData().getServer()
pixelfactor = server.getPixelCalibration().getPixelHeightMicrons()
tile_px = 256
tile_mic = tile_px * pixelfactor 

def imageData = getCurrentImageData();

// Create BufferedReader
def csvReader = new BufferedReader(new FileReader('/path/centroids.csv'));

int sizePixels = 256
row = csvReader.readLine() // first row (header)

// Loop through all the rows of the CSV file.
while ((row = csvReader.readLine()) != null) {
    def rowContent = row.split("\t")
    double x = rowContent[0] as double;
    double y = rowContent[1] as double;
    
    print("x: " + x)
    print("y: " + y)
    print("________")

    // Create annotation
    def roi = new RectangleROI((x*1/pixelfactor-sizePixels/2), (y*1/pixelfactor-sizePixels/2), sizePixels, sizePixels);
    def annotation = new PathAnnotationObject(roi, PathClassFactory.getPathClass("Type " + rowContent[3]));
    imageData.getHierarchy().addPathObject(annotation, true);
}

You can get the image name like this:


and insert that into the file name.
Pete does that in the old data export scripts here:

Edit: oh, you were already getting the name from the metadata as well. Not sure why you couldn’t add that into the file name string?

1 Like

Sorry I have one more question:

I would like to check for the tiletype before it gets written to the csv. For example the origin annotation also gets written into the csv as a tile (the centroid of it) which I would not like to have.
If I don’t change anything, I get for example this in the csv (the row in the middle is unwanted):

16202.3659 6730.1591 Tile 16 Tumor
15037.9243 8735.5863 Tile 624 Tumor
15593.4527689504 8038.75689357207 null Image
15620.1451 8282.7479 Tile 422 Tumor

What I would like to have is:

for (p in getAnnotationObjects())
    if (p.getParent().getPathClass().equals("Tumor"))
        file << System.lineSeparator() << p.getROI().getCentroidX()*pixelfactor << "\t" << p.getROI().getCentroidY()*pixelfactor << "\t" << p.getName() << "\t" << p.getParent().getPathClass()
print 'Done!'

But if I try that, I get this:

ERROR: I cannot find 'p'!

ERROR: MissingPropertyException at line 118: No such property: p for class: Script52

ERROR: org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:65)

ok got it!

Changed the insert tiles script so that it checks the column of the csv where I have inserted “Tumor” for every tumor tile.

    def roi = new RectangleROI((x*1/pixelfactor-sizePixels/2), (y*1/pixelfactor-sizePixels/2), sizePixels, sizePixels);
    def annotation = new PathAnnotationObject(roi, PathClassFactory.getPathClass("Type " + rowContent[3]));
    if (rowContent[3].equals("Tumor"))
        imageData.getHierarchy().addPathObject(annotation, true);
1 Like

In case it helps in the future I generally filter at the beginning of the loop. In your case, some of the errors probably came from the fact that some annotations will not have Parent objects, and therefor no parent object class, which will cause problems (the top layer annotation, for instance).

Might instead be

tumorParent = getAnnotationObjects().findAll{it.getParent()?.getPathClass() == getPathClass("Tumor")}

tumorParent.each{ p->
    print p.getName()
}

Or in a single terrible to read line!

getAnnotationObjects().findAll{it.getParent()?.getPathClass() == getPathClass("Tumor")}.each{ p->print p.getName()}

I suspect the bigger problem with your first code was the .equals though, since I think getPathClass returns a class, not a string.

getAnnotationObjects().each{
if(it.getPathClass().equals("Tumor")){ print it}
}
The code above returns nothing, for example, even when I have tumor annotations.