Jekyll2021-06-25T10:36:36+00:00https://petebankhead.github.io/feed.xmlPete’s blogA blog about QuPath, computational pathology, and bioimage analysis in general. But mostly about QuPath.Update on this blog2020-08-21T00:00:00+00:002020-08-21T00:00:00+00:00https://petebankhead.github.io/qupath/2020/08/21/blog-status<p>Now that QuPath v0.2 has been released, the blog has served its purpose as a record of personal musings along the way.</p>
<p>The blog will linger around a bit longer so that links still work, but I don’t plan to add new posts.</p>
<p>To be clear, <strong>QuPath is still being very actively developed!</strong> It’s just this blog that’s being retired.</p>
<p>Please use the forum at <a href="https://forum.image.sc/tag/qupath">https://forum.image.sc/tag/qupath</a> for any questions and discussions – and stay up to date with QuPath-related happenings through</p>
<ul>
<li>the homepage: <a href="http://qupath.github.io">https://qupath.github.io</a></li>
<li>the documentation: <a href="https://qupath.readthedocs.io">https://qupath.readthedocs.io</a></li>
<li>the YouTube channel: <a href="https://youtube.com/c/qupath">https://youtube.com/c/qupath</a></li>
<li>QuPath’s twitter: <a href="https://twitter.com/qupath">https://twitter.com/qupath</a></li>
<li>my twitter: <a href="https://twitter.com/petebankhead">https://twitter.com/petebankhead</a></li>
</ul>Now that QuPath v0.2 has been released, the blog has served its purpose as a record of personal musings along the way.QuPath v0.2.0-m8 available2019-12-10T00:00:00+00:002019-12-10T00:00:00+00:00https://petebankhead.github.io/qupath/2019/12/10/eighth-milestone<h4 id="qupath-v020-m8-is-now-online">QuPath v0.2.0-m8 is <a href="https://github.com/qupath/qupath/releases/tag/v0.2.0-m8">now online</a>!</h4>
<p>So is v0.2.0-m7 but I didn’t get around to writing about it.
Both emerged just before teaching workshops because I wanted to use the changes…</p>
<p>The updates are relatively minor (and therefore hopefully not too disruptive if you’re on v0.2.0-m6), but focus on</p>
<ol>
<li>fixing niggly annoyances</li>
<li>laying the foundation for later work.</li>
</ol>
<p>If the niggly annoyances were annoying you then I hope you’ll enjoy the benefits.</p>
<p>The changelogs are below:</p>
<h3 id="version-020-m8">Version 0.2.0-m8</h3>
<ul>
<li>Fixed repainting bug that could cause existing annotations to temporarily shift when drawing new annotations</li>
<li>Fixed <em>Zoom to fit</em> bug that meant command did not correctly resize and center the image in the viewer</li>
<li>Added <em>Match viewer resolutions</em> command to help synchronize when using multiple viewers</li>
<li>Improved tile export within a script</li>
<li>Improved interactive transformations
<ul>
<li>More options for <em>Interactive image alignment</em>, including support to specify affine transform manually</li>
<li>Log affine transform when applying ‘Rotate annotation’</li>
</ul>
</li>
</ul>
<h3 id="version-020-m7">Version 0.2.0-m7</h3>
<ul>
<li>Fixed bug that could cause QuPath to freeze when selecting objects with a mini-viewer active, see https://github.com/qupath/qupath/issues/377</li>
<li>Improved performance converting shapes to geometries, see https://github.com/qupath/qupath/issues/378</li>
<li>Improved robustness when drawing complex shapes, see https://github.com/qupath/qupath/issues/376</li>
<li>Improved stability when script directories cannot be found, see https://github.com/qupath/qupath/issues/373</li>
<li>Prompt to save each image when closing a project with multiple viewers active</li>
<li>Updated <em>Rotate annotation</em> command to use JTS</li>
</ul>
<p>For full details on what has changed, see the <a href="https://github.com/qupath/qupath/commits/master">commit log</a>.</p>QuPath v0.2.0-m8 is now online!Changing the hierarchy2019-11-17T00:00:00+00:002019-11-17T00:00:00+00:00https://petebankhead.github.io/qupath/2019/11/17/changing-the-hierarchy<p><strong>QuPath’s <a href="https://github.com/qupath/qupath/wiki/Object-hierarchies">object hierarchy</a> has been an important part of the software since the start.</strong></p>
<p>It’s where all the annotations, cells and other objects reside - arranged in a hierarchical way according to a few (ostensibly) simple rules.
It has served the software well… mostly.</p>
<p>In v0.2.0 the behavior is changing.
The purpose of this post is to summarize how, and expand on my recent tweeting about the subject.
If you use QuPath, it’s probably important to know this.</p>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Hmmm, so in a moment of wild abandon I deleted a couple of dozen lines of code to see if <a href="https://twitter.com/hashtag/QuPath?src=hash&ref_src=twsrc%5Etfw">#QuPath</a> would survive. Now suddenly images with large & complex annotations work *much* more smoothly... <a href="https://t.co/13rLlsywtc">pic.twitter.com/13rLlsywtc</a></p>— Pete Bankhead (@petebankhead) <a href="https://twitter.com/petebankhead/status/1194762514942353408?ref_src=twsrc%5Etfw">November 13, 2019</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<h2 id="goals-of-the-object-hierarchy">Goals of the object hierarchy</h2>
<p>When I first wrote the object hierarchy, I had two main intentions:</p>
<ol>
<li>To be able to create a hierarchical representation of tissue structures</li>
<li>To be able to dynamically calculate measurements quickly (e.g. number of cells in a region)</li>
</ol>
<p>Through QuPath you could see the relationships between objects under the ‘Hierarchy’ tab:</p>
<p><img src="https://petebankhead.github.io/assets/images/m6-hierarchy/hierarchy_screenshot.jpg" alt="QuPath hierarchy" class="center-image max-width-60 shadow-image" /></p>
<p>Conceptually, you might have something like this:</p>
<p><img src="https://petebankhead.github.io/assets/images/m6-hierarchy/hierarchy_dynamic_measurements.png" alt="Conceptual hierarchy" class="center-image max-width-60" /></p>
<p>Measurements were made quickly by recursively ‘looking down the hierarchy’ to see what was there.</p>
<p>The goal was that it should <em>feel</em> intuitive… so you could use the software and it would do as you expect, without requiring you to think about it all the time.</p>
<p>In practice, it needed <a href="https://github.com/qupath/qupath/wiki/Object-hierarchies#maintaining-the-hierarchy">some defined rules</a> that were sufficiently easy to understand and fast to apply.
These rules were based on the Region of Interest (ROI) and object type, and the two main ones were:</p>
<ul>
<li>A detection (e.g. a cell) was a <em>child</em> of an object if its <em>centroid was inside</em> that object’s ROI</li>
<li>An annotation was a <em>child</em> of an object if its <em>entire ROI</em> was inside that object’s ROI</li>
</ul>
<p>There were a few subtleties (e.g. detections couldn’t parent annotations)… but that was basically it.</p>
<h2 id="problems-with-the-object-hierarchy">Problems with the object hierarchy</h2>
<p>Those rules initially worked fine for my purposes: mostly tissue microarray analysis, sometimes analysis of larger tissue sections.</p>
<p>However as QuPath has become used more widely (> 60,000 downloads now…), more sophisticated commands were added (e.g. the pixel classifier), and the software was applied to more complicated applications, the limitations have become increasingly clear.</p>
<h3 id="1-performance">1. Performance</h3>
<p>Firstly, automatically resolving the hierarchy can make things <em>sloooow</em>.</p>
<p>Detections aren’t really the problem: figuring out if a centroid is inside a ROI is fast.
Doing this for a million cells may not be super-fast… but it is usually tolerable.</p>
<p>However figuring out if an annotation is fully inside another annotation can get <em>very</em> involved.
Particularly as annotations become larger, more complex, and more numerous.</p>
<p>And now with the pixel classifier, it’s a lot easier to generate thousands of complex annotations.</p>
<p><img src="https://petebankhead.github.io/assets/images/m6-hierarchy/many_annotations.jpg" alt="Overlapping annotations" class="center-image max-width-80 shadow-image" /></p>
<p>QuPath’s performance with many annotations is a reoccurring topic on the forum, e.g. <a href="https://forum.image.sc/t/qupath-ver-5-using-up-100-cpu-when-doing-simple-annotations-on-slides/31272">here</a> and <a href="https://forum.image.sc/t/qupath-0-2-0-extremely-slow-with-large-annotation-projects/24042">here</a>.</p>
<blockquote>
<p>To find the culprit when things are <em>sloooow</em>, <a href="https://visualvm.github.io/">VisualVM</a> is fantastically helpful.</p>
</blockquote>
<h3 id="2-ambiguity">2. Ambiguity</h3>
<p>If performance was the only problem, someone smarter than me might find a way to make it faster.</p>
<p>But that doesn’t solve a bigger issue: whenever objects overlap, the simple rules for resolving the hierarchy break down.
It’s no longer clear what relationships objects <em>should</em> have with one another, and the results can be surprising.</p>
<blockquote>
<p>In the image below, which ellipse is the ‘parent’ of the cells in the middle?</p>
</blockquote>
<p><img src="https://petebankhead.github.io/assets/images/m6-hierarchy/overlapping_annotations.jpg" alt="Overlapping annotations" class="center-image max-width-60 shadow-image" /></p>
<h3 id="3-confusion">3. Confusion</h3>
<p>In practice, the ambiguity wasn’t usually a problem.
Very often, when using QuPath the hierarchy is basically ignored.
Objects are requested like they all exist in a big list, and how they are represented internally doesn’t really matter.</p>
<p>But sometimes it could cause confusion.</p>
<p>For example, in the image below from v0.1.2, one would <em>expect</em> that the selected rectangle contains cells.
But because it is a ‘child’ of the ellipse, and the ellipse is not completely contained within the outer rectangle, it doesn’t - at least as far as the hierarchy is concerned.</p>
<p><img src="https://petebankhead.github.io/assets/images/m6-hierarchy/hierarchy_old.jpg" alt="Confusing hierarchy" class="center-image" /></p>
<h2 id="changes-in-v020-m1">Changes in v0.2.0-m1</h2>
<p>My first attempt to address this was to <strong>make the hierarchy less important</strong>.</p>
<p>This was achieved by always counting detections inside annotations based on their spatial location, not where they happened to fall in the hierarchy.</p>
<p>The change was introduced in <a href="https://qupath.github.io/QuPath-v0.2.0">v0.2.0-m1</a> and depended on <a href="https://locationtech.github.io/jts/"><em>Java Topology Suite</em> (JTS)</a>.
Applying it to the example above, we get a result that I think is considerably more intuitive.</p>
<p><img src="https://petebankhead.github.io/assets/images/m6-hierarchy/hierarchy_new.jpg" alt="Easier hierarchy" class="center-image" /></p>
<p>JTS brings all manner of <em>very</em> useful operations that can be performed on ROIs - and here the key feature is a <em>spatial cache</em> that makes it much faster to find objects based on location.
This has made the performance aspect of using a hierarchy less critical.</p>
<blockquote>
<p>Putting this into practice has involved being able to convert QuPath ROIs to a corresponding JTS representation (a ‘Geometry’).
This has proven deceptively difficult, but after some teething problems in m6 I <em>think</em> this now works robustly.
If you find ROIs that fail, please let me know…</p>
</blockquote>
<p>This change helped reduce (not eliminate) the confusion of the hierarchy, but didn’t help performance.</p>
<p>In fact, performance is probably worse, because finding cells to count using the spatial cache is still slower than simply looking down the hierarchy.</p>
<h2 id="best-of-both-worlds">Best of both worlds?</h2>
<p>You might ask: <strong>if the hierarchy is so troublesome, why keep it at all?</strong></p>
<p>Well, I still think the first of my original intentions makes sense: sometimes it is really helpful to be able to represent hierarchical structures within an image.
Having a flat list of objects (e.g. like ImageJ’s ROI Manager) can be very limiting.
So it would be nice not to dispense with it entirely.</p>
<p><strong>The proposed solution in v0.2.0-m6 is to give the user control</strong>.</p>
<p>The idea is this:</p>
<ul>
<li>Keep QuPath’s hierarchy the same internally. Objects <em>do</em> still have parent/child relationships, and are stored in a hierarchical way.</li>
<li>When adding detections, continue to add them as children of the annotation/TMA core that was selected whenever they were first detected. This hasn’t changed.</li>
<li>When adding an annotation, make the <em>default</em> behavior to add it directly below the image: don’t try to resolve where it should fit in based on the old (and broken) rules about ‘completely contained’.</li>
<li>Make it <em>optional</em> to request that the object <em>is</em> inserted to the ‘right’ place in the hierarchy.</li>
</ul>
<p>The last point is achieved by a new <em>Insert in hierarchy</em> command, which you can access in three ways:</p>
<ul>
<li><em>Objects → Annotations… → Insert into hierarchy</em></li>
<li>Right-clicking with one or more annotations selected</li>
<li><code class="language-plaintext highlighter-rouge">Ctrl + Shift + I</code></li>
</ul>
<p>The rule for insertion is almost the same as before but takes into consideration the <em>area</em> of the annotations that might be parents.
In short, the annotation will be assigned to the <em>smallest</em> potential parent that completely contains its ROI.</p>
<p>Here, you can see it in action.
Notice that by default, annotations are in a ‘flat’ list - but the old hierarchical arrangement can be achieved individually or all together either with the menu options or using shortcut keys.</p>
<p><img src="https://petebankhead.github.io/assets/images/m6-hierarchy/hierarchy.gif" alt="QuPath new hierarchy" class="center-image shadow-image" /></p>
<p>You can also use <em>Objects → Annotations… → Resolve hierarchy</em> to try to resolve <em>all</em> objects in the image in this way.
This is scriptable with <code class="language-plaintext highlighter-rouge">resolveHierarchy()</code>, but best avoid using it if you have a lot of objects.</p>
<p>Together, I hope these changes will retain the advantages of representing objects hierarchically without the previous problems, and thereby make it possible to use QuPath for increasingly complex tasks.</p>
<h2 id="outlook">Outlook</h2>
<p>I don’t know if the approach in v0.2.0-m6 is the best one, although it’s the best I’ve come up with so far.</p>
<p>I suspect refinements will be needed as people try it out and find limitations.</p>
<p>But I hope that by including it in a milestone version that can be easily tested, and explaining the reasons here, it can start a discussion that helps iron out any bugs - or identifies a better approach altogether.</p>
<p>Please let me know what you think!</p>QuPath’s object hierarchy has been an important part of the software since the start.QuPath v0.2.0-m6 available2019-11-17T00:00:00+00:002019-11-17T00:00:00+00:00https://petebankhead.github.io/qupath/2019/11/17/sixth-milestone<h4 id="qupath-v020-m6-is-now-online">QuPath v0.2.0-m6 is <a href="https://github.com/qupath/qupath/releases/tag/v0.2.0-m6">now online</a>!</h4>
<p>This milestone contains <strong>two important bug fixes</strong>, <strong>one big change</strong> and several smaller improvements.</p>
<blockquote>
<p><strong>Warning!</strong> It’s generally not a good idea to switch between different QuPath versions.
You should be able to read older data files within v0.2.0-m6, but the reverse might not be true.
Backup often!</p>
<p>If you find bugs, <em>please report them on <a href="https://github.com/qupath/qupath/issues">GitHub</a> or <a href="https://forum.image.sc/tags/qupath">image.sc</a></em> - don’t just stick with an older milestone that seemed to work!</p>
<p>For all other questions or discussions, please use <a href="https://forum.image.sc/tags/qupath">image.sc</a>.</p>
</blockquote>
<h3 id="two-important-bug-fixes">Two important bug fixes</h3>
<p>1) In v0.2.0-m5, the <em>Positive per mm^2 measurement</em> could be wrong. It would divide by the image area, not the annotation area - so to get the correct measurement you would need to calculate the value yourself based on <em>Num positive</em> and area. This bug only affected v0.2.0-m5, not other versions.</p>
<p><img src="https://petebankhead.github.io/assets/images/m6/positive_density.jpg" alt="Positive density" class="center-image max-width-80 shadow-image" /></p>
<p>2) Converting ROIs with ‘extra pieces nested within holes’ into Java Topology Suite Geometries could lose the nested bits. Most ROIs aren’t like this, not all commands involve JTS, and not all JTS-related commands depend upon the conversion being perfect… so the impact of any error should be quite limited - but it has now been addressed.</p>
<p>An example of a troublesome ROI with pieces inside holes is shown below.</p>
<p><img src="https://petebankhead.github.io/assets/images/m6/complicated_roi.jpg" alt="Complicated ROI" class="center-image max-width-40 shadow-image" /></p>
<h3 id="one-big-change-the-object-hierarchy">One big change: The object hierarchy</h3>
<p>The change to the way objects are handled in QuPath is <strong><a href="/qupath/2019/11/17/changing-the-hierarchy.html">important enough to have its own blog post</a></strong>.
I recommend reading it to understand what is different.</p>
<p>To summarize the outcome:</p>
<ul>
<li>The user now has more control over how and when the object hierarchy is used - the default behavior is different (and simpler) than it had been.</li>
<li>Things that worked before should still work (if they don’t, please report the bug!), but you should be aware that the precise way in which they work may not be identical.</li>
<li>Some operations, especially involving many objects/annotations, should be <em>a lot</em> faster than they were previously.</li>
</ul>
<p><img src="https://petebankhead.github.io/assets/images/m6-hierarchy/hierarchy.gif" alt="QuPath new hierarchy" class="center-image max-width-60 shadow-image" /></p>
<h3 id="everything-else">Everything else</h3>
<p>Most other changes are concerned with stability and performance.</p>
<h4 id="new-roi-type-impacts-data-files">New ROI type (impacts data files!)</h4>
<p>As part of addressing the JTS bug above, <code class="language-plaintext highlighter-rouge">AreaROI</code> has been replaced with <code class="language-plaintext highlighter-rouge">GeometryROI</code>.</p>
<p>What does that mean? Well, <em>complex ROIs created in v0.2.0-m6 may not be readable in earlier QuPath versions.</em>
So please avoid modifying files in m6 if you plan to continue with an earlier QuPath version.</p>
<p>But it also means that some processing should be much faster, since there is less conversion between different ROI representions required.</p>
<blockquote>
<p>To avoid losing work from the past, it should still be possible to <em>read</em> older data files into m6.
But you may find the performance isn’t as good when doing this, because QuPath will occasionally need to convert old-style ROIs to new ones - which can be slow if they are very complex.</p>
</blockquote>
<h4 id="smoother-drawing">Smoother drawing</h4>
<p>In addition to changing how the hierarchy operates, the way annotations are painted on screen has been updated.
This should hopefully mean the drawing tools (e.g. brush) work more smoothly, even when many other objects are nearby.</p>
<blockquote>
<p>If you see any weird visual effects it suggests I’ve got this wrong… but I’m not aware of any problems.</p>
</blockquote>
<h4 id="reorganized-menus">Reorganized menus</h4>
<p>Some menus have been reorganised, and some old commands removed (OpenCV-based cell detection, local binary patterns) on the understanding they were rarely/never used and just added clutter.</p>
<blockquote>
<p>If I’ve removed something you need we can discuss it.
The reorganization shouldn’t really matter, because you use <code class="language-plaintext highlighter-rouge">Ctrl + L</code> to find commands - right?!</p>
</blockquote>
<h4 id="create-points-from-detections-and-keep-the-detections">Create points from detections (and keep the detections)</h4>
<p>The <em>Convert detections to points</em> button under the <em>Counting tool</em> now uses nucleus centroids (rather than cell centroids) where appropriate, and makes deleting the previous detections optional.</p>
<p>If you have a lot of point annotations, QuPath should handle them better and faster.</p>
<p><img src="https://petebankhead.github.io/assets/images/m6/points.jpg" alt="Points" class="center-image max-width-60 shadow-image" /></p>
<h4 id="new-detection-centroid-distances-2d-command">New <em>Detection centroid distances 2D</em> command</h4>
<p>Building on the last option, there’s a new command that can be used to find distances to cells with different classifications.</p>
<blockquote>
<p>Note there is a small rounding error… so that the distance from an object to itself is typically recorded as slightly greater than 0. This may be improved in a future release.</p>
</blockquote>
<p><img src="https://petebankhead.github.io/assets/images/m6/distances.jpg" alt="Distances" class="center-image max-width-60 shadow-image" /></p>
<p><img src="https://petebankhead.github.io/assets/images/m6/distances2.jpg" alt="Distances again" class="center-image max-width-60 shadow-image" /></p>
<h4 id="other-changes--fixes">Other changes & fixes</h4>
<ul>
<li>The pixel classifier shows live area measurements with ‘Classification’ output (in m5 this worked only with ‘Probability’ output)</li>
<li>Extra shortcuts, including <code class="language-plaintext highlighter-rouge">Ctrl+Alt+A</code> to select annotations, <code class="language-plaintext highlighter-rouge">Ctrl+Alt+D</code> to select detections</li>
<li>Undo/Redo and tile cache size information added to Memory Monitor</li>
<li>Added support for ImageWriters to write to output streams</li>
<li>Updated build script to Gradle 6.0</li>
<li>Use <em>bioformats_package.jar</em> rather than separate dependences (easier to upgrade separately)</li>
</ul>
<p>For full details on what has changed, see the <a href="https://github.com/qupath/qupath/commits/master">commit log</a>.</p>QuPath v0.2.0-m6 is now online!QuPath news2019-11-16T00:00:00+00:002019-11-16T00:00:00+00:00https://petebankhead.github.io/qupath/2019/11/16/qupath-update<p><strong>As v0.2.0-m6 is near, it’s time for another update on what’s going on & what’s planned.</strong></p>
<p>Firstly, the big news is that <strong><a href="https://chanzuckerberg.com/eoss/proposals/">QuPath has been selected as one of 42 projects</a> to be funded by the Chan Zuckerberg Initiative</strong> under the <strong><a href="https://chanzuckerberg.com/newsroom/chan-zuckerberg-initiative-awards-5-million-for-open-source-software-projects-essential-to-science/"><em>Essential Open Source Software for Science</em></a></strong> program.</p>
<p>Alongside recent <strong>ISSF3 funding</strong> (University of Edinburgh/Wellcome Trust), things are looking rather a lot better in terms of supporting and developing the software, at least over the next 12 months or so.</p>
<p><em>Look out for more announcements in this regard… and perhaps also a QuPath workshop in 2020.</em></p>
<p><strong>Meanwhile, back in 2019 the milestones continue.</strong>
I appreciate that semi-frequent-but-somewhat-sporadic not-called-stable updates aren’t ideal and can be disruptive, but it has been a necessary consequence of me scrabbling around trying to find spare hours to write the code whenever I could.</p>
<p><img src="https://petebankhead.github.io/assets/images/coding_late.jpg" alt="Coding late" class="center-image max-width-40" /></p>
<p>However, there would have been far fewer of those coding hours were it not for the irrepressible superuser <a href="https://forum.image.sc/u/research_associate"><strong>Research Associate</strong></a> answering many of the questions on <a href="https://forum.image.sc/">https://forum.image.sc/</a>.</p>
<p>Along with <strong>Sara & Zbigniew</strong> from the <em>La Jolla Institute for Immunology</em>, RA has also been testing the latest milestones ahead of their release - helping to catch bugs almost as quickly as I have created them. So huge thanks to them!</p>
<p>I do hope that, despite disruption, the milestone approach has been useful.
I have found it helpful (albeit stressful…) to get feedback as it went along, rather than coding in isolation for a year and then turning up suddenly with a whole new version no one wants.</p>
<p>It also seemed to me practically necessary, because supporting the old stuff while my head is full of the new stuff I’m currently working on is… hard.</p>
<p>Although supporting a plethora of milestone versions is also hard. If you are stuck on an older milestone, please do think of updating… at least for your next project, and do check out the changelog with each new release to see if there have been any bugs fixed that might affect you.</p>
<p>I hope to get off the milestone track over the next few months - and then start on the next version.</p>
<p>To demystify the process and help with planning, here’s my <strong>todo list for v0.2.0</strong>, describing the things that I think are essential before it can be considered stable:</p>
<h4 id="1-refining-existing-features">1. Refining existing features</h4>
<ul>
<li><strong>Polishing up the pixel classifier</strong>.
<ul>
<li>Improving the options for scripting, converting to objects and making measurements.</li>
</ul>
</li>
<li><strong>Rewriting the object classifier</strong>
<ul>
<li>Bringing it more in line with the pixel classifier.</li>
<li>Improve support for more complex classification (e.g. with multiplexed images)</li>
</ul>
</li>
</ul>
<h4 id="2-documentation">2. Documentation</h4>
<ul>
<li><strong>User documentation</strong>
<ul>
<li>An all-new website that is more comprehensive and more maintainable.</li>
</ul>
</li>
<li><strong>Developer documentation</strong>
<ul>
<li>Comprehensive javadocs hosted… somewhere.</li>
</ul>
</li>
</ul>
<h4 id="3-performance--stability">3. Performance & stability</h4>
<ul>
<li><strong>Handling more objects</strong>
<ul>
<li>Continuous improvements in how to work with & query large numbers of objects.</li>
</ul>
</li>
<li><strong>Better progress monitoring</strong>
<ul>
<li>Is it working? Has it crashed? Ideally QuPath would give more feedback & not leave us guessing.</li>
</ul>
</li>
<li><strong>Fixing bugs</strong></li>
</ul>
<p>There is a lot more I would <em>like</em> to include.
But I think these three main topics are the most urgent, and the most useful to the most people.</p>
<p><strong>There will certainly be more to come later.</strong> I hope the brighter funding outlook will help QuPath transition into becoming a more wholeheartedly community project, and less subject to the whims (and mistakes) of a single developer.</p>
<p>To that end, I’d ask that if you are using the software and care about where it goes, <em>please get involved</em>!</p>
<p>The software really shouldn’t be seen as something that just exists and sometimes changes, but rather something you can actively help shape and influence.</p>
<p>One way is through <strong>participating in <a href="https://forum.image.sc/tags/qupath">image.sc</a></strong> - not solely to ask questions, but also to answer them and take part in wider discussions.</p>
<p>I suggest the forum first because I prefer to keep things public where possible, so that the discussions might help others as well. However, if you want to talk <strong>projects and collaborations</strong> where the forum isn’t a good fit and I should wear my academic hat, you can also find me at the <a href="https://www.ed.ac.uk/pathology/people/staff-students/peter-bankhead">University of Edinburgh</a>.</p>
<p>In the end, QuPath is just a tool that is hopefully useful.
My interest is in trying to apply image analysis to further biomedical research - and hope that QuPath can help meet some of the needs in this area.</p>As v0.2.0-m6 is near, it’s time for another update on what’s going on & what’s planned.QuPath v0.2.0-m5 available2019-11-02T00:00:00+00:002019-11-02T00:00:00+00:00https://petebankhead.github.io/qupath/2019/11/02/fifth-milestone<h4 id="qupath-v020-m5-is-now-online">QuPath v0.2.0-m5 is <a href="https://github.com/qupath/qupath/releases/tag/v0.2.0-m5">now online</a>!</h4>
<p>This milestone contains <em>even more changes</em> than m4. The main ones are summarized below.</p>
<blockquote>
<p>If you are using an earlier milestone, it is <strong>strongly recommended to update</strong> (after backing up your work, of course).</p>
<p>Many bugs and issues have been fixed. If you find new ones, <em>please report them on <a href="https://github.com/qupath/qupath/issues">GitHub</a> or <a href="https://forum.image.sc/tags/qupath">image.sc</a></em> - don’t just stick with an older version that seemed to work!</p>
<p>For <em>all other questions or discussions</em> (not bugs), please use <a href="https://forum.image.sc/tags/qupath">image.sc</a> only.</p>
</blockquote>
<h3 id="more-functional-pixel-classifier">More functional pixel classifier!</h3>
<p>The biggest visible change is that the pixel classifier has taken a large step towards being useful.</p>
<p>Currently, it can be applied to do three main things:</p>
<ul>
<li>Measure areas (including relative proportions of different classified regions)</li>
<li>Create objects (both annotations & detections)</li>
<li>Classify detections (based on the pixel classification at the detection centroid)</li>
</ul>
<p>With <a href="#writing-images">a little bit of scripting</a>, you can also export the classification as an indexed image or probability map to use elsewhere.</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/pixel_classifier.jpg" alt="Pixel classifer" class="center-image max-width-80 shadow-image" /></p>
<h4 id="apply-classifiers-to-different-images">Apply classifiers to different images</h4>
<p>When you start building a pixel classifier, you can now switch to different images to immediately see the result of applying the classifier there.</p>
<p>When using a project, you can also save your classifier and then use <em>Classify → Pixel classification → Load pixel classifier</em> to get it back on a new image.
The classifier itself is stored within the project directory.</p>
<h4 id="create-a-training-image">Create a training image</h4>
<p>Note that you <em>can’t</em> continue training the same classifier while skipping between images: a new classifier is always trained from the annotations on the current image only.</p>
<p>However, there is a way to work around this:</p>
<ol>
<li>Put all the images that should be used to train the classifier in a single project</li>
<li>Open each image and create annotations for one or more representative (or challenging) regions, and assign the annotations all the same classification.</li>
<li>Run <em>Classify → Create project training image</em></li>
</ol>
<p>For Step 2. I suggest using <em>Objects → Annotations… → Specify annotation</em>, and choosing the classification <em>Region*</em>.</p>
<p>The end result should be a single image composed of the regions you selected, which can then be used to train a classifier - immediately seeing how it performs across a (parts of) multiple images.</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/training_more.jpg" alt="Training regions" class="center-image max-width-80 shadow-image" /></p>
<p><img src="https://petebankhead.github.io/assets/images/m5/training_image.jpg" alt="Training image" class="center-image max-width-80 shadow-image" /></p>
<blockquote>
<p><em>Create project training image</em> works by extracting regions dynamically from the original image files - it doesn’t need to write a new image file. Usually this should be ok, but if you have memory troubles you may want to <a href="#writing-images">write the image as an OME-TIFF</a> for better performance.</p>
</blockquote>
<h4 id="new-features--feature-overlays">New features & feature overlays</h4>
<p>The performance of the pixel classifier depends a lot on the features used for training.
First, you’ll need to select these with the <em>Edit</em> button.</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/features_select.png" alt="Features select" class="center-image max-width-40 shadow-image" /></p>
<p>The features are basically filtered copies of the image.
The classification result is determined from the values of pixels in the feature images - so ideally these features should accent the kind of thing the classifier should detect.</p>
<p>When training a classifier, you can now visualize the features in context using the drop-down menu on the bottom right of the training window.</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/features_orig.jpg" alt="Features image" class="center-image max-width-80 shadow-image" /></p>
<p><img src="https://petebankhead.github.io/assets/images/m5/features_gauss.jpg" alt="Features image Gaussian" class="center-image max-width-80 shadow-image" /></p>
<p><img src="https://petebankhead.github.io/assets/images/m5/features_gradient_mag.JPG" alt="Features image gradient mag" class="center-image max-width-80 shadow-image" /></p>
<p><img src="https://petebankhead.github.io/assets/images/m5/features_hessian.JPG" alt="Features image Hessian" class="center-image max-width-80 shadow-image" /></p>
<p>This helps with the decision about which features are meaningful: use too many and the classifier can perform worse rather than better.</p>
<p>The following table gives an informal summary of where each feature might come in useful:</p>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Gaussian filter</strong></td>
<td>General-purpose (color & intensity)</td>
</tr>
<tr>
<td><strong>Laplacian of Gaussian</strong></td>
<td>Blob-like things, some edges</td>
</tr>
<tr>
<td><strong>Weighted deviation</strong></td>
<td>Textured vs. smooth areas</td>
</tr>
<tr>
<td><strong>Gradient magnitude</strong></td>
<td>Edges</td>
</tr>
<tr>
<td><strong>Structure tensor eigenvalues</strong></td>
<td>Long, stringy things</td>
</tr>
<tr>
<td><strong>Structure tensor coherence</strong></td>
<td>‘Oriented’ regions</td>
</tr>
<tr>
<td><strong>Hessian determinant</strong></td>
<td>Blob-like things (more specific than Laplacian)</td>
</tr>
<tr>
<td><strong>Hessian eigenvalues</strong></td>
<td>Long, stringy things</td>
</tr>
</tbody>
</table>
<p>But now that you can visualize them, probably best do that and draw your own conclusions.</p>
<blockquote>
<p>Generally, I stick with Gaussian and add others if necessary.
I usually avoid structure tensor features as a) I haven’t found them terribly useful, and b) they take more time and memory to compute.</p>
</blockquote>
<h4 id="advanced-options">Advanced options</h4>
<p>There are some additional advanced options available for the pixel classifier.</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/pixel_advanced.png" alt="Pixel classification advanced options" class="center-image max-width-60 shadow-image" /></p>
<p>Perhaps the most interesting is the ability to set a <em>boundary strategy</em>.
In short, this means that any area annotation (e.g. rectangle, polygon) with a (non-ignored) classification, QuPath can <em>treat the boundary of the annotation as if it had a different classification from the rest of the shape</em>.</p>
<p>The purpose of these methods are to make it possible to train a classifier that tries to learn the separation between dense structures.</p>
<p>Furthermore, when choosing a boundary strategy you also need to specify the line thickness - which also influences the thickness of other line annotations, regardless of boundary strategy.</p>
<blockquote>
<p>A class whose name ends with an asterisk (e.g. <em>Region*</em> or <em>Ignore*</em>) is ignored in some circumstances, e.g. when calculating areas after pixel classification or when generating objects.</p>
</blockquote>
<p>A multichannel fluorescence image (thanks to La Jolla Institute for Immunology)…</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/pixel_bounds.jpg" alt="Pixel classification image" class="center-image max-width-80 shadow-image" /></p>
<p>…with a pixel classifier using a boundary class with thin boundaries…</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/pixel_bounds_thinner.jpg" alt="Pixel classification thin boundaries" class="center-image max-width-80 shadow-image" /></p>
<p>…or thicker boundaries.</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/pixel_bounds_thicker.jpg" alt="Pixel classification thick boundaries" class="center-image max-width-80 shadow-image" /></p>
<h4 id="limitations--plans">Limitations & plans</h4>
<p>There is a lot more to be done with the pixel classifier, including:</p>
<ul>
<li>Make it (more easily) scriptable</li>
<li>Finalize the format for saving/loading classifiers (this might change!)</li>
<li>Create a way to make measurements ‘stick’ in annotations</li>
<li>Improve the local normalization option (you can try it already, but I don’t recommend it…)</li>
<li>Add extra options for what to do with the output (e.g. classify cells based on partial overlap, subcellular detection)</li>
<li>Add support for color transforms (not just image channels)</li>
<li>Make it possible to be more selective about which features are calculated for which channels</li>
<li>Show progress bars for things that take a long time</li>
<li>Document how all this relates to deep learning…</li>
</ul>
<h3 id="simple-thresholding">Simple thresholding</h3>
<p>Not everything requires a fully-trained pixel classifier.
Sometimes a simple threshold will do.</p>
<p><em>Classify → Pixel classification → Create simple thresholder</em> is intended for such times.</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/threshold_tissue.jpg" alt="Threshold tissue" class="center-image max-width-80 shadow-image" /></p>
<p>This command should ultimately replace <em>Simple tissue detection</em>. The concept is similar but the classifier approach provides much more control and interactive feedback.</p>
<blockquote>
<p>QuPath’s pixel classification framework is intended to be flexible (which is where deep learning will fit in soon).
Setting a threshold is really creating a classifier… just a very simple one.</p>
</blockquote>
<h3 id="wand-trickery">Wand trickery</h3>
<p>The <em>Wand</em> tool has a new trick that is useful with the pixel classifier (including simple thresholding).</p>
<p>If you press the <em>Ctrl</em> key then all smoothing will be disabled, and the wand will expand for pixels with <em>exactly</em> the same color.
This is helpful if you have an opaque overlay on top of the image, since the wand will then more closely expand to the edge of any region.</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/threshold_wand_trick.jpg" alt="Threshold wand" class="center-image max-width-80 shadow-image" /></p>
<p>Separately, there is an additional wand option in the preferences to choose slightly different behavior by being applied to images with different color transforms.</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/wand_prefs.jpg" alt="Wand options" class="center-image max-width-40 shadow-image" /></p>
<h3 id="scripting-improvements">Scripting improvements</h3>
<p>As QuPath’s internals move around with each new milestone, scripts break.
It shouldn’t always be this way, but for now maintaining backwards compatibility would just require too much time and be too restrictive when it comes to improving the software overall.</p>
<p>A few changes have been made to help mitigate the trouble I cause.</p>
<h4 id="automatic-imports">Automatic imports</h4>
<p>Previously, QuPath would import one special class: <code class="language-plaintext highlighter-rouge">QPEx</code>.
This contains all the common scripting methods like <code class="language-plaintext highlighter-rouge">getAnnotationObjects()</code> and <code class="language-plaintext highlighter-rouge">runPlugin()</code>.</p>
<p>But going beyond this requires using parts of QuPath strewn across hundreds more source code files.
These need to be imported before they can be used.</p>
<p>To help with this, the script editor can now import a list of useful classes.
The following example prints them out:</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/script_imports.png" alt="Default imports" class="center-image max-width-60 shadow-image" /></p>
<p>This means that you can use any methods that you like from these classes without a need to import.</p>
<blockquote>
<p>The line <code class="language-plaintext highlighter-rouge">static qupath.lib.gui.scripting.QPEx*</code> means that the available methods are imported directly - that’s why you can type <code class="language-plaintext highlighter-rouge">getSelectedObject()</code> rather than <code class="language-plaintext highlighter-rouge">QPEx.getSelectedObject()</code>.</p>
<p>This doesn’t happen for all classes because it would be hard to separate a mess of hundreds of methods: using the class name (e.g. <code class="language-plaintext highlighter-rouge">ROIs</code>, <code class="language-plaintext highlighter-rouge">PathObjects</code>) is useful to group them by theme.</p>
</blockquote>
<p>Here’s an example to programmatically create annotations:</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/script_rois.JPG" alt="Scripting ROIs" class="center-image max-width-80 shadow-image" /></p>
<p>And here’s one showing an error message:</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/script_message.JPG" alt="Scripting errors" class="center-image max-width-80 shadow-image" /></p>
<p>Previously, you’d have to explicitly import the class before being able to do this (and curse me if I’ve moved the class since you last needed it).</p>
<p>Now, you can leave it up to the default import statements instead, provided that <em>Run → Include default imports</em> is selected.</p>
<h4 id="some-more-informative-errors">(Some) more informative errors</h4>
<p>To ease the transition further, I have added some more informative error messages for common (Pete-induced) problems.</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/script_error.png" alt="Scripting error" class="center-image max-width-80 shadow-image" /></p>
<h4 id="finding-methods">Finding methods</h4>
<p>The default imports are a start, but you still need to know what the classes are and what they can do.</p>
<p>One important trick is <code class="language-plaintext highlighter-rouge">Ctrl + space</code>.
Press this in the script editor and it will try a (very rudimentary) auto-complete.
Press <code class="language-plaintext highlighter-rouge">space</code> repeatedly to cycle through the options.</p>
<p>Another is the new <code class="language-plaintext highlighter-rouge">describe</code> method, used as shown here:</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/script_describe.png" alt="Scripting describe" class="center-image max-width-80 shadow-image" /></p>
<p>It is, admittedly, a poor substitute for a full software development environment.
But there’s always <a href="https://github.com/qupath/qupath/wiki/Advanced-scripting-with-IntelliJ">IntelliJ for serious scripting</a>.</p>
<h4 id="new-methods">New methods</h4>
<p>Several new built-in methods have also been added to <code class="language-plaintext highlighter-rouge">QPEx</code> make life easier:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Set the pixel width & height to 0.5 microns</span>
<span class="n">setPixelSizeMicrons</span><span class="o">(</span><span class="mf">0.5</span><span class="o">,</span> <span class="mf">0.5</span><span class="o">)</span>
<span class="c1">// Set the names of channels (in order)</span>
<span class="c1">// Since v0.2.0-m4 channel names are more important for commands</span>
<span class="c1">// such as cell detection</span>
<span class="n">setChannelNames</span><span class="o">(</span><span class="s1">'First'</span><span class="o">,</span> <span class="s1">'Second'</span><span class="o">,</span> <span class="s1">'Third'</span><span class="o">)</span>
<span class="c1">// Replace the classifications for all objects</span>
<span class="n">replaceClassification</span><span class="o">(</span><span class="s1">'Tumor'</span><span class="o">,</span> <span class="s1">'Stroma'</span><span class="o">)</span>
<span class="c1">// Export summary measurements for the entire image</span>
<span class="kt">def</span> <span class="n">path</span> <span class="o">=</span> <span class="n">buildFilePath</span><span class="o">(</span><span class="n">PROJECT_BASE_DIR</span><span class="o">,</span> <span class="s1">'export.txt'</span><span class="o">)</span>
<span class="n">saveImageMeasurements</span><span class="o">(</span><span class="n">path</span><span class="o">)</span>
</code></pre></div></div>
<h4 id="syntax-highlighting">Syntax highlighting</h4>
<p>A final change in the script editor is in the colors used for syntax highlighting: methods no longer turn orange (although keywords do), and some other colors are different.</p>
<p>The reason for this is…</p>
<h3 id="dark-mode">Dark mode</h3>
<p>As the eagle-eyed will have spotted, some of the screenshots have been rather darker than before.</p>
<p>QuPath has always had a kind of ‘Dark mode’, but it wasn’t terribly consistent; the script editor, for example, remained a glaring white, and now and again black-on-almost-black text wasn’t very legible.</p>
<p>v0.2.0-m5 now has a dark mode that should be better behaved.</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/style_light.png" alt="Light mode" class="center-image max-width-40 shadow-image" /></p>
<p><img src="https://petebankhead.github.io/assets/images/m5/style_dark.png" alt="Dark mode" class="center-image max-width-40 shadow-image" /></p>
<h3 id="a-cancel-button-that-cancels">A ‘Cancel’ button that cancels</h3>
<p>Continuing the theme of ‘things that were in QuPath but didn’t really work’, when running commands (e.g. cell detection) over a large area there was always a large ‘Cancel’ button.</p>
<p>This was more aspirational than functional: it made a request that the command should stop running, but in practice the command would almost invariably ignore this and continue on its merry way to completion… however long it took.</p>
<p>Now, cancel actually <em>can</em> cancel.</p>
<p>And, even better, QuPath will attempt to restore the state to how it was before the command was started - rather than just aborting the mission awkwardly and leaving a lot of lingering tiles around.</p>
<p>As part of making this work, progress when running a command is displayed differently - with colors used to indicate tiles that are pending, processing or complete.</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/progress.jpg" alt="A cancelling cancel" class="center-image max-width-80 shadow-image" /></p>
<h3 id="better-cell-detection-in-large-regions">Better cell detection in large regions</h3>
<p>Alongside the improved ‘Cancel’ button, work has been done to improve the consistency of how QuPath resolves overlaps whenever a large region is split into smaller tiled regions for parallel processing.</p>
<p>This is most evident with <em>Cell detection</em> (including <em>Positive cell detection</em>), where the size of the overlaps has also been increased.</p>
<p>Note that <em>this does make some difference to the cells that are detected</em>.
Absolute counts when running cell detection in v0.2.0-m5 and earlier versions are likely to differ.</p>
<p>However, it addresses an issue whereby running cell detection with the same parameters on the same (large) region could sometimes give slightly different results because of differences in how the tile overlaps were resolved.
Now, the results should be the same each time.</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/cells.jpg" alt="Cell detection" class="center-image max-width-80 shadow-image" /></p>
<h3 id="major-roi-revisions">Major ROI revisions</h3>
<p>ROIs have undergone major changes… with much effort expended in making it <em>look</em> like they have not changed at all.</p>
<p>The primary difference is that QuPath makes much more use of <a href="https://en.wikipedia.org/wiki/JTS_Topology_Suite"><em>Java Topology Suite</em></a> (JTS).
You can convert any QuPath ROI into a JTS Geometry simply by calling <code class="language-plaintext highlighter-rouge">ROI.getGeometry()</code>, opening up a world of topological possibilities.</p>
<p>One of the visible outcomes of this is that some commands can now be much faster, such as converting pixel classifications to objects and splitting objects/filling holes.
The <em>Objects → Annotations… → Remove fragments</em> command (previously called <em>Refine annotations</em>) gives one way to do this.</p>
<p>JTS is also used in the (renamed) <em>Analyze → Calculate features → Distance to annotations 2D</em> command, shown below with the new <em>Svidro2</em> colormap (added due to popular<sup>1</sup> demand).</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/svidro.jpg" alt="Distance to annotations" class="center-image max-width-80 shadow-image" /></p>
<blockquote>
<p><sup>1</sup> From one person, but a significant one person.</p>
</blockquote>
<h3 id="improvements-when-annotating-in-detail">Improvements when annotating in detail</h3>
<p>When using the pixel classifier, I often find I want to be <em>really</em> careful when annotating close to boundaries.
Two changes help with this:</p>
<ul>
<li>The viewer permits zooming in to a higher magnification</li>
<li>The (area) drawing tools can now ‘snap’ to pixel coordinates</li>
</ul>
<p>The snapping is a work-in-progress; in particular, when moving a snapped object it can end up ‘between’ pixel coordinates.
But even in this imperfect form I personally find it to be very useful.</p>
<p>It is possible to turn off <em>Use pixel snapping</em> in the preferences.</p>
<p><em>Using the brush tool to create a ROI with snapping on:</em></p>
<p><img src="https://petebankhead.github.io/assets/images/m5/snapping.jpg" alt="Snapping on" class="center-image max-width-80 shadow-image" /></p>
<p><em>Using the brush tool to create a ROI with snapping off:</em></p>
<p><img src="https://petebankhead.github.io/assets/images/m5/snapping_off.jpg" alt="Snapping off" class="center-image max-width-80 shadow-image" /></p>
<p>In each case a binary mask is created and shown in ImageJ using the following script:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">roi</span> <span class="o">=</span> <span class="n">getSelectedROI</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">img</span> <span class="o">=</span> <span class="n">BufferedImageTools</span><span class="o">.</span><span class="na">createROIMask</span><span class="o">(</span><span class="n">roi</span><span class="o">,</span> <span class="mf">1.0</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">imp</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ij</span><span class="o">.</span><span class="na">ImagePlus</span><span class="o">(</span><span class="s2">"Roi"</span><span class="o">,</span> <span class="n">img</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">request</span> <span class="o">=</span> <span class="n">RegionRequest</span><span class="o">.</span><span class="na">createInstance</span><span class="o">(</span><span class="s2">"Anything"</span><span class="o">,</span> <span class="mi">1</span><span class="o">,</span> <span class="n">roi</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">roiIJ</span> <span class="o">=</span> <span class="n">IJTools</span><span class="o">.</span><span class="na">convertToIJRoi</span><span class="o">(</span><span class="n">roi</span><span class="o">,</span> <span class="o">-</span><span class="n">request</span><span class="o">.</span><span class="na">getX</span><span class="o">(),</span> <span class="o">-</span><span class="n">request</span><span class="o">.</span><span class="na">getY</span><span class="o">(),</span> <span class="mf">1.0</span><span class="o">)</span>
<span class="n">imp</span><span class="o">.</span><span class="na">setRoi</span><span class="o">(</span><span class="n">roiIJ</span><span class="o">)</span>
<span class="n">imp</span><span class="o">.</span><span class="na">show</span><span class="o">()</span>
</code></pre></div></div>
<p>While sub-pixel annotations can look pleasingly smooth, one of the benefits of snapping is that the binary images created are more predictable. Another is that they (usually) contain fewer vertices, so QuPath may run a bit faster if you have a lot of annotations.</p>
<h3 id="writing-images">Writing images</h3>
<p>QuPath’s ability to export images has improved in two main ways.</p>
<h4 id="from-the-user-interface">From the user interface</h4>
<p><em>File → Export images…</em> has been revised to give three options:</p>
<ul>
<li><strong>Original pixels</strong> Export image regions selecting from a range of file formats, including JPEG, PNG, ImageJ TIFF, ImageJ ZIP and OME-TIFF.</li>
<li><strong>Rendered RGB</strong> Export an RGB version of the image <em>as it appears with the current viewer settings</em>. This is somewhat like ImageJ’s <em>Flatten</em> command.</li>
<li><strong>OME TIFF</strong> Similar to <em>original pixels</em> exclusively for OME-TIFF format, but with more fine-grained options to control the output (e.g. with compression type, pyramidal levels).</li>
</ul>
<h4 id="from-scripts">From scripts</h4>
<p>Writing images in scripts used to be extremely cumbersome.
Well no more!</p>
<p>In the event that your image <em>is a manageable size to export in one go</em> you can use</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">getCurrentServer</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">path</span> <span class="o">=</span> <span class="n">buildFilePath</span><span class="o">(</span><span class="n">PROJECT_BASE_DIR</span><span class="o">,</span> <span class="s1">'output.ome.tif'</span><span class="o">)</span>
<span class="n">writeImageRegion</span><span class="o">(</span><span class="n">server</span><span class="o">,</span> <span class="n">path</span><span class="o">)</span>
</code></pre></div></div>
<p>Often, you’ll want to extract a region.
Here, I export the selected ROI downsampled by a factor of 4:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">getCurrentServer</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">path</span> <span class="o">=</span> <span class="n">buildFilePath</span><span class="o">(</span><span class="n">PROJECT_BASE_DIR</span><span class="o">,</span> <span class="s1">'output.ome.tif'</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">roi</span> <span class="o">=</span> <span class="n">getSelectedROI</span><span class="o">()</span>
<span class="kt">double</span> <span class="n">downsample</span> <span class="o">=</span> <span class="mi">4</span>
<span class="kt">def</span> <span class="n">region</span> <span class="o">=</span> <span class="n">RegionRequest</span><span class="o">.</span><span class="na">createInstance</span><span class="o">(</span><span class="n">server</span><span class="o">.</span><span class="na">getPath</span><span class="o">(),</span> <span class="n">downsample</span><span class="o">,</span> <span class="n">roi</span><span class="o">)</span>
<span class="n">writeImageRegion</span><span class="o">(</span><span class="n">server</span><span class="o">,</span> <span class="n">region</span><span class="o">,</span> <span class="n">path</span><span class="o">)</span>
</code></pre></div></div>
<p>Or to export the full image downsampled by a factor of 40:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">getCurrentServer</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">path</span> <span class="o">=</span> <span class="n">buildFilePath</span><span class="o">(</span><span class="n">PROJECT_BASE_DIR</span><span class="o">,</span> <span class="s1">'output.ome.tif'</span><span class="o">)</span>
<span class="kt">double</span> <span class="n">downsample</span> <span class="o">=</span> <span class="mi">40</span>
<span class="kt">def</span> <span class="n">region</span> <span class="o">=</span> <span class="n">RegionRequest</span><span class="o">.</span><span class="na">createInstance</span><span class="o">(</span><span class="n">server</span><span class="o">,</span> <span class="n">downsample</span><span class="o">)</span>
<span class="n">writeImageRegion</span><span class="o">(</span><span class="n">server</span><span class="o">,</span> <span class="n">region</span><span class="o">,</span> <span class="n">path</span><span class="o">)</span>
</code></pre></div></div>
<p>You can change the file extension depending upon what file type you want, but beware that some file types don’t support all possible images (e.g. .jpg cannot be used successfully with a multiplexed, non-RGB fluorescence image).</p>
<p>Perhaps more excitingly, because there is not yet a <em>proper</em> way to export the pixel classification results as an image, you can use this script (which may also help with batch processing using <em>Run for project</em>).</p>
<blockquote>
<p>This should be thought of as a workaround rather than the official way… but better than nothing.</p>
</blockquote>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// We do need to import this...</span>
<span class="kn">import</span> <span class="nn">qupath.lib.classifiers.pixel.PixelClassificationImageServer</span>
<span class="c1">// Load the classifier</span>
<span class="kt">def</span> <span class="n">project</span> <span class="o">=</span> <span class="n">getProject</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">classifier</span> <span class="o">=</span> <span class="n">project</span><span class="o">.</span><span class="na">getPixelClassifiers</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="s1">'to test'</span><span class="o">)</span>
<span class="c1">// Get the current image & create the (dynamically calculated) classified image</span>
<span class="kt">def</span> <span class="n">imageData</span> <span class="o">=</span> <span class="n">getCurrentImageData</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="k">new</span> <span class="n">PixelClassificationImageServer</span><span class="o">(</span><span class="n">imageData</span><span class="o">,</span> <span class="n">classifier</span><span class="o">)</span>
<span class="c1">// Define an output path & file extension (may also choose .png, .ome.tif or something else)</span>
<span class="kt">def</span> <span class="n">path</span> <span class="o">=</span> <span class="n">buildFilePath</span><span class="o">(</span><span class="n">PROJECT_BASE_DIR</span><span class="o">,</span> <span class="s1">'classification'</span><span class="o">)</span>
<span class="n">mkdirs</span><span class="o">(</span><span class="n">path</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">name</span> <span class="o">=</span> <span class="n">getProjectEntry</span><span class="o">().</span><span class="na">getImageName</span><span class="o">()</span>
<span class="n">path</span> <span class="o">=</span> <span class="n">buildFilePath</span><span class="o">(</span><span class="n">path</span><span class="o">,</span> <span class="n">name</span> <span class="o">+</span> <span class="s1">'.tif'</span><span class="o">)</span>
<span class="c1">// Write the image</span>
<span class="n">writeImage</span><span class="o">(</span><span class="n">server</span><span class="o">,</span> <span class="n">path</span><span class="o">)</span>
</code></pre></div></div>
<h3 id="memory-monitor--show-input">Memory monitor & show input</h3>
<p>A while back I wrote a <a href="/qupath/scripting/2018/03/06/script-memory-monitor.html">Memory Monitor script</a>.</p>
<p>It’s now a built-in command under <em>View → Show memory monitor</em>.</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/memory_monitor.png" alt="Memory monitor" class="center-image max-width-60 shadow-image" /></p>
<p>For good measure, it’s accompanied by <em>View → Show input on screen</em> to make shortcuts visible.</p>
<p><img src="https://petebankhead.github.io/assets/images/m5/input_display.png" alt="Input display" class="center-image max-width-40 shadow-image" /></p>
<h3 id="many-more-improvements-and-bug-fixes">Many more improvements and bug fixes</h3>
<p>There are lots of other changes and fixes, including:</p>
<ul>
<li>Fixed size estimate for large images (previously caused some images not to open)</li>
<li>Fixed bug that meant the file chooser forgot the last directory</li>
<li><em>Distance to annotations</em> command supports line and point ROIs (not just areas)</li>
<li>Converting tile classifications to annotations <a href="https://github.com/qupath/qupath/issues/359">#359</a></li>
<li>Calculating intensity features for RGB fluorescence <a href="https://github.com/qupath/qupath/issues/365">#365</a></li>
<li>Setting stroke thickness, thanks to @jballanc <a href="https://github.com/qupath/qupath/pull/362">#362</a></li>
</ul>
<p>For full details on what has changed, see the <a href="https://github.com/qupath/qupath/commits/master">commit log</a>.</p>QuPath v0.2.0-m5 is now online!Building QuPath2019-10-01T00:00:00+00:002019-10-01T00:00:00+00:00https://petebankhead.github.io/qupath/2019/10/01/building-qupath<p><strong>Building software can be tricky, but hopefully this won’t be - thanks to <a href="http://gradle.org"><em>Gradle</em></a>.</strong></p>
<p>The following instructions are for the <em>latest</em> version of QuPath (v0.2.0-m4 at the time of writing).
Things won’t necessarily work quite the same way with older versions.</p>
<p>The following instructions assume:</p>
<ul>
<li>You’re starting from scratch</li>
<li>You’re not an expert in building software</li>
</ul>
<p>If you <em>are</em> an expert, you’ll know which steps you can skip or amend.</p>
<h2 id="step-1-installing-prerequisites">Step 1: Installing prerequisites</h2>
<h3 id="install-a-java-development-kit-jdk">Install a Java Development Kit (JDK)</h3>
<p>QuPath requires OpenJDK 11 (or later*).
You can get this through <a href="https://adoptopenjdk.net/">https://adoptopenjdk.net/</a></p>
<p>During installation you may be asked if you want to add the JDK to your PATH.
To make things easier, I do.</p>
<p>If you can’t (e.g. because of some other Java software needing the PATH set to something else) I’m afraid I’ll leave resolving that up to you.</p>
<blockquote>
<p><strong><em>-OpenJDK 11 vs. OpenJDK 13</em></strong></p>
<p>At the time of writing, Gradle does not yet support OpenJDK 13.
Therefore you’ll need OpenJDK 11 to start Gradle, even if you ultimately use OpenJDK 13 when building QuPath itself.</p>
</blockquote>
<h3 id="download-the-latest-jpackage">Download the latest JPackage</h3>
<p>To create the QuPath installer, you’ll need <em>jpackage</em>.</p>
<p>One day, <em>jpackage</em> is likely to be part of the JDK, and everything will be simpler.</p>
<p>But for now, you need to download an early access build separately from <a href="https://jdk.java.net/jpackage/">jdk.java.net</a>.</p>
<p>Extract the contents (e.g. right-click and unzip) to a folder of your choosing <em>and remember where it is because you’ll need the path to that folder later</em>.</p>
<h2 id="step-2-get-the-qupath-source-code">Step 2: Get the QuPath source code</h2>
<p>You can get the QuPath source code from <a href="https://github.com/qupath/qupath">https://github.com/qupath/qupath</a></p>
<p>If you’re using either Mac or Windows, the following steps may help.</p>
<ul>
<li>Install Atom (a text editor) from <a href="https://atom.io/">https://atom.io/</a></li>
<li>Install GitHub Desktop from <a href="https://desktop.github.com/">https://desktop.github.com/</a></li>
<li>Press the <em>Clone or download</em> button at <a href="https://github.com/qupath/qupath">https://github.com/qupath/qupath</a> and choose <em>Open in Desktop</em></li>
</ul>
<p><img src="https://petebankhead.github.io/assets/images/building/building-clone.png" alt="Clone" class="center-image max-width-40 shadow-image" /></p>
<h2 id="step-3-add-a-gradleproperties-file">Step 3: Add a gradle.properties file</h2>
<p>Go to GitHub Desktop, make sure that the ‘Current repository’ is ‘qupath’ and choose <em>Repository → Open in Atom</em></p>
<p><img src="https://petebankhead.github.io/assets/images/building/building-github-desktop-project.png" alt="GitHub Desktop project" class="center-image max-width-40 shadow-image" /></p>
<p>Under the ‘Project’ tab on the left within <em>Atom</em>, right-click on the top ‘qupath’ and choose <em>New File</em>.</p>
<p><img src="https://petebankhead.github.io/assets/images/building/building-properties-create.png" alt="GitHub Desktop project" class="center-image max-width-40 shadow-image" /></p>
<p>When prompted to <em>Enter the path for the new file</em> type <code class="language-plaintext highlighter-rouge">gradle.propertes</code></p>
<p><img src="https://petebankhead.github.io/assets/images/building/building-properties.png" alt="GitHub Desktop project" class="center-image max-width-60 shadow-image" /></p>
<p>Inside <em>gradle.properties</em>, enter the following text</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>packager=/path/to/jpackage
</code></pre></div></div>
<p>where you’ll need to replace <code class="language-plaintext highlighter-rouge">/path/to/jpackage</code> with actual path.</p>
<blockquote>
<p>Note that <em>jpackage</em> will be somewhere inside the folder you extracted earlier. On Windows, it’s the path to <em>jpackage.exe</em> you are looking for and what you need to type should look something like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>packager=C:/Users/myname/Documents/jpackage/jdk-14/bin/jpackage.exe
</code></pre></div> </div>
</blockquote>
<p>If you <em>want</em>, you can add extra information to <em>gradle.properties</em> to customize how QuPath is built (or how quickly it is built), e.g.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// Essential!
packager=/path/to/jpackage
// Optional
org.gradle.parallel=true // If you are impatient
org.gradle.java.home=/path/to/another/jdk // If you want to use a different JDK to build QuPath (e.g. OpenJDK 13)
request-git-tag=true // If you installed Git & want to know *exactly* what was the last commit prior to building the software
</code></pre></div></div>
<h2 id="step-4-build-qupath-with-gradle">Step 4: Build QuPath with Gradle</h2>
<h3 id="open-the-qupath-source-directory-in-a-command-prompt">Open the QuPath source directory in a command prompt</h3>
<p>One way to do this is to go back to GitHub Desktop and choose <em>Repository → Open in Command Prompt</em></p>
<blockquote>
<p>You may be asked if you want to install Git.</p>
<p>You don’t have to (I think…), but if you do then you’ll be ask a lot of questions during the installation.
One of them is to choose a text editor, where you can select <em>Atom</em>.</p>
</blockquote>
<h2 id="run-gradlew">Run gradlew</h2>
<p>At the command prompt, type the following:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gradlew createPackage
</code></pre></div></div>
<p>for Windows, or</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./gradlew createPackage
</code></pre></div></div>
<p>for MacOS and Linux.</p>
<p>This will download Gradle and all its dependencies, so may take a bit of time (and an internet connection) the first time you run it.</p>
<p>If all goes well, you should see a triumphant message that the build was successful.</p>
<p><img src="https://petebankhead.github.io/assets/images/building/building-success.png" alt="Building success" class="center-image max-width-40 shadow-image" /></p>
<p>Afterwards, you should find QuPath inside the <code class="language-plaintext highlighter-rouge">./build/dist</code> subdirectory. You may then drag it to a more convenient location.</p>
<p><strong>Congratulations!</strong> You’ve now built QuPath, and can run it as normal from now on… at least until there is another update, when you can repeat the (hopefully painless) process.</p>
<hr />
<h2 id="extras">Extras</h2>
<h3 id="variations--troubleshooting">Variations & troubleshooting</h3>
<p>The code above should create everything you need to run QuPath.</p>
<p>If you want an installer instead, you can use</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gradlew createPackage -Ptype=installer
</code></pre></div></div>
<p>Note that for this to work you’ll need to install <a href="https://wixtoolset.org/">WIX Toolset</a>.</p>
<p>Inevitably, things will go wrong at some point.
When this happens, it’s worth running</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gradlew clean
</code></pre></div></div>
<p>to clean up old files that could be causing trouble.</p>
<p>You can also combine the options, e.g.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gradlew clean createPackage -Ptype=installer
</code></pre></div></div>
<p>will clean first, build QuPath & create an installer all in one go.</p>
<h3 id="getting-the-latest-updates">Getting the latest updates</h3>
<p>Once you’ve built QuPath once, updating it to use the latest source code should be much easier.</p>
<p>In <em>GitHub Desktop</em>, see the right-most button on the main toolbar.
This serves two purposes: to <em>Fetch</em> information about the latest changes (from GitHub) and to <em>Pull</em> the changes down to your computer.</p>
<p><img src="https://petebankhead.github.io/assets/images/building/building-branches.png" alt="Building branches" class="center-image max-width-80 shadow-image" /></p>
<p>If the option is <em>Fetch origin</em>, and you press the button then if there are any changes to pull the text on the button will switch to <em>Pull origin</em> with info about the number of changes available.</p>
<p>You can press it again to pull those changes, and then rebuild QuPath using <code class="language-plaintext highlighter-rouge">gradlew</code> if necessary.</p>
<p><img src="https://petebankhead.github.io/assets/images/building/building-pull.png" alt="Pulling" class="center-image max-width-40 shadow-image" /></p>
<p>You can also use the middle button in <em>GitHub Desktop</em> to switch ‘branches’.
Branches basically make it possible to have different versions of the code in development in parallel while trying out new changes.</p>
<p>The following screenshot shows QuPath where I have checked out a specific branch called ‘pete-m5’.</p>
<p><img src="https://petebankhead.github.io/assets/images/building/building-branches-m5.png" alt="Building branches" class="center-image max-width-80 shadow-image" /></p>
<p>If the changes prove worthwhile, the changes in ‘pete-m5’ will be merged back into the ‘master’ branch.</p>
<h3 id="running-from-an-ide">Running from an IDE</h3>
<p>You should be able to import QuPath into any IDE (e.g. eclipse, IntelliJ) that supports Gradle.</p>
<p>I personally use eclipse for QuPath development, which allows me to run the software in debug mode - and even change the code while it is running.</p>
<p>To do this, I use <em>Run → Debug As → QuPath</em>.</p>
<p>To make this option available, you’ll first need to create a debug configuration with <em>Run → Debug Configurations…</em>.</p>
<p>Within this dialog, I use the following options to control the available memory and set the working directory/Java library path.</p>
<p><img src="https://petebankhead.github.io/assets/images/building/building-eclipse-1.png" alt="Building with eclipse" class="center-image max-width-60 shadow-image" /></p>
<p><img src="https://petebankhead.github.io/assets/images/building/building-eclipse-2.png" alt="Building with eclipse" class="center-image max-width-60 shadow-image" /></p>Building software can be tricky, but hopefully this won’t be - thanks to Gradle.Scripting in v0.2.0-m42019-08-21T00:00:00+00:002019-08-21T00:00:00+00:00https://petebankhead.github.io/qupath/2019/08/21/scripting-in-v020<p>There have now been <a href="https://qupath.github.io/QuPath-v0.2.0">four milestone releases</a> of QuPath v0.2.0 over the past six months - where <em>milestone</em> means ‘not finished, potentially buggy, but usable enough to be worthwhile’.</p>
<p>All together they’ve been downloaded over 10,000 times so far. From the <a href="https://forum.image.sc/tags/qupath">image.sc</a> forum and <a href="https://github.com/qupath/qupath/issues">GitHub issues</a> a few things have become clear:</p>
<ul>
<li>The new annotation tools in the milestone versions make it hard to go back to v0.1.2</li>
<li>Clear, reproducible bug reports are <em>very</em> useful</li>
<li>The pixel classifier is pretty popular… and really needs to be saveable and scriptable one day</li>
<li>People use scripting <em>a lot</em>. And the changes I’m making are breaking scripts <em>a lot</em></li>
</ul>
<p>This post will focus on the last point, and give a bit of a brain dump on what I’ve been doing.</p>
<h3 id="contents">Contents</h3>
<ul>
<li><a href="#why-all-the-breaking">Why all the breaking?</a></li>
<li><a href="#whats-up-with-the-documentation">What’s up with the documentation?</a></li>
<li><a href="#and-javadocs">And javadocs?</a></li>
<li><a href="#what-do-i-need-to-know-to-write-fantastic-scripts">What do I need to know to write fantastic scripts?</a>
<ul>
<li><a href="#default-imports">Default imports</a></li>
<li><a href="#projects">Projects</a></li>
<li><a href="#accessing-the-current-image">Accessing the current image</a></li>
<li><a href="#accessing-image-metadata">Accessing image metadata</a></li>
<li><a href="#accessing-pixels">Accessing pixels</a></li>
<li><a href="#creating-rois">Creating ROIs</a></li>
<li><a href="#creating-objects">Creating objects</a></li>
<li><a href="#working-with-bufferedimages">Working with BufferedImages</a></li>
<li><a href="#working-with-imagej">Working with ImageJ</a></li>
<li><a href="#working-with-opencv">Working with OpenCV</a></li>
<li><a href="#working-with-rois">Manipulating ROIs</a></li>
<li><a href="#working-with-java-topology-suite">Working with Java Topology Suite</a></li>
<li><a href="#serialization--json">Serialization & JSON</a></li>
</ul>
</li>
<li><a href="#what-next">What next?</a></li>
</ul>
<h3 id="why-all-the-breaking">Why all the breaking?</h3>
<p>Basically, the QuPath code wasn’t (and isn’t) as neat and tidy as it ought to be</p>
<p>Until v0.1.2, I wrote it largely in isolation with myself being the only developer I had to satisfy.
I was sometimes too forgiving towards me.</p>
<p>Then, immediately after v0.1.2 was available, I had an unfortunate enforced break from developing it.</p>
<p>Since returning to QuPath in 2018, I’ve been working through a long backlog of ideas and necessary improvements to make the software more powerful, robust and accessible to other developers… and (not yet entirely successfully) applying for funding to try to share the task a bit.</p>
<h3 id="whats-up-with-the-documentation">What’s up with the documentation?</h3>
<p>I have <em>mostly</em> left the documentation at <a href="https://github.com/qupath/qupath/wiki">https://github.com/qupath/qupath/wiki</a> untouched.
It refers to v0.1.2, which is still considered the ‘stable’ version.</p>
<p>Lacking in time, I prioritize working on the code to try to stabilize v0.2.0 as soon as possible. But I do manage <a href="https://twitter.com/petebankhead">bursts of documentation in under 280 characters</a> now and then.</p>
<p>Fortunately, the mysterious superuser <em><a href="https://forum.image.sc/u/research_associate">Research_Associate</a></em> has written a <a href="https://forum.image.sc/t/qupath-intro-choose-your-own-analysis-adventure/27906"><em>spectacular</em> post on image.sc</a> that serves as alternative documentation and contains some more up-to-date information.</p>
<h3 id="and-javadocs">And <em>javadocs</em>?</h3>
<p>Javadocs are on my todo list… I’ve written thousands of them in the last few months, but there are many more to write.
Since the API isn’t stable (and I haven’t got around to it) they aren’t hosted online yet.</p>
<p><em>However</em> QuPath is a lot easier to build now than it was. The steps are described in the <a href="https://github.com/qupath/qupath/blob/master/README.md#building-qupath-with-gradle">ReadMe</a>.</p>
<p>If you get that working, you can also try</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gradlew mergedJavadocs
</code></pre></div></div>
<p>to generate some local Javadocs for yourself.</p>
<h3 id="what-do-i-need-to-know-to-write-fantastic-scripts">What do I need to know to write fantastic scripts?</h3>
<p>Here’s an overview of the general concepts in <em>QuPath</em>:</p>
<ul>
<li>Your images may (and probably should) be organized in a <code class="language-plaintext highlighter-rouge">Project</code>
<ul>
<li>Each image in the project is represented by a <code class="language-plaintext highlighter-rouge">ProjectImageEntry</code></li>
<li>When you open a <code class="language-plaintext highlighter-rouge">ProjectImageEntry</code>, you get an <code class="language-plaintext highlighter-rouge">ImageData</code> displayed in the viewer
<ul>
<li>The <code class="language-plaintext highlighter-rouge">ImageData</code> stores a few things, including:
<ul>
<li>The <code class="language-plaintext highlighter-rouge">ImageType</code> (e.g. Brightfield, Fluorescence)</li>
<li>Any <code class="language-plaintext highlighter-rouge">ColorDeconvolutionStains</code> required (if brightfield)</li>
<li>An <code class="language-plaintext highlighter-rouge">ImageServer</code>, for accessing pixels and metadata</li>
<li>A <code class="language-plaintext highlighter-rouge">PathObjectHierarchy</code>, containing <code class="language-plaintext highlighter-rouge">PathObjects</code> in a tree-like structure
<ul>
<li>Each <code class="language-plaintext highlighter-rouge">PathObject</code> contains a <code class="language-plaintext highlighter-rouge">ROI</code> and a <code class="language-plaintext highlighter-rouge">MeasurementList</code></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>When you analyze an image in QuPath, you take your <code class="language-plaintext highlighter-rouge">ImageData</code>, access pixels from the <code class="language-plaintext highlighter-rouge">ImageServer</code> and try to represent what the image contains in the <code class="language-plaintext highlighter-rouge">PathObjectHierarchy</code>. Then you query the object hierarchy to extract some kind of summary measurements.</p>
<p>This is all fairly similar to v0.1.2, so fits with that documentation <a href="https://github.com/qupath/qupath/wiki/QuPath-concepts">here</a>.</p>
<p>The following sections describe how to use these concepts in practice.
Some of this has changed a lot.</p>
<h4 id="default-imports">Default imports</h4>
<p>In the <em>Script Editor</em>, there is an option <em>Run → Include default bindings</em>.</p>
<p>If this is selected, <em>QuPath</em> will add the following line to the top of your script:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">static</span> <span class="n">qupath</span><span class="o">.</span><span class="na">lib</span><span class="o">.</span><span class="na">gui</span><span class="o">.</span><span class="na">scripting</span><span class="o">.</span><span class="na">QPEx</span><span class="o">.*</span>
</code></pre></div></div>
<p>This means you’ve access to all the static methods in <a href="https://github.com/qupath/qupath/blob/master/qupath-gui-fx/src/main/java/qupath/lib/gui/scripting/QPEx.java"><code class="language-plaintext highlighter-rouge">QPEx</code></a> and <a href="https://github.com/qupath/qupath/blob/master/qupath-core/src/main/java/qupath/lib/scripting/QP.java"><code class="language-plaintext highlighter-rouge">QP</code></a> directly.</p>
<p>All the examples below assume that <code class="language-plaintext highlighter-rouge">QPEx</code> is imported one way or another.
If you don’t want to rely on the default import, just put that line at the top of your scripts explicitly.</p>
<h4 id="projects">Projects</h4>
<p>The following simple script prints the names of all images in a project:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">project</span> <span class="o">=</span> <span class="n">getProject</span><span class="o">()</span>
<span class="k">for</span> <span class="o">(</span><span class="n">entry</span> <span class="k">in</span> <span class="n">project</span><span class="o">.</span><span class="na">getImageList</span><span class="o">())</span> <span class="o">{</span>
<span class="n">print</span> <span class="n">entry</span><span class="o">.</span><span class="na">getImageName</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>
<p>The script below is rather more interesting; it will</p>
<ul>
<li>Open each image in turn</li>
<li>Extract the annotations from the hierarchy</li>
<li>Print the image name & annotation count per image</li>
</ul>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">project</span> <span class="o">=</span> <span class="n">getProject</span><span class="o">()</span>
<span class="k">for</span> <span class="o">(</span><span class="n">entry</span> <span class="k">in</span> <span class="n">project</span><span class="o">.</span><span class="na">getImageList</span><span class="o">())</span> <span class="o">{</span>
<span class="kt">def</span> <span class="n">imageData</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="na">readImageData</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">hierarchy</span> <span class="o">=</span> <span class="n">imageData</span><span class="o">.</span><span class="na">getHierarchy</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">annotations</span> <span class="o">=</span> <span class="n">hierarchy</span><span class="o">.</span><span class="na">getAnnotationObjects</span><span class="o">()</span>
<span class="n">print</span> <span class="n">entry</span><span class="o">.</span><span class="na">getImageName</span><span class="o">()</span> <span class="o">+</span> <span class="s1">'\t'</span> <span class="o">+</span> <span class="n">annotations</span><span class="o">.</span><span class="na">size</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>
<p>The extra logging messages generated when opening each image can be annoying, so you might want to print everything at the end instead.
Creating a <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/StringBuilder.html"><code class="language-plaintext highlighter-rouge">StringBuilder</code></a> can help:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">sb</span> <span class="o">=</span> <span class="k">new</span> <span class="n">StringBuilder</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">project</span> <span class="o">=</span> <span class="n">getProject</span><span class="o">()</span>
<span class="k">for</span> <span class="o">(</span><span class="n">entry</span> <span class="k">in</span> <span class="n">project</span><span class="o">.</span><span class="na">getImageList</span><span class="o">())</span> <span class="o">{</span>
<span class="kt">def</span> <span class="n">imageData</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="na">readImageData</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">hierarchy</span> <span class="o">=</span> <span class="n">imageData</span><span class="o">.</span><span class="na">getHierarchy</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">annotations</span> <span class="o">=</span> <span class="n">hierarchy</span><span class="o">.</span><span class="na">getAnnotationObjects</span><span class="o">()</span>
<span class="n">sb</span> <span class="o"><<</span> <span class="n">entry</span><span class="o">.</span><span class="na">getImageName</span><span class="o">()</span> <span class="o">+</span> <span class="s1">'\t'</span> <span class="o">+</span> <span class="n">annotations</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o"><<</span> <span class="s1">'\n'</span>
<span class="o">}</span>
<span class="n">print</span> <span class="n">sb</span><span class="o">.</span><span class="na">toString</span><span class="o">()</span>
</code></pre></div></div>
<p>Both options are rather a lot slower than they need to be, because <em>QuPath</em> will go to the bother of constructing the full <code class="language-plaintext highlighter-rouge">ImageData</code> (including <code class="language-plaintext highlighter-rouge">ImageServer</code>) for every image - even though it never needs to actually access pixels.</p>
<p>You can avoid this as follows:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">project</span> <span class="o">=</span> <span class="n">getProject</span><span class="o">()</span>
<span class="k">for</span> <span class="o">(</span><span class="n">entry</span> <span class="k">in</span> <span class="n">project</span><span class="o">.</span><span class="na">getImageList</span><span class="o">())</span> <span class="o">{</span>
<span class="kt">def</span> <span class="n">hierarchy</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="na">readHierarchy</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">annotations</span> <span class="o">=</span> <span class="n">hierarchy</span><span class="o">.</span><span class="na">getAnnotationObjects</span><span class="o">()</span>
<span class="n">print</span> <span class="n">entry</span><span class="o">.</span><span class="na">getImageName</span><span class="o">()</span> <span class="o">+</span> <span class="s1">'\t'</span> <span class="o">+</span> <span class="n">annotations</span><span class="o">.</span><span class="na">size</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>
<blockquote>
<p>Note: These scripts won’t work in v0.1.2, where the process was <em>much</em> more awkward…</p>
</blockquote>
<h4 id="accessing-the-current-image">Accessing the current image</h4>
<p>The above scripts can access images in a project, regardless of whether they are open in the GUI or not.</p>
<p>Often, you only need to access the image currently open. In that case, just use</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">imageData</span> <span class="o">=</span> <span class="n">getCurrentImageData</span><span class="o">()</span>
<span class="n">print</span> <span class="n">imageData</span>
</code></pre></div></div>
<p>This gets the image from the current viewer. It is equivalent to:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">viewer</span> <span class="o">=</span> <span class="n">getCurrentViewer</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">imageData</span> <span class="o">=</span> <span class="n">viewer</span><span class="o">.</span><span class="na">getImageData</span><span class="o">()</span>
<span class="n">print</span> <span class="n">imageData</span>
</code></pre></div></div>
<p>In conjunction with <em>Run → Run for project</em> you often don’t need to loop through project images directly - just write a script for the current image, then run that script for all images with <em>Run for project</em>.</p>
<h4 id="accessing-image-metadata">Accessing image metadata</h4>
<p>To get image metadata, you’ll need the <code class="language-plaintext highlighter-rouge">ImageServer</code>:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">imageData</span> <span class="o">=</span> <span class="n">getCurrentImageData</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">imageData</span><span class="o">.</span><span class="na">getServer</span><span class="o">()</span>
<span class="n">print</span> <span class="n">server</span>
</code></pre></div></div>
<p>In recent QuPath milestones, this is equivalent to:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">getCurrentServer</span><span class="o">()</span>
<span class="n">print</span> <span class="n">server</span>
</code></pre></div></div>
<p>You can then query properties of the image. Simple ones can be accessed directly, e.g.</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">getCurrentServer</span><span class="o">()</span>
<span class="n">print</span> <span class="n">server</span><span class="o">.</span><span class="na">getWidth</span><span class="o">()</span> <span class="o">+</span> <span class="s1">' x '</span> <span class="o">+</span> <span class="n">server</span><span class="o">.</span><span class="na">getHeight</span><span class="o">()</span>
</code></pre></div></div>
<p>All the key metadata exists in an <code class="language-plaintext highlighter-rouge">ImageServerMetadata</code> object:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">getCurrentServer</span><span class="o">()</span>
<span class="n">print</span> <span class="n">server</span><span class="o">.</span><span class="na">getMetadata</span><span class="o">()</span>
</code></pre></div></div>
<p>Pixel sizes are in a <code class="language-plaintext highlighter-rouge">PixelCalibrationObject</code> (different from v0.1.2, where you got them directly from the server!):</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">getCurrentServer</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">cal</span> <span class="o">=</span> <span class="n">server</span><span class="o">.</span><span class="na">getMetadata</span><span class="o">().</span><span class="na">getPixelCalibration</span><span class="o">()</span>
<span class="n">print</span> <span class="n">cal</span>
</code></pre></div></div>
<p>As a shortcut, you can also use</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">getCurrentServer</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">cal</span> <span class="o">=</span> <span class="n">server</span><span class="o">.</span><span class="na">getPixelCalibration</span><span class="o">()</span>
<span class="n">print</span> <span class="n">cal</span>
</code></pre></div></div>
<p>In the past, pixels were either in microns or uncalibrated.
In the future, <em>QuPath</em> might need to support other pixel units and so this assumption is a bit less critical than it was before.
It is tempting to make pixel size requests more general and elaborate (always asking for units), but for now the need to request pixel sizes in microns is so common that there remain helper methods to do this:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">getCurrentServer</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">cal</span> <span class="o">=</span> <span class="n">server</span><span class="o">.</span><span class="na">getPixelCalibration</span><span class="o">()</span>
<span class="n">print</span> <span class="n">cal</span><span class="o">.</span><span class="na">getPixelWidthMicrons</span><span class="o">()</span>
<span class="n">print</span> <span class="n">cal</span><span class="o">.</span><span class="na">getPixelHeightMicrons</span><span class="o">()</span>
<span class="n">print</span> <span class="n">cal</span><span class="o">.</span><span class="na">getAveragedPixelSizeMicrons</span><span class="o">()</span>
</code></pre></div></div>
<p>You can expect the result to be <code class="language-plaintext highlighter-rouge">Double.NaN</code> if the size information is not available.
You can check for this using ‘standard’ Java/Groovy.</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">double</span> <span class="n">myNaN</span> <span class="o">=</span> <span class="n">Double</span><span class="o">.</span><span class="na">NaN</span>
<span class="c1">// Two Java/Groovy-friendly ways to check values are 'usable'</span>
<span class="n">print</span> <span class="n">Double</span><span class="o">.</span><span class="na">isNaN</span><span class="o">(</span><span class="n">myNaN</span><span class="o">)</span>
<span class="n">print</span> <span class="n">Double</span><span class="o">.</span><span class="na">isFinite</span><span class="o">(</span><span class="n">myNaN</span><span class="o">)</span>
<span class="c1">// A bad way to check for NaN - confusing because Java & Groovy handle == differently</span>
<span class="n">print</span> <span class="o">(</span><span class="n">myNaN</span> <span class="o">==</span> <span class="n">Double</span><span class="o">.</span><span class="na">NaN</span><span class="o">)</span> <span class="c1">// Don't do this!</span>
</code></pre></div></div>
<h4 id="accessing-pixels">Accessing pixels</h4>
<p>If you want pixels, you’ll get them as a Java <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/java/awt/image/BufferedImage.html"><code class="language-plaintext highlighter-rouge">BufferedImage</code></a>.</p>
<p>To do so, you need to <em>request</em> them from a server with a <code class="language-plaintext highlighter-rouge">RegionRequest</code>.
This includes the server path, a downsample factor and bounding box coordinates (defined in full resolution pixel units, with the origin at the top left of the image):</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">qupath.lib.regions.*</span>
<span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">getCurrentServer</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">path</span> <span class="o">=</span> <span class="n">server</span><span class="o">.</span><span class="na">getPath</span><span class="o">()</span>
<span class="kt">double</span> <span class="n">downsample</span> <span class="o">=</span> <span class="mf">4.0</span>
<span class="kt">int</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">100</span>
<span class="kt">int</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">200</span>
<span class="kt">int</span> <span class="n">width</span> <span class="o">=</span> <span class="mi">1000</span>
<span class="kt">int</span> <span class="n">height</span> <span class="o">=</span> <span class="mi">2000</span>
<span class="kt">def</span> <span class="n">request</span> <span class="o">=</span> <span class="n">RegionRequest</span><span class="o">.</span><span class="na">createInstance</span><span class="o">(</span><span class="n">path</span><span class="o">,</span> <span class="n">downsample</span><span class="o">,</span> <span class="n">x</span><span class="o">,</span> <span class="n">y</span><span class="o">,</span> <span class="n">width</span><span class="o">,</span> <span class="n">height</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">img</span> <span class="o">=</span> <span class="n">server</span><span class="o">.</span><span class="na">readBufferedImage</span><span class="o">(</span><span class="n">request</span><span class="o">)</span>
<span class="n">print</span> <span class="n">img</span>
</code></pre></div></div>
<p>There are two reasons why QuPath uses <code class="language-plaintext highlighter-rouge">RegionRequest</code> objects:</p>
<ul>
<li>You’d otherwise need to pass a lot of parameters to the <code class="language-plaintext highlighter-rouge">readBufferedImage</code> method</li>
<li><code class="language-plaintext highlighter-rouge">RegionRequests</code> can be (and are) used as keys for an image cache</li>
</ul>
<p>In any case, the above script assumes a single-plane image. If you may have a z-stack, you can define the z-slice and time point in your request:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">qupath.lib.regions.*</span>
<span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">getCurrentServer</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">path</span> <span class="o">=</span> <span class="n">server</span><span class="o">.</span><span class="na">getPath</span><span class="o">()</span>
<span class="kt">double</span> <span class="n">downsample</span> <span class="o">=</span> <span class="mf">4.0</span>
<span class="kt">int</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">100</span>
<span class="kt">int</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">200</span>
<span class="kt">int</span> <span class="n">width</span> <span class="o">=</span> <span class="mi">1000</span>
<span class="kt">int</span> <span class="n">height</span> <span class="o">=</span> <span class="mi">2000</span>
<span class="kt">int</span> <span class="n">z</span> <span class="o">=</span> <span class="mi">0</span>
<span class="kt">int</span> <span class="n">t</span> <span class="o">=</span> <span class="mi">0</span>
<span class="kt">def</span> <span class="n">request</span> <span class="o">=</span> <span class="n">RegionRequest</span><span class="o">.</span><span class="na">createInstance</span><span class="o">(</span><span class="n">path</span><span class="o">,</span> <span class="n">downsample</span><span class="o">,</span> <span class="n">x</span><span class="o">,</span> <span class="n">y</span><span class="o">,</span> <span class="n">width</span><span class="o">,</span> <span class="n">height</span><span class="o">,</span> <span class="n">z</span><span class="o">,</span> <span class="n">t</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">img</span> <span class="o">=</span> <span class="n">server</span><span class="o">.</span><span class="na">readBufferedImage</span><span class="o">(</span><span class="n">request</span><span class="o">)</span>
<span class="n">print</span> <span class="n">img</span>
</code></pre></div></div>
<p>If you have a selected object with a <code class="language-plaintext highlighter-rouge">ROI</code> in the image, you can also use that to create the request:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">qupath.lib.regions.*</span>
<span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">getCurrentServer</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">roi</span> <span class="o">=</span> <span class="n">getSelectedROI</span><span class="o">()</span>
<span class="kt">double</span> <span class="n">downsample</span> <span class="o">=</span> <span class="mf">4.0</span>
<span class="kt">def</span> <span class="n">request</span> <span class="o">=</span> <span class="n">RegionRequest</span><span class="o">.</span><span class="na">createInstance</span><span class="o">(</span><span class="n">server</span><span class="o">.</span><span class="na">getPath</span><span class="o">(),</span> <span class="n">downsample</span><span class="o">,</span> <span class="n">roi</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">img</span> <span class="o">=</span> <span class="n">server</span><span class="o">.</span><span class="na">readBufferedImage</span><span class="o">(</span><span class="n">request</span><span class="o">)</span>
<span class="n">print</span> <span class="n">img</span>
</code></pre></div></div>
<blockquote>
<p>The <em>server path</em> previously was an image path and it could be used to construct a new server… but this is no longer the case. Rather, the key thing now is that it must be unique for a server, since it is used for caching.</p>
<p><code class="language-plaintext highlighter-rouge">server.getPath()</code> may be renamed to <code class="language-plaintext highlighter-rouge">server.getID()</code> or similar in the future to reflect this.</p>
</blockquote>
<h4 id="creating-rois">Creating ROIs</h4>
<p>Previously, there were public constructors for ROIs. <em>You shouldn’t use these now!</em></p>
<p>Rather, use the static methods in the <code class="language-plaintext highlighter-rouge">ROIs</code> class.</p>
<p>This will require specifying the z-slice and timepoint. To avoid passing lots of parameters (and getting the order mixed up), you should instead use an <code class="language-plaintext highlighter-rouge">ImagePlane</code> object:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">qupath.lib.roi.ROIs</span>
<span class="kn">import</span> <span class="nn">qupath.lib.regions.ImagePlane</span>
<span class="kt">int</span> <span class="n">z</span> <span class="o">=</span> <span class="mi">0</span>
<span class="kt">int</span> <span class="n">t</span> <span class="o">=</span> <span class="mi">0</span>
<span class="kt">def</span> <span class="n">plane</span> <span class="o">=</span> <span class="n">ImagePlane</span><span class="o">.</span><span class="na">getPlane</span><span class="o">(</span><span class="n">z</span><span class="o">,</span> <span class="n">t</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">roi</span> <span class="o">=</span> <span class="n">ROIs</span><span class="o">.</span><span class="na">createRectangleROI</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="mi">100</span><span class="o">,</span> <span class="mi">100</span><span class="o">,</span> <span class="n">plane</span><span class="o">)</span>
<span class="n">print</span> <span class="n">roi</span>
</code></pre></div></div>
<p>There are various different kinds of ROI that can be created, including with <code class="language-plaintext highlighter-rouge">createEllipseROI</code>, <code class="language-plaintext highlighter-rouge">createPolygonROI</code>, <code class="language-plaintext highlighter-rouge">createLineROI</code>.</p>
<h4 id="creating-objects">Creating objects</h4>
<p>To actually make a ROI visible, it needs to be part of an object.</p>
<p>The <code class="language-plaintext highlighter-rouge">PathObjects</code> class helps in a similar way to <code class="language-plaintext highlighter-rouge">ROIs</code> - again, you shouldn’t create objects using constructors directly.</p>
<p>This script creates a new annotation with an ellipse ROI, and adds it to the hierarchy for the current image (using the <code class="language-plaintext highlighter-rouge">QPEx.addObject()</code> method):</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">qupath.lib.objects.PathObjects</span>
<span class="kn">import</span> <span class="nn">qupath.lib.roi.ROIs</span>
<span class="kn">import</span> <span class="nn">qupath.lib.regions.ImagePlane</span>
<span class="kt">int</span> <span class="n">z</span> <span class="o">=</span> <span class="mi">0</span>
<span class="kt">int</span> <span class="n">t</span> <span class="o">=</span> <span class="mi">0</span>
<span class="kt">def</span> <span class="n">plane</span> <span class="o">=</span> <span class="n">ImagePlane</span><span class="o">.</span><span class="na">getPlane</span><span class="o">(</span><span class="n">z</span><span class="o">,</span> <span class="n">t</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">roi</span> <span class="o">=</span> <span class="n">ROIs</span><span class="o">.</span><span class="na">createEllipseROI</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="mi">100</span><span class="o">,</span> <span class="mi">100</span><span class="o">,</span> <span class="n">plane</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">annotation</span> <span class="o">=</span> <span class="n">PathObjects</span><span class="o">.</span><span class="na">createAnnotationObject</span><span class="o">(</span><span class="n">roi</span><span class="o">)</span>
<span class="n">addObject</span><span class="o">(</span><span class="n">annotation</span><span class="o">)</span>
</code></pre></div></div>
<p>To create a detection rather than an annotation, you’d use <code class="language-plaintext highlighter-rouge">createDetectionObject</code>.</p>
<p>Putting it together with previous sections, to create square tiles across an entire image for the current <code class="language-plaintext highlighter-rouge">ImagePlane</code> we could use:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">qupath.lib.objects.PathObjects</span>
<span class="kn">import</span> <span class="nn">qupath.lib.roi.ROIs</span>
<span class="kn">import</span> <span class="nn">qupath.lib.regions.ImagePlane</span>
<span class="kt">def</span> <span class="n">imageData</span> <span class="o">=</span> <span class="n">getCurrentImageData</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">plane</span> <span class="o">=</span> <span class="n">getCurrentViewer</span><span class="o">().</span><span class="na">getImagePlane</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">imageData</span><span class="o">.</span><span class="na">getServer</span><span class="o">()</span>
<span class="kt">int</span> <span class="n">tileSize</span> <span class="o">=</span> <span class="mi">1024</span>
<span class="kt">def</span> <span class="n">tiles</span> <span class="o">=</span> <span class="o">[]</span>
<span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">y</span> <span class="o"><</span> <span class="n">server</span><span class="o">.</span><span class="na">getHeight</span><span class="o">()</span> <span class="o">-</span> <span class="n">tileSize</span><span class="o">;</span> <span class="n">y</span> <span class="o">+=</span> <span class="n">tileSize</span><span class="o">)</span> <span class="o">{</span>
<span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">x</span> <span class="o"><</span> <span class="n">server</span><span class="o">.</span><span class="na">getWidth</span><span class="o">()</span> <span class="o">-</span> <span class="n">tileSize</span><span class="o">;</span> <span class="n">x</span> <span class="o">+=</span> <span class="n">tileSize</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">def</span> <span class="n">roi</span> <span class="o">=</span> <span class="n">ROIs</span><span class="o">.</span><span class="na">createRectangleROI</span><span class="o">(</span><span class="n">x</span><span class="o">,</span> <span class="n">y</span><span class="o">,</span> <span class="n">tileSize</span><span class="o">,</span> <span class="n">tileSize</span><span class="o">,</span> <span class="n">plane</span><span class="o">)</span>
<span class="n">tiles</span> <span class="o"><<</span> <span class="n">PathObjects</span><span class="o">.</span><span class="na">createAnnotationObject</span><span class="o">(</span><span class="n">roi</span><span class="o">)</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">addObjects</span><span class="o">(</span><span class="n">tiles</span><span class="o">)</span>
</code></pre></div></div>
<h4 id="working-with-bufferedimages">Working with BufferedImages</h4>
<p>Once you have a BufferedImage, you are already in Java-land and don’t need QuPath-specific documentation for most things.</p>
<p>Scripts like <a href="https://petebankhead.github.io/qupath/scripting/2018/03/13/script-export-import-binary-masks.html">this one</a> to create binary images can then help <em>with one major change</em>.
Previously, you had to do some awkward gymnastics to convert a <code class="language-plaintext highlighter-rouge">ROI</code> into a <code class="language-plaintext highlighter-rouge">java.awt.Shape</code> object. That’s now easier:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">def</span> <span class="n">roi</span> <span class="o">=</span> <span class="n">getSelectedROI</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">shape</span> <span class="o">=</span> <span class="n">roi</span><span class="o">.</span><span class="na">getShape</span><span class="o">()</span>
<span class="n">print</span> <span class="n">shape</span>
</code></pre></div></div>
<p>Here’s a script applying this to pull out a region from an RGB image for a selected ROI, and show that region in ImageJ along with a new binary mask:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">qupath.lib.regions.*</span>
<span class="kn">import</span> <span class="nn">ij.*</span>
<span class="kn">import</span> <span class="nn">java.awt.Color</span>
<span class="kn">import</span> <span class="nn">java.awt.image.BufferedImage</span>
<span class="c1">// Read RGB image & show in ImageJ (won't work for multichannel!)</span>
<span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">getCurrentServer</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">roi</span> <span class="o">=</span> <span class="n">getSelectedROI</span><span class="o">()</span>
<span class="kt">double</span> <span class="n">downsample</span> <span class="o">=</span> <span class="mf">4.0</span>
<span class="kt">def</span> <span class="n">request</span> <span class="o">=</span> <span class="n">RegionRequest</span><span class="o">.</span><span class="na">createInstance</span><span class="o">(</span><span class="n">server</span><span class="o">.</span><span class="na">getPath</span><span class="o">(),</span> <span class="n">downsample</span><span class="o">,</span> <span class="n">roi</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">img</span> <span class="o">=</span> <span class="n">server</span><span class="o">.</span><span class="na">readBufferedImage</span><span class="o">(</span><span class="n">request</span><span class="o">)</span>
<span class="k">new</span> <span class="nf">ImagePlus</span><span class="o">(</span><span class="s2">"Image"</span><span class="o">,</span> <span class="n">img</span><span class="o">).</span><span class="na">show</span><span class="o">()</span>
<span class="c1">// Create a binary mask & show in ImageJ</span>
<span class="kt">def</span> <span class="n">shape</span> <span class="o">=</span> <span class="n">roi</span><span class="o">.</span><span class="na">getShape</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">imgMask</span> <span class="o">=</span> <span class="k">new</span> <span class="n">BufferedImage</span><span class="o">(</span><span class="n">img</span><span class="o">.</span><span class="na">getWidth</span><span class="o">(),</span> <span class="n">img</span><span class="o">.</span><span class="na">getHeight</span><span class="o">(),</span> <span class="n">BufferedImage</span><span class="o">.</span><span class="na">TYPE_BYTE_GRAY</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">g2d</span> <span class="o">=</span> <span class="n">imgMask</span><span class="o">.</span><span class="na">createGraphics</span><span class="o">()</span>
<span class="n">g2d</span><span class="o">.</span><span class="na">scale</span><span class="o">(</span><span class="mf">1.0</span><span class="s">/request.getDownsample(), 1.0/</span><span class="n">request</span><span class="o">.</span><span class="na">getDownsample</span><span class="o">())</span>
<span class="n">g2d</span><span class="o">.</span><span class="na">translate</span><span class="o">(-</span><span class="n">request</span><span class="o">.</span><span class="na">getX</span><span class="o">(),</span> <span class="o">-</span><span class="n">request</span><span class="o">.</span><span class="na">getY</span><span class="o">())</span>
<span class="n">g2d</span><span class="o">.</span><span class="na">setColor</span><span class="o">(</span><span class="n">Color</span><span class="o">.</span><span class="na">WHITE</span><span class="o">)</span>
<span class="n">g2d</span><span class="o">.</span><span class="na">fill</span><span class="o">(</span><span class="n">shape</span><span class="o">)</span>
<span class="n">g2d</span><span class="o">.</span><span class="na">dispose</span><span class="o">()</span>
<span class="k">new</span> <span class="nf">ImagePlus</span><span class="o">(</span><span class="s2">"Mask"</span><span class="o">,</span> <span class="n">imgMask</span><span class="o">).</span><span class="na">show</span><span class="o">()</span>
</code></pre></div></div>
<p>The mask is generated using Java’s built-in rendering of Shapes.</p>
<h4 id="working-with-imagej">Working with ImageJ</h4>
<p>The above is fine for simple cases, but fails to make the most of ImageJ.
It doesn’t set the image metadata, so there’s no way to relate back extracted regions to where they were originally in the whole slide image.
It also doesn’t work in general for multichannel images.</p>
<p>If you want to apply ImageJ scripting in QuPath, it is best to let QuPath take care of the conversion.
<code class="language-plaintext highlighter-rouge">IJTools</code> is the new class that helps with that (or <code class="language-plaintext highlighter-rouge">IJExtension</code> to interact directly with the GUI).</p>
<p>The following script is similar to that above, but works for multichannel images and sets ImageJ properties.
It also doesn’t create a mask directly, but rather converts the QuPath ROI so that further processing (e.g. to generate the mask) can be performed in ImageJ.</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">qupath.lib.regions.*</span>
<span class="kn">import</span> <span class="nn">qupath.imagej.tools.IJTools</span>
<span class="kn">import</span> <span class="nn">qupath.imagej.gui.IJExtension</span>
<span class="kn">import</span> <span class="nn">ij.*</span>
<span class="c1">// Request an ImageJ instance - this will open the GUI if necessary</span>
<span class="c1">// This isn't essential, but makes it it possible to interact with any image that is shown</span>
<span class="n">IJExtension</span><span class="o">.</span><span class="na">getImageJInstance</span><span class="o">()</span>
<span class="c1">// Read image & show in ImageJ</span>
<span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">getCurrentServer</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">roi</span> <span class="o">=</span> <span class="n">getSelectedROI</span><span class="o">()</span>
<span class="kt">double</span> <span class="n">downsample</span> <span class="o">=</span> <span class="mf">4.0</span>
<span class="kt">def</span> <span class="n">request</span> <span class="o">=</span> <span class="n">RegionRequest</span><span class="o">.</span><span class="na">createInstance</span><span class="o">(</span><span class="n">server</span><span class="o">.</span><span class="na">getPath</span><span class="o">(),</span> <span class="n">downsample</span><span class="o">,</span> <span class="n">roi</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">pathImage</span> <span class="o">=</span> <span class="n">IJTools</span><span class="o">.</span><span class="na">convertToImagePlus</span><span class="o">(</span><span class="n">server</span><span class="o">,</span> <span class="n">request</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">imp</span> <span class="o">=</span> <span class="n">pathImage</span><span class="o">.</span><span class="na">getImage</span><span class="o">()</span>
<span class="n">imp</span><span class="o">.</span><span class="na">show</span><span class="o">()</span>
<span class="c1">// Convert QuPath ROI to ImageJ Roi & add to open image</span>
<span class="kt">def</span> <span class="n">roiIJ</span> <span class="o">=</span> <span class="n">IJTools</span><span class="o">.</span><span class="na">convertToIJRoi</span><span class="o">(</span><span class="n">roi</span><span class="o">,</span> <span class="n">pathImage</span><span class="o">)</span>
<span class="n">imp</span><span class="o">.</span><span class="na">setRoi</span><span class="o">(</span><span class="n">roiIJ</span><span class="o">)</span>
</code></pre></div></div>
<p>This introduces another class: <code class="language-plaintext highlighter-rouge">PathImage</code>.</p>
<p>This is basically a wrapper for an image of some kind (here, an ImageJ <code class="language-plaintext highlighter-rouge">ImagePlus</code>) along with some calibration information.</p>
<p>Often we don’t need the <code class="language-plaintext highlighter-rouge">PathImage</code> wrapper, but here we keep it so that we can pass it to <code class="language-plaintext highlighter-rouge">IJTools.convertToIJRoi(roi, pathImage)</code> later.</p>
<h4 id="working-with-opencv">Working with OpenCV</h4>
<p>Rather than <code class="language-plaintext highlighter-rouge">BufferedImage</code> or <code class="language-plaintext highlighter-rouge">ImagePlus</code> objects, perhaps you prefer to write your processing code using OpenCV.</p>
<p>In v0.1.2, <em>QuPath</em> used the default OpenCV Java bindings - which were troublesome in multiple ways. Now, it uses <a href="https://github.com/bytedeco/javacpp-presets/tree/master/opencv">JavaCPP</a>.</p>
<p>However, although OpenCV can be nice to code with it can also be hard to code with <em>interactively</em>. Therefore in <em>QuPath</em> there are helper functions to help convert from OpenCV to ImageJ when necessary. The following shows this in action:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">qupath.lib.regions.*</span>
<span class="kn">import</span> <span class="nn">qupath.imagej.tools.IJTools</span>
<span class="kn">import</span> <span class="nn">qupath.opencv.tools.OpenCVTools</span>
<span class="kn">import</span> <span class="nn">org.bytedeco.opencv.opencv_core.Size</span>
<span class="kn">import</span> <span class="nn">static</span> <span class="n">org</span><span class="o">.</span><span class="na">bytedeco</span><span class="o">.</span><span class="na">opencv</span><span class="o">.</span><span class="na">global</span><span class="o">.</span><span class="na">opencv_core</span><span class="o">.*</span>
<span class="kn">import</span> <span class="nn">static</span> <span class="n">org</span><span class="o">.</span><span class="na">bytedeco</span><span class="o">.</span><span class="na">opencv</span><span class="o">.</span><span class="na">global</span><span class="o">.</span><span class="na">opencv_imgproc</span><span class="o">.*</span>
<span class="kn">import</span> <span class="nn">ij.*</span>
<span class="c1">// Read BufferedImage region</span>
<span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">getCurrentServer</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">roi</span> <span class="o">=</span> <span class="n">getSelectedROI</span><span class="o">()</span>
<span class="kt">double</span> <span class="n">downsample</span> <span class="o">=</span> <span class="mf">4.0</span>
<span class="kt">def</span> <span class="n">request</span> <span class="o">=</span> <span class="n">RegionRequest</span><span class="o">.</span><span class="na">createInstance</span><span class="o">(</span><span class="n">server</span><span class="o">.</span><span class="na">getPath</span><span class="o">(),</span> <span class="n">downsample</span><span class="o">,</span> <span class="n">roi</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">img</span> <span class="o">=</span> <span class="n">server</span><span class="o">.</span><span class="na">readBufferedImage</span><span class="o">(</span><span class="n">request</span><span class="o">)</span>
<span class="c1">// Convert to an OpenCV Mat, then apply a difference of Gaussians filter</span>
<span class="kt">def</span> <span class="n">mat</span> <span class="o">=</span> <span class="n">OpenCVTools</span><span class="o">.</span><span class="na">imageToMat</span><span class="o">(</span><span class="n">img</span><span class="o">)</span>
<span class="n">mat2</span> <span class="o">=</span> <span class="n">mat</span><span class="o">.</span><span class="na">clone</span><span class="o">()</span>
<span class="n">GaussianBlur</span><span class="o">(</span><span class="n">mat</span><span class="o">,</span> <span class="n">mat2</span><span class="o">,</span> <span class="k">new</span> <span class="n">Size</span><span class="o">(</span><span class="mi">15</span><span class="o">,</span> <span class="mi">15</span><span class="o">),</span> <span class="mf">2.0</span><span class="o">)</span>
<span class="n">GaussianBlur</span><span class="o">(</span><span class="n">mat</span><span class="o">,</span> <span class="n">mat</span><span class="o">,</span> <span class="k">new</span> <span class="n">Size</span><span class="o">(</span><span class="mi">15</span><span class="o">,</span> <span class="mi">15</span><span class="o">),</span> <span class="mf">1.0</span><span class="o">)</span>
<span class="n">subtract</span><span class="o">(</span><span class="n">mat</span><span class="o">,</span> <span class="n">mat2</span><span class="o">,</span> <span class="n">mat</span><span class="o">)</span>
<span class="n">mat2</span><span class="o">.</span><span class="na">close</span><span class="o">()</span>
<span class="c1">// Convert Mat to an ImagePlus, setting pixel calibration info & then show it</span>
<span class="kt">def</span> <span class="n">imp</span> <span class="o">=</span> <span class="n">OpenCVTools</span><span class="o">.</span><span class="na">matToImagePlus</span><span class="o">(</span><span class="n">mat</span><span class="o">,</span> <span class="s2">"My image"</span><span class="o">)</span>
<span class="n">IJTools</span><span class="o">.</span><span class="na">calibrateImagePlus</span><span class="o">(</span><span class="n">imp</span><span class="o">,</span> <span class="n">request</span><span class="o">,</span> <span class="n">server</span><span class="o">)</span>
<span class="n">imp</span><span class="o">.</span><span class="na">show</span><span class="o">()</span>
</code></pre></div></div>
<h4 id="manipulating-rois">Manipulating ROIs</h4>
<p>Having met <code class="language-plaintext highlighter-rouge">IJTools</code> and <code class="language-plaintext highlighter-rouge">OpenCVTools</code>, it may be nice to know there are also <code class="language-plaintext highlighter-rouge">RoiTools</code> and <code class="language-plaintext highlighter-rouge">PathObjectTools</code> classes.
In all cases, these contain static methods that may be useful.</p>
<p>Here, we see how to create and merge two ROIs:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">qupath.lib.roi.ROIs</span>
<span class="kn">import</span> <span class="nn">qupath.lib.roi.RoiTools</span>
<span class="kn">import</span> <span class="nn">qupath.lib.objects.PathObjects</span>
<span class="kn">import</span> <span class="nn">qupath.lib.regions.ImagePlane</span>
<span class="kt">def</span> <span class="n">plane</span> <span class="o">=</span> <span class="n">ImagePlane</span><span class="o">.</span><span class="na">getDefaultPlane</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">roi1</span> <span class="o">=</span> <span class="n">ROIs</span><span class="o">.</span><span class="na">createRectangleROI</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="mi">100</span><span class="o">,</span> <span class="mi">100</span><span class="o">,</span> <span class="n">plane</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">roi2</span> <span class="o">=</span> <span class="n">ROIs</span><span class="o">.</span><span class="na">createEllipseROI</span><span class="o">(</span><span class="mi">80</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="mi">100</span><span class="o">,</span> <span class="mi">100</span><span class="o">,</span> <span class="n">plane</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">roi3</span> <span class="o">=</span> <span class="n">RoiTools</span><span class="o">.</span><span class="na">combineROIs</span><span class="o">(</span><span class="n">roi1</span><span class="o">,</span> <span class="n">roi2</span><span class="o">,</span> <span class="n">RoiTools</span><span class="o">.</span><span class="na">CombineOp</span><span class="o">.</span><span class="na">ADD</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">annotation</span> <span class="o">=</span> <span class="n">PathObjects</span><span class="o">.</span><span class="na">createAnnotationObject</span><span class="o">(</span><span class="n">roi3</span><span class="o">)</span>
<span class="n">addObject</span><span class="o">(</span><span class="n">annotation</span><span class="o">)</span>
</code></pre></div></div>
<h4 id="working-with-java-topology-suite">Working with Java Topology Suite</h4>
<p>It’s quite possible that your ROI manipulation wishes extend beyond what <em>QuPath</em> ROIs support directly.</p>
<p>Fortunately, you can shift to the fabulous <a href="https://github.com/locationtech/jts">Java Topology Suite</a> - rather easily.
Here’s an example that will convert a <em>QuPath</em> <code class="language-plaintext highlighter-rouge">ROI</code> to a JTS <code class="language-plaintext highlighter-rouge">Geometry</code>, expand it, and then create a new annotation from the result:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">qupath.lib.objects.PathObjects</span>
<span class="kn">import</span> <span class="nn">qupath.lib.roi.jts.ConverterJTS</span><span class="o">;</span>
<span class="kt">def</span> <span class="n">roi</span> <span class="o">=</span> <span class="n">getSelectedROI</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">geometry</span> <span class="o">=</span> <span class="n">roi</span><span class="o">.</span><span class="na">getGeometry</span><span class="o">()</span>
<span class="n">geometry</span> <span class="o">=</span> <span class="n">geometry</span><span class="o">.</span><span class="na">buffer</span><span class="o">(</span><span class="mi">100</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">roi2</span> <span class="o">=</span> <span class="n">ConverterJTS</span><span class="o">.</span><span class="na">convertGeometryToROI</span><span class="o">(</span><span class="n">geometry</span><span class="o">,</span> <span class="n">roi</span><span class="o">.</span><span class="na">getImagePlane</span><span class="o">())</span>
<span class="kt">def</span> <span class="n">annotation</span> <span class="o">=</span> <span class="n">PathObjects</span><span class="o">.</span><span class="na">createAnnotationObject</span><span class="o">(</span><span class="n">roi2</span><span class="o">)</span>
<span class="n">addObject</span><span class="o">(</span><span class="n">annotation</span><span class="o">)</span>
</code></pre></div></div>
<h4 id="serialization--json">Serialization & JSON</h4>
<p>QuPath v0.1.2 uses Java’s built-in serialization quite a lot for saving/reloading things.</p>
<p>This is quite compact and easy to use, but horrendous to maintain and impractical for sharing data with anything written in another programming language.
Still, it lives on in .qpdata files… for now.</p>
<p><a href="https://en.wikipedia.org/wiki/JSON">JSON</a>, by contrast, is text-based and readable.
v0.1.2 already used JSON for project files (.qpproj), but now uses it increasingly where possible.</p>
<p>JSON is not always appropriate (e.g. attempting to represent a full object hierarchy containing a million objects as JSON would be horribly slow, complex and memory-hungry) but it is generally more maintainable and portable compared to Java serialization.</p>
<p>The library QuPath uses to help with JSON is <a href="https://github.com/google/gson">Gson</a>.
Gson makes it beautifully straightforward to turn almost anything into a JSON representation and back… <em>if</em> you know exactly what Java class is involved.</p>
<p>Here’s a Groovy example that doesn’t rely on anything QuPath-specific:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">com.google.gson.GsonBuilder</span>
<span class="kt">def</span> <span class="n">gson</span> <span class="o">=</span> <span class="k">new</span> <span class="n">GsonBuilder</span><span class="o">()</span>
<span class="o">.</span><span class="na">setPrettyPrinting</span><span class="o">()</span>
<span class="o">.</span><span class="na">create</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">myMap</span> <span class="o">=</span> <span class="o">[</span><span class="s1">'Hello'</span><span class="o">:</span> <span class="mi">1</span><span class="o">,</span> <span class="s1">'I am a map'</span><span class="o">:</span> <span class="mi">2</span><span class="o">]</span>
<span class="n">print</span> <span class="n">myMap</span>
<span class="kt">def</span> <span class="n">json</span> <span class="o">=</span> <span class="n">gson</span><span class="o">.</span><span class="na">toJson</span><span class="o">(</span><span class="n">myMap</span><span class="o">)</span>
<span class="n">print</span> <span class="n">json</span>
<span class="kt">def</span> <span class="n">myMap2</span> <span class="o">=</span> <span class="n">gson</span><span class="o">.</span><span class="na">fromJson</span><span class="o">(</span><span class="n">json</span><span class="o">,</span> <span class="n">Map</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="n">print</span> <span class="n">myMap2</span>
</code></pre></div></div>
<p>You may notice that the map you get back doesn’t look exactly the same when printed… what looked like an integer may now look like a floating point value.
But otherwise they match.</p>
<p>In practice, when working with generic classes and subclasses things can rapidly become <em>a lot</em> more complex, bringing in the world of type hierarchy adapters and the like.</p>
<p>I have spent a long time battling with these things in the hope that you won’t have to.
Rather than creating your own <code class="language-plaintext highlighter-rouge">Gson</code> object, you can request one from QuPath that is pre-initialized to work with a lot of the kind of structures you’ll meet in QuPath.</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">qupath.lib.io.GsonTools</span>
<span class="kt">boolean</span> <span class="n">prettyPrint</span> <span class="o">=</span> <span class="kc">true</span>
<span class="kt">def</span> <span class="n">gson</span> <span class="o">=</span> <span class="n">GsonTools</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="n">prettyPrint</span><span class="o">)</span>
<span class="n">println</span> <span class="s1">'My ROI'</span>
<span class="n">println</span> <span class="n">gson</span><span class="o">.</span><span class="na">toJson</span><span class="o">(</span><span class="n">getSelectedROI</span><span class="o">())</span>
<span class="n">println</span><span class="o">()</span>
<span class="n">println</span> <span class="s1">'My object'</span>
<span class="n">println</span> <span class="n">gson</span><span class="o">.</span><span class="na">toJson</span><span class="o">(</span><span class="n">getSelectedObject</span><span class="o">())</span>
<span class="n">println</span><span class="o">()</span>
<span class="n">println</span> <span class="s1">'My server'</span>
<span class="n">println</span> <span class="n">gson</span><span class="o">.</span><span class="na">toJson</span><span class="o">(</span><span class="n">getCurrentServer</span><span class="o">())</span>
</code></pre></div></div>
<p>To convert back, you’ll need to supply the appropriate QuPath class.
Because of the magic <code class="language-plaintext highlighter-rouge">GsonTools</code> does for you, this doesn’t need to be the <em>exact</em> class - you can use <code class="language-plaintext highlighter-rouge">PathObject</code> and get a detection or annotation as appropriate.</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">qupath.lib.objects.PathObject</span>
<span class="kn">import</span> <span class="nn">qupath.lib.io.GsonTools</span>
<span class="kt">boolean</span> <span class="n">prettyPrint</span> <span class="o">=</span> <span class="kc">true</span>
<span class="kt">def</span> <span class="n">gson</span> <span class="o">=</span> <span class="n">GsonTools</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="n">prettyPrint</span><span class="o">)</span>
<span class="c1">// Get the selected object & convert to JSON</span>
<span class="kt">def</span> <span class="n">pathObject</span> <span class="o">=</span> <span class="n">getSelectedObject</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">json</span> <span class="o">=</span> <span class="n">gson</span><span class="o">.</span><span class="na">toJson</span><span class="o">(</span><span class="n">pathObject</span><span class="o">)</span>
<span class="n">print</span> <span class="n">json</span>
<span class="c1">// Create a NEW object from the JSON representation</span>
<span class="kt">def</span> <span class="n">pathObject2</span> <span class="o">=</span> <span class="n">gson</span><span class="o">.</span><span class="na">fromJson</span><span class="o">(</span><span class="n">json</span><span class="o">,</span> <span class="n">PathObject</span><span class="o">)</span>
<span class="n">print</span> <span class="n">pathObject2</span>
<span class="c1">// Confirm that we really do have a *different* object</span>
<span class="k">if</span> <span class="o">(</span><span class="n">pathObject</span> <span class="o">==</span> <span class="n">pathObject2</span><span class="o">)</span>
<span class="n">print</span> <span class="s1">'Objects are the same'</span>
<span class="k">else</span>
<span class="n">print</span> <span class="s1">'Objects are NOT the same'</span>
<span class="c1">// Add the object to the hierarchy to check it matches, with a name to help</span>
<span class="n">pathObject2</span><span class="o">.</span><span class="na">setName</span><span class="o">(</span><span class="s1">'The one from JSON'</span><span class="o">)</span>
<span class="n">addObject</span><span class="o">(</span><span class="n">pathObject2</span><span class="o">)</span>
</code></pre></div></div>
<p>This should also work for an <code class="language-plaintext highlighter-rouge">ImageServer</code>:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">qupath.lib.images.servers.ImageServer</span>
<span class="kn">import</span> <span class="nn">qupath.lib.io.GsonTools</span>
<span class="kt">boolean</span> <span class="n">prettyPrint</span> <span class="o">=</span> <span class="kc">true</span>
<span class="kt">def</span> <span class="n">gson</span> <span class="o">=</span> <span class="n">GsonTools</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="n">prettyPrint</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">server</span> <span class="o">=</span> <span class="n">getCurrentServer</span><span class="o">()</span>
<span class="kt">def</span> <span class="n">json</span> <span class="o">=</span> <span class="n">gson</span><span class="o">.</span><span class="na">toJson</span><span class="o">(</span><span class="n">getCurrentServer</span><span class="o">())</span>
<span class="kt">def</span> <span class="n">server2</span> <span class="o">=</span> <span class="n">gson</span><span class="o">.</span><span class="na">fromJson</span><span class="o">(</span><span class="n">json</span><span class="o">,</span> <span class="n">ImageServer</span><span class="o">)</span>
<span class="n">print</span> <span class="n">server</span>
<span class="n">print</span> <span class="n">server</span><span class="o">.</span><span class="na">class</span>
<span class="n">print</span> <span class="n">server2</span>
<span class="n">print</span> <span class="n">server2</span><span class="o">.</span><span class="na">class</span>
<span class="nf">if</span> <span class="o">(</span><span class="n">server</span> <span class="o">==</span> <span class="n">server2</span><span class="o">)</span>
<span class="n">print</span> <span class="s1">'Servers ARE the same'</span>
<span class="k">else</span>
<span class="n">print</span> <span class="s1">'Servers ARE NOT the same'</span>
<span class="k">if</span> <span class="o">(</span><span class="n">server</span><span class="o">.</span><span class="na">getMetadata</span><span class="o">()</span> <span class="o">==</span> <span class="n">server2</span><span class="o">.</span><span class="na">getMetadata</span><span class="o">())</span>
<span class="n">print</span> <span class="s1">'Metadata IS the same'</span>
<span class="k">else</span>
<span class="n">print</span> <span class="s1">'Metadata IS NOT the same'</span>
</code></pre></div></div>
<p>Figuring out a JSON way to represent ImageServers has taken up rather a lot of my time recently… but so far this seems to be working.</p>
<p>Note however that <em>not everything can be converted to JSON</em>.
For example, you can’t do this with an object hierarchy or an <code class="language-plaintext highlighter-rouge">ImageData</code>.
You also probably don’t/shouldn’t want to, given the efficiency issues mentioned above.</p>
<p>Nevertheless, where possible QuPath tries to use a representation that may be used elsewhere.</p>
<p>For example, for ROIs and objects, QuPath follows the <a href="https://en.wikipedia.org/wiki/GeoJSON">GeoJSON</a> specification.
This should (although I haven’t tried…) make it possible to exchange regions with other software, e.g. get them into Python via <a href="https://shapely.readthedocs.io/en/stable/manual.html">Shapely</a>.</p>
<blockquote>
<p>Using GeoJSON does impose some limitations; notably, ellipses become polygons.</p>
</blockquote>
<p><code class="language-plaintext highlighter-rouge">GsonTools</code> also aims to wrap around OpenCV’s JSON serialization, e.g.</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">org.bytedeco.opencv.global.opencv_core</span>
<span class="kn">import</span> <span class="nn">org.bytedeco.opencv.opencv_core.Mat</span>
<span class="kn">import</span> <span class="nn">qupath.lib.io.GsonTools</span>
<span class="kt">boolean</span> <span class="n">prettyPrint</span> <span class="o">=</span> <span class="kc">true</span>
<span class="kt">def</span> <span class="n">gson</span> <span class="o">=</span> <span class="n">GsonTools</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="n">prettyPrint</span><span class="o">)</span>
<span class="kt">def</span> <span class="n">mat</span> <span class="o">=</span> <span class="n">Mat</span><span class="o">.</span><span class="na">eye</span><span class="o">(</span><span class="mi">3</span><span class="o">,</span> <span class="mi">3</span><span class="o">,</span> <span class="n">opencv_core</span><span class="o">.</span><span class="na">CV_32FC1</span><span class="o">).</span><span class="na">asMat</span><span class="o">()</span>
<span class="n">print</span> <span class="n">gson</span><span class="o">.</span><span class="na">toJson</span><span class="o">(</span><span class="n">mat</span><span class="o">)</span>
</code></pre></div></div>
<p>Eventually this will make OpenCV classifiers JSON-serializable within QuPath and finally <a href="https://github.com/qupath/qupath/issues/3430">avoid needing to retrain existing classifiers when reloading them</a>.</p>
<h3 id="what-next">What next?</h3>
<p>This post gives an overview of QuPath scripting for v0.2.0-m4.
The API has changed considerably before… albeit with quite a lot of resemblance.</p>
<p>The goal is to make everything more logical and easier to extend.
Scripting <em>should</em> be intuitive, and allow you do interact with the data in whatever way you like.
Admittedly there is more work to do to achieve this… but it’s a start.</p>
<p>For the main classes you’ll need, it should be possible to at least guess their names.
For example, you should avoid using direct constructors for <code class="language-plaintext highlighter-rouge">PathObjects</code> and <code class="language-plaintext highlighter-rouge">ROIs</code> and use the static classes instead.
Similarly, if you see <code class="language-plaintext highlighter-rouge">Tools</code> and the end of a classname you can be fairly sure it contains more static methods useful for manipulating objects of whatever type the classname begins with.</p>
<p>Here’s a list of some classes you might want to import, and their current locations:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">qupath.imagej.gui.IJExtension</span>
<span class="kn">import</span> <span class="nn">qupath.imagej.tools.IJTools</span>
<span class="kn">import</span> <span class="nn">qupath.lib.gui.scripting.QPEx</span>
<span class="kn">import</span> <span class="nn">qupath.lib.images.servers.ImageServer</span>
<span class="kn">import</span> <span class="nn">qupath.lib.io.GsonTools</span>
<span class="kn">import</span> <span class="nn">qupath.lib.objects.PathObjects</span>
<span class="kn">import</span> <span class="nn">qupath.lib.objects.classes.PathClassFactory</span>
<span class="kn">import</span> <span class="nn">qupath.lib.objects.classes.PathClassTools</span>
<span class="kn">import</span> <span class="nn">qupath.lib.regions.ImagePlane</span>
<span class="kn">import</span> <span class="nn">qupath.lib.regions.RegionRequest</span>
<span class="kn">import</span> <span class="nn">qupath.lib.roi.ROIs</span>
<span class="kn">import</span> <span class="nn">qupath.lib.roi.jts.ConverterJTS</span>
<span class="kn">import</span> <span class="nn">qupath.opencv.tools.OpenCVTools</span>
</code></pre></div></div>
<p>Lest they move again or you need others, you can find them by searching on GitHub or (much easier) <a href="https://github.com/qupath/qupath/wiki/Advanced-scripting-with-IntelliJ">setting up QuPath with an IDE like IntelliJ</a>.</p>
<p>In writing this post, I already see things in the API that I don’t like and want to refactor soon… and probably will.
When I do, I’ll try to remember to update these scripts.</p>
<p>All of this remains a work-in-progress, but at least now there is some documentation for anyone who wants to script in the meantime.</p>There have now been four milestone releases of QuPath v0.2.0 over the past six months - where milestone means ‘not finished, potentially buggy, but usable enough to be worthwhile’.QuPath v0.2.0-m4 now available2019-08-20T00:00:00+00:002019-08-20T00:00:00+00:00https://petebankhead.github.io/qupath/2019/08/20/fourth-milestone<p><strong>QuPath v0.2.0-m4 is <a href="https://github.com/qupath/qupath/releases/tag/v0.2.0-m4">now online</a>!</strong></p>
<p>As usual, <em>many changes</em> while moving towards the next release. Here’s an overview of the main ones.</p>
<blockquote>
<p>Please report bugs on <a href="https://github.com/qupath/qupath/issues">GitHub</a> - and for <em>all other questions or discussions</em> please use <a href="https://forum.image.sc/tags/qupath">image.sc</a>.</p>
</blockquote>
<h3 id="cell-detection--intensity-measurements">Cell detection & intensity measurements</h3>
<p>The <em>Positive cell detection</em> command is no longer just for hematoxylin & DAB staining.
It can now be used for any brightfield or multiplexed image where ‘positive’ cells can be identified with a single feature.</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/cells-detected.jpg" alt="Cell detection" class="center-image max-width-80 shadow-image" /></p>
<blockquote>
<p>Original TIFF image if <em>LuCa-7color_[13860,52919]_1x1component_data.tif</em> from Perkin Elmer, part of the <a href="https://downloads.openmicroscopy.org">Bio-Formats sample data</a> available under <a href="http://creativecommons.org/licenses/by/4.0/">CC-BY 4.0</a>.</p>
</blockquote>
<p>As part of these changes, both <em>Positive cell detection</em> and <em>Add intensity measurements</em> now use channel names rather than numbers*.</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/cells-histogram.jpg" alt="Cell histograms" class="center-image max-width-60 shadow-image" /></p>
<p>This should make it rather a lot easier to decipher measurements later.
The intensity command has also been adjusted to allow measurements (including textures) to be calculated within the cell nucleus only.</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/cells-intensity-dialog.jpg" alt="Nucleus intensities" class="center-image max-width-40 shadow-image" /></p>
<blockquote>
<p>*-Be especially cautious when scripting - <em>Add intensity measurements</em> still uses channel order in scripts.</p>
<p>(Also, please avoid using scripts recorded using old versions of QuPath because of these changes.) -*</p>
</blockquote>
<h3 id="generate-image-pyramids-from-separate-fields-of-view">Generate image pyramids from separate fields of view</h3>
<p><strong>Perhaps the most exiting feature for anyone working with multispectral images…</strong></p>
<p>A <a href="https://gist.github.com/petebankhead/b5a86caa333de1fdcff6bdee72a20abe">new QuPath script</a> is able to merge separate spectrally-unmixed fields of view from a Vectra® Polaris™ system, and output a <a href="http://blog.openmicroscopy.org/file-formats/community/2018/11/29/ometiffpyramid/">multichannel, pyramidal OME-TIFF</a>.</p>
<p>So starting from a single field of view…</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/merging-single.jpg" alt="Single field of view" class="center-image max-width-60 shadow-image" /></p>
<p>…it is possible to merge multiple fields of view…</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/merging-multiple.jpg" alt="Several stitched fields of view" class="center-image max-width-60 shadow-image" /></p>
<p>…even resulting in a full whole slide image.</p>
<p>For this to work it is not necessary for all fields of view to be present.
If part of the image is not scanned the pixel values in all channels will simply be 0.</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/merging-full.jpg" alt="Full stitched image" class="center-image max-width-80 shadow-image" /></p>
<blockquote>
<p>Original images shown above thanks to <a href="https://www.ed.ac.uk/pathology/people/staff-students/sandrine-prost">Dr Sandrine Prost, University of Edinburgh</a>.</p>
</blockquote>
<p>The script is <a href="https://gist.github.com/petebankhead/b5a86caa333de1fdcff6bdee72a20abe"><strong>here</strong></a>.
If it proves useful enough, it might become a built-in feature in a future release.</p>
<h3 id="more-options-when-importing-images-to-projects">More options when importing images to projects</h3>
<p>v0.2.0-m3 introduced a new image import dialog when working with projects, and the ability to drag & drop multiple images for import.</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/importing-full.png" alt="Import dialog" class="center-image max-width-40 shadow-image" /></p>
<p>One of the options is to <em>rotate</em> the image.
The option existed in m3… in m4 it <em>actually works</em>.</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/importing-option-rotate.png" alt="Image type option" class="center-image max-width-60 shadow-image" /></p>
<p>This differs from <em>View → Rotate image</em>, which is really only for visualization. Rotation on import treats the image as if it had been saved in a rotated way (i.e. with coordinates changed accordingly). It is designed to resolve cases, especially with Tissue Microarrays, where the image is oriented unexpectedly.</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/importing-rotated.jpg" alt="Rotating" class="center-image max-width-60 shadow-image" /></p>
<p>In general, you <em>really</em> need to set the <em>Image type</em> in QuPath before doing any analysis, since some commands (e.g. cell detection) change behavior based on the type.
This <em>can</em> be auto-estimated (you can turn this on/off in the preferences), but it’s wise to take control since the estimate might be wrong.</p>
<p>You could always set the type later by double-clicking on the entry under the ‘Image’ tab:</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/importing-image-type.png" alt="Image type" class="center-image max-width-60 shadow-image" /></p>
<p>Now you can - and probably should - specify the type in the import dialog.</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/importing-option-type.png" alt="Image type option" class="center-image max-width-60 shadow-image" /></p>
<p>You can also now specify the ‘server type’ or ‘image provider’ (same thing).
Basically, this is the library that reads the pixels and image metadata, e.g. OpenSlide or Bio-Formats.</p>
<p>Previously, QuPath would make the decision for you but this wouldn’t always be optimal because some formats are supported by multiple readers in different ways.
For example, an .ndpi file might open faster using OpenSlide - but if it’s a z-stack then you’d need to use Bio-Formats to access all the slices.</p>
<p>Now, if you know what you want you can take control by specifying the image reading library during import.
(Choose wrong and the image might not open… but choose appropriately and it may open more quickly and more reliably. Or just let QuPath choose as before.)</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/importing-option-provider.png" alt="Server option" class="center-image max-width-60 shadow-image" /></p>
<p>Finally, QuPath is designed to handle very large, ‘pyramidal’ images. Such images contain data at with multiple resolutions stored in the same file.</p>
<p>QuPath is also designed for smaller microscopy images (<em>mostly</em> 2D).</p>
<p>But until now, it hasn’t worked particularly will with large images that aren’t pyramidal.
To mitigate this, there’s now an option for QuPath to generate a ‘pseudo-pyramid’ if needed.</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/importing-option-pyramids.png" alt="Pyramidalize" class="center-image max-width-60 shadow-image" /></p>
<p>QuPath <em>does</em> still need to access all the full-resolution pixels at least once.
Still, you can use this to substantially improve performance for moderately large images, e.g. 10,000 x 10,000 pixels.
For even bigger non-pyramidal images conversion to a pyramidal OME-TIFF is a better idea.</p>
<h3 id="repair-projects-by-finding-missingmoved-images">Repair projects by finding missing/moved images</h3>
<p>QuPath’s projects contain the following file paths (as <a href="https://en.wikipedia.org/wiki/Uniform_Resource_Identifier">URIs</a>):</p>
<ul>
<li>Absolute paths to all images</li>
<li>Absolute path to the project itself</li>
</ul>
<p>That should all work fine so long as your images stay in <em>exactly</em> the same place.</p>
<p>The trouble is if images are moved or deleted, or you want to share a project with someone else who can’t access the images in the same way.
Previously, this meant that in QuPath you needed to manually edit the <code class="language-plaintext highlighter-rouge">project.qpproj</code> file to make things work again.</p>
<p>This started to improve in v0.2.0-m3.
Now whenever a project is being opened, QuPath will:</p>
<ul>
<li>Check if all the absolute image paths match real files</li>
<li>If a file is missing
<ul>
<li>Check if the project has moved, and if so try relative paths to the missing images</li>
<li>Show a dialog for the user to fix any remaining broken paths / accept suggested changes</li>
</ul>
</li>
</ul>
<p>When the dialog appears you can fix a path by double-clicking on it and locating the missing file.</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/fixing-m3.png" alt="Fixing images in m3" class="center-image max-width-60 shadow-image" /></p>
<p>In practice, this only actually worked on Windows.
In v0.2.0-m4 it should work on Linux and Mac as well - but it is still rather awkward if you have a lot of images.</p>
<p>So now there’s a new button ‘Search’.
This is for cases where multiple files are missing (perhaps all of them?) and you need to update many paths at once.</p>
<p>You can click on ‘Search’ and choose a <em>directory</em> containing the missing images.
QuPath will then try to find all the files in the directory with names that correspond to those that are missing.</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/fixing-m4.png" alt="Fixing images in m4" class="center-image max-width-60 shadow-image" /></p>
<p>But actually it can be a little more useful than that.</p>
<p>You don’t <em>really</em> need to give it the directory that contains the images; you can also give QuPath a ‘parent’ directory.
QuPath will then search recursively through sub-directories (up to a limit, currently set to 8 levels) for missing images.</p>
<p>This should ease the pain of working with QuPath projects across multiple computers, or when files move around.</p>
<h3 id="new-ways-to-expand-lines-including-tumor-margins">New ways to expand lines (including tumor margins!)</h3>
<p>An earlier post on this blog discussed how to <a href="https://petebankhead.github.io/qupath/scripts/2018/08/08/three-regions.html">Create annotations around the tumor</a>.</p>
<p>Basically, QuPath gives options to grow, shrink, add and subtract areas, and by combining these it is possible to help analysis around tumor margins etc.</p>
<p>This worked fine for areas, but wasn’t really designed to work with lines.</p>
<p>The new <em>Polyline</em> tool introduced in previous milestone releases helps:</p>
<p><img src="https://user-images.githubusercontent.com/4690904/62520963-8cae1e00-b826-11e9-9e21-4cbc94065b33.jpg" alt="Line" class="center-image max-width-60 shadow-image" /></p>
<p>However, applying the <em>Expand annotations</em> to line/polyline annotations would expand in all directions - and ‘remove interior’ had no effect (since the line had no thickness):</p>
<p><img src="https://user-images.githubusercontent.com/4690904/62520803-38a33980-b826-11e9-946a-c3365e251bd2.jpg" alt="Rounded" class="center-image max-width-60 shadow-image" /></p>
<p>This meant that if you want a ‘thick’ boundary, you would need to carefully draw a line along the <em>middle</em> of where you want the boundary to be - rather than at the edge of the structure being annotated.</p>
<p>Now, in v0.2.0-m4 two things have changed:</p>
<ul>
<li>There is an option to set the ‘line cap’ to be round (as before), flat or square</li>
<li>If selected, ‘Remove interior’ will remove an approximately 1-pixel thick line</li>
</ul>
<p>Changing the line cap to <em>Flat</em> gives a quite different expansion:</p>
<p><img src="https://user-images.githubusercontent.com/4690904/62521003-a9e2ec80-b826-11e9-81b8-a16db61769af.jpg" alt="Flat" class="center-image max-width-60 shadow-image" /></p>
<p>Combined with ‘Remove interior’, this is enough to make a separation.</p>
<p>You can then use <em>Objects → Annotations → Split annotations</em>…</p>
<p><img src="https://user-images.githubusercontent.com/4690904/62521123-f1697880-b826-11e9-965e-12510c734579.jpg" alt="Split" class="center-image max-width-60 shadow-image" /></p>
<p>…which allows you to separate the inner/outer expansion regions.</p>
<p><img src="https://user-images.githubusercontent.com/4690904/62521078-d72f9a80-b826-11e9-8a1b-cc1293b44d60.jpg" alt="After split" class="center-image max-width-60 shadow-image" /></p>
<p>Then you can delete one if you don’t want it, and refine any weird bits that might have emerged (like at the left side on this image).</p>
<h3 id="more-control-of-measurement-maps">More control of Measurement Maps</h3>
<p>The perceptually uniform colormaps added in v0.2.0-m3 can now be accessed from a drop-down menu.</p>
<p>Your preferred colormap is also remembered across measurements & when QuPath is restarted.</p>
<p>You can also now double click on the labels that show the min/max display range to specify exact values.
This is particularly useful if you want to create figures with the same display range for comparison.</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/measurement-maps.jpg" alt="Measurement maps" class="center-image max-width-80 shadow-image" /></p>
<h3 id="improved-memory-management--image-caching">Improved memory management & image caching</h3>
<p>Under the hood, QuPath’s method of caching tiles has changed substantially (now using a <a href="https://github.com/google/guava/wiki/CachesExplained">Guava cache</a>).</p>
<p>Also, the amount of memory given to tile caching is now adjustable in the preferences.</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/memory-pref.png" alt="Tile cache preference" class="center-image max-width-40 shadow-image" /></p>
<p>Previously, this was limited to 4 GB.
This was plenty for most RGB images, but with multiplexed images it could fill up quickly.
Now if you have a lot of RAM available, it’s possible to improve performance by having a much larger tile cache… which also helps when generating the OME-TIFFs as described above.</p>
<h3 id="many-more-improvements-and-bug-fixes">Many more improvements and bug fixes</h3>
<p>There are lots of other changes and fixes, including:</p>
<ul>
<li>You can access scripts from a project directly through the <em>Automate</em> menu again</li>
<li><em>Send region to ImageJ</em> with overlay now only sends objects in the cropped field of view being sent (rather than lots of objects from anywhere in the image)</li>
<li>The font size can be adjusted in the preferences for the cursor location label</li>
<li>QuPath allows smaller regions to be extracted from images (down to a single pixel) - which helps with <em>Send region to Image</em> or adding intensity features</li>
<li>Code formatting in the script editor is asynchronous… which can be considered better or worse. It crashes less anyway.</li>
<li>A bug preventing SLIC superpixels from being generated as been fixed</li>
</ul>
<p>For full details on what has changed, see the <a href="https://github.com/qupath/qupath/commits/master">commit log</a>.</p>
<p><img src="https://petebankhead.github.io/assets/images/m4/viewer-font.jpg" alt="Viewer font size" class="center-image max-width-60 shadow-image" /></p>QuPath v0.2.0-m4 is now online!QuPath v0.2.0-m3 now available2019-07-25T00:00:00+00:002019-07-25T00:00:00+00:00https://petebankhead.github.io/qupath/2019/07/25/third-milestone<blockquote>
<p>Originally posted at <a href="https://forum.image.sc/t/third-milestone-on-the-path-to-qupath-v0-2-0/27953">forum.image.sc</a> and even more originally tweeted <a href="https://twitter.com/QuPath/status/1153748322471088128">here</a></p>
</blockquote>
<p><strong>QuPath v0.2.0-m3 is <a href="https://github.com/qupath/qupath/releases/tag/v0.2.0-m3">now online</a>!</strong></p>
<p>There are <em>many changes</em>, mostly below the surface - but quite a few above the surface too.</p>
<p>The biggest differences involve projects, which have been completely revised. It’s now easier to import images (just drag & drop multiple files) & QuPath tries harder to help resolve broken paths (e.g. when your images move).</p>
<!--  -->
<p><img src="https://aws1.discourse-cdn.com/business4/uploads/imagej/original/3X/8/6/86d6794f626e8c8c38eb31db52b733f275607520.png" alt="Importing images" class="center-image shadow-image max-width-40" />
<img src="https://aws1.discourse-cdn.com/business4/uploads/imagej/original/3X/5/8/5831b66c3d7d07633b90d556721dd5c41d12d6ea.png" alt="Fixing URIs" class="center-image shadow-image max-width-40" /></p>
<p>You can finally set pixel sizes by double-clicking the values under the ‘Image’ tab - optionally with a line or area drawn to help figure out the calibration</p>
<blockquote>
<p>Sidenote: you’ll need to be using a project to store the changes…</p>
</blockquote>
<p><img src="https://aws1.discourse-cdn.com/business4/uploads/imagej/original/3X/d/7/d77b036f33d0f17d36294ccc2520f788b2d7ed95.jpeg" alt="Setting pixels by area" class="center-image shadow-image max-width-60" />
<img src="https://aws1.discourse-cdn.com/business4/uploads/imagej/optimized/3X/3/6/369b7b982754e68e8c694b67575b34d0f5c36c03_2_552x332.jpeg" alt="Setting pixels by line" class="center-image shadow-image max-width-60" /></p>
<p>A major reason to update projects was to add ‘fancy’ images in the future, which aren’t stored directly in a single file but might need to be calculated dynamically (e.g. by applying some kind of transform). This could be to crop regions, concatenate channels, normalize colors…</p>
<p><em>Create sparse image from project</em> is a first example. It generates a new image consisting of regions extracted from other images in a project (all the ones with an annotation classified as ‘Region*’). I expect this to be useful one day… maybe to train a pixel classifier.</p>
<p><img src="https://aws1.discourse-cdn.com/business4/uploads/imagej/optimized/3X/d/6/d6fc17e6cc540e165f357bb87c3fc5e8af964ce4_2_552x272.jpeg" alt="Sparse images" class="center-image shadow-image max-width-60" /></p>
<p>Speaking of which, the pixel classifier remains a work-in-progress. Not yet save-able or scriptable, it has been extensively revised & can calculate some new features (including some in 3D!) on its path to being useful.</p>
<p><img src="https://aws1.discourse-cdn.com/business4/uploads/imagej/original/3X/2/f/2fa7b21c5d3448133c0e23e8a31ad5bf0eacb167.png" alt="Feature selection" class="center-image shadow-image max-width-40" /></p>
<p><img src="https://aws1.discourse-cdn.com/business4/uploads/imagej/original/3X/0/d/0daa2b9307dfb72cd02e9d4700cbd480606cf9ab.jpeg" alt="Viewing features" class="center-image shadow-image max-width-60" /></p>
<p>New, perceptually uniform colormaps have also arrived (thanks go <a href="https://bids.github.io/colormap/">here</a>):</p>
<p><img src="https://aws1.discourse-cdn.com/business4/uploads/imagej/original/3X/0/b/0b7f0212ec9fd03c1168506718d1306726d2c5a6.jpeg" alt="Viridis" class="center-image shadow-image max-width-60" /></p>
<p><img src="https://aws1.discourse-cdn.com/business4/uploads/imagej/optimized/3X/5/a/5a7b84af808e451b3c3eb2d459c08939b45118db_2_552x355.jpeg" alt="Magma (I think)" class="center-image shadow-image max-width-60" /></p>
<p>There’s more control when you send images to <strong>ImageJ</strong> - including z-stacks</p>
<p><img src="https://aws1.discourse-cdn.com/business4/uploads/imagej/optimized/3X/a/0/a088693ab86c5479c908e28f0cf381d796941333_2_552x332.jpeg" alt="Sending to ImageJ" class="center-image shadow-image max-width-60" /></p>
<p>Bio-Formats v6.2.0 is included - which means improved WSI support thanks to the the Bio-Formats team & Glencoe Software. QuPath’s ability to write pyramidal OME-TIFF images is also much improved (albeit still mostly accessible by scripting).</p>
<p><img src="https://aws1.discourse-cdn.com/business4/uploads/imagej/optimized/3X/3/a/3aa929b4ead742d3e07c40f26a5c410138bac251_2_552x267.jpeg" alt="Exporting OME-TIFF" class="center-image shadow-image max-width-60" /></p>
<p>The brush & wand are better. Previously, exuberant brushing would result in lots of disconnected circles - now things are smoother. Also, with a compatible graphics tablet the Wand is pressure sensitive - and both tools behave a better with touchscreens.</p>
<p><strong>v0.2.0-m2</strong>
<img src="https://aws1.discourse-cdn.com/business4/uploads/imagej/optimized/3X/4/e/4ef474e35746e83af146a8571d26e29909bade13_2_479x399.jpeg" alt="Brushing in m2" class="center-image shadow-image max-width-60" /></p>
<p><strong>v0.2.0-m3</strong>
<img src="https://aws1.discourse-cdn.com/business4/uploads/imagej/original/3X/c/7/c72a5ae33432f0527ad73fd9e6663161fdafff02.jpeg" alt="Brushing in m3" class="center-image shadow-image max-width-60" /></p>
<p>And lastly, especially for coders, there’s a first implementation of a GsonTools class to help convert various QuPath-related things to & from a JSON representation. Here, it’s applied to convert a QuPath object into a GeoJSON feature.</p>
<p><img src="https://aws1.discourse-cdn.com/business4/uploads/imagej/original/3X/1/b/1b1aede0dd742a8652172a2a54627c0afaaed2aa.png" alt="GeoJSON" class="center-image shadow-image max-width-60" /></p>Originally posted at forum.image.sc and even more originally tweeted here