Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it possible to import python-opencv contours? #81

Closed
ElEd2 opened this issue Jun 12, 2017 · 9 comments
Closed

Is it possible to import python-opencv contours? #81

ElEd2 opened this issue Jun 12, 2017 · 9 comments

Comments

@ElEd2
Copy link

ElEd2 commented Jun 12, 2017

Hi,

I'm interested in using QuPath to explore the results of an image segmentation algorithm (for large .svs files) I've written in python+opencv. Would this work? What would be the best way to load the contours?

Cheers,
Ed

@petebankhead
Copy link
Member

This is certainly possible, although will involve writing some Python and/or Groovy code.

The basic process is:

  1. Get your contours (somehow) into arrays of x and y coordinates accessible to QuPath

  2. Create PolygonROI objects from each pair of coordinate arrays. If polygons are not sufficient, and you rather need complex shapes with holes, it is also possible - but considerably more awkward.

  3. Create some kind of PathObject for each PolygonROI; probably a PathDetectionObject (if there will be a lot of them) or PathAnnotationObject (if there won't). There is some more information here.

  4. Add each PathObject to the object hierarchy in QuPath so that it can be displayed.

There is some information relevant to the last 3 steps at #61

For the first step, there are a few different options:

  • If you are much more comfortable with Python rather than Groovy/Java, then you could try one of the methods of using Python with QuPath described in the Wiki. Conceivably, you might even be able to run your whole code that way… or else just parse the results exported in a Python-friendly format.

  • You could try using OpenCV via its Java bindings via Groovy via QuPath. If you set things up as described here then the dependencies should be accessible.

  • You could write a simple Python script to export the coordinates for each contour, and then write a simple Groovy script to parse this and bring the coordinates into QuPath.

Of these, I would choose the last option. There may be some merit in the others, but I expect they would be more complicated to set up.

There are lots of tricks and shortcuts in Groovy that may help with the parsing, e.g. in order to extract floating point coordinates (such as those required to construct the PolygonROI) from a String you might use this:

String inputString = "1.0, 2.0, 3.0, 4.0, 50.0"
float[] x = inputString.tokenize(',') as float[]
print x

Finally, I should mention that the coordinates should be in pixel units corresponding to the highest-resolution plane in your SVS file.

@ElEd2
Copy link
Author

ElEd2 commented Jun 13, 2017

Excellent, I'll have a go at this. Thanks for the quick answer!
Cheers,
Ed

@Svidro
Copy link

Svidro commented Jun 13, 2017

If you get this working, I would love to see how!

@aditsanghvi94
Copy link

Does the script have to be run from the QuPath GUI? I'm trying something similar, do let me know if you got this working @ElEd2

@ElEd2
Copy link
Author

ElEd2 commented Aug 4, 2017

Unfortunately I've had to park my image segmentation project for now, so might be a couple of months before I get back to this.

@ElEd2
Copy link
Author

ElEd2 commented Nov 13, 2017

Hi,

Sorry, took me a while to get back to this.

I've written some code to write python opencv contours to file

def write_cnt_text_file(cnt_list, file_name):
    with open(file_name, 'w') as file:
        for cnt_i in cnt_list:
            file.write(','.join(['%f' % num for num in cnt_i[:,0,0]])+"\n")
            file.write(','.join(['%f' % num for num in cnt_i[:,0,1]])+"\n")

and groovy code to read them in:

import qupath.lib.objects.*
import qupath.lib.roi.*

// Some code taken from here https://groups.google.com/forum/#!topic/qupath-users/j_Wd1hy4eKM

// This runs for img_mPAS_devel_PurpleClone
def file = new File('pythonOpenCV/test_cnt.txt')
def lines = file.readLines()

num_rois = lines.size/2
for (i = 0; i <num_rois; i++) {
    float[] x1 = lines[2*i].tokenize(',') as float[]
    float[] y1 = lines[2*i+1].tokenize(',') as float[]    
    // Create object
    def roi = new PolygonROI(x1, y1, -1, 0, 0)
    def pathObject = new PathDetectionObject(roi)
    //def pathObject = new PathAnnotationObject(roi)
    // Add object to hierarchy
    addObject(pathObject)
}
print("Done!")

Cheers,
Ed

@nathanin
Copy link

@ElEd2 Thank you for the scripts! I'll be using them once I figure out how to use the groovy code. Quick question: In my case, I have contours for the entire slide area. This means millions of vertices if I output everything naively. Do you experience any performance issues with loading or zooming in your case?

@petebankhead
Copy link
Member

I can't speak for @ElEd2 (thanks from me too for the scripts!), but performance should be ok.

One important thing is that you should definitely use PathDetectionObject and not PathAnnotationObject, just like in the code above (there's a comparison of the different object types here).

With this many objects involved, you also probably don't want to add your objects to the hierarchy one-by-one within the loop, since this will trigger a lot of costly checks and events. Calling addObjects and passing a list should do much better. So the loop above could become

def pathObjects = []
for (i = 0; i <num_rois; i++) {
   // The rest of the stuff, as above
  pathObjects << new PathDetectionObject(roi)
}
addObjects(pathObjects)

If this still doesn't perform well enough, and you don't mind deleting anything that might already exist on the hierarchy, using the following instead of addObjects() should perform better still:

clearAllObjects()
getCurrentHierarchy().getRootObject().addPathObjects(pathObjects)
fireHierarchyUpdate()

Anyhow, the reason I think that it should work one way or another is that you can generate similar numbers of vertices running the cell detection in QuPath itself. In that case, various tricks are used to help, e.g.

  • Contours are smoothed after detection, and then simplified to reduce the numbers of vertices that need to be drawn
  • Image tiles representing the objects are drawn on demand and cached - similar to having a pyramidal image, but one where the tiles are quickly created only when needed
  • When viewing the image at a sufficiently low resolution, QuPath will check if a detection is well represented by a single pixel or rectangle and just draw that instead (to avoid the effort of handling all the vertices)

You could do the polygon simplification on the OpenCV side, perhaps with approxPolyDP, or else on the QuPath side after already generating the polygon, using ShapeSimplifier.simplifyPolygon(PolygonROI polygon, final double altitudeThreshold).

Despite all that, I haven't tried doing this exact conversion before and my guess is that you might have a problem with having really really huge text files. If that's the case then it could be the bottleneck... but that can be solved too if necessary.

@petebankhead
Copy link
Member

I've now written a bit about OpenCV contours & QuPath in a blog post.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants