Measurement export and potentially missing columns

Hello!

I am trying to use the MeasurementsExporter as per the example here
https://qupath.readthedocs.io/en/latest/docs/tutorials/exporting_measurements.html

One issue we have been faced with is when exporting counts of different CellObject classes as a result from a multiplex analysis like the one shown here:
https://qupath.readthedocs.io/en/latest/docs/tutorials/multiplex_analysis.html

Because the MeasurementExporter only exports measurements which exist, this causes different column numbers and orders when exporting results from different projects, in the case that some categories do not exist.

For example, we are trying to classify cells into 3 classes: A, B and C
Each cell could be A, B, AB, AC, BC or ABC

Suppose that for one project with some settings , we get all classes represented, but for a second project other classes are missing (which is an interesting phenotype)

Now the results tables have a different number of columns, which makes concatenation of these difficult.

Would it be possible to make it so that if the column name is specified explicitely by the user (using includeOnlyColumns), then it must appear in the results table, even if it does not exist? This would make downstream processing easier.
Otherwise, what workaround could we use to define this statistic and ensure it is always present for export?

I know we could try to put everything into a single project but this is not convenient when running a new analysis with new data. However it would be nice to be able to directly compare the results without having to resort to table manipulation in another software.

Thanks for any insight

Oli

@melvingelbard wrote the measurement exporter so knows much more about it than me, although just to be sure are you referring primarily to supporting this via the UI or scripting (or either would do)?

The export with specific columns might already be scriptable, but I’m not certain and don’t know if that meets your requirements anyway. I imagine adding support to the UI is a bit more awkward, since it would require providing another way to enter column headings… but should be possible.

In any case, script-free workarounds I can think of are:

  • Merge to the same project as you suggest (which is simplified by the fact that Project… → Add images also supports projects)
  • Add just one (pseudo?)-image to each project that contains all columns – thereby making them available when the ‘Populate’ button is pressed

A variation on this old script might work for the merging, since as far as I recall it’s pretty much just Groovy – and doesn’t require any QuPath-specific bits:

1 Like

For me it is through scripting only, as through the UI as it is now, we cannot choose column names other than the ones obtainable via ‘Populate’ and I do not think a user going through to the GUI would like to hand-write a column name…

It is definitely scriptable and awesome but it ignores the defined column if it does not exist (rather than, for example, keep the column and leave a NaN or null as value, hence the discussion :slight_smile:)

I like the idea of having a ‘pseudo-image’ containing all possible columns, and I will also try to add the measurement directly before export, but an option in the builder like forceColumnNames(true) could be excellent.

Best

Oli

1 Like

Thanks Oli, makes sense – I defer to @melvingelbard’s export knowledge for any code changes.

Re. the workaround, here’s a truly terrible hack that might be applied to one image to make the measurements available (I haven’t tested it for anything serious)…

def columns = """
First measurement
Second measurement
Something else
"""

def empty = PathObjects.createAnnotationObject(ROIs.createRectangleROI(0, 0, 1, 1, ImagePlane.getDefaultPlane()))
empty.setName('Ignore me')
ml = empty.getMeasurementList()
for (col in columns.split('\n'))
    if (col)
        ml.putMeasurement(col, Double.NaN)

addObject(empty)
1 Like

Hi Pete,

Thanks for the script. I was going to write something for each object but making a single object makes sense too

def forceColumnNames( def objects, def measurements ) {
    objects.each{ o ->
        measurements.each{ m->
            if ( measurement( o, m ) == Double.NaN ) o.getMeasurementList().putMeasurement(m, 0)
        }
    }

I noticed that doing this actually changes the column order in the exported results. It does not follow the order provided as String[] in the includeOnlyColumns() builder method. I will open a bug report and provide a minimum example.

Thanks for everything already and I will wait for @melvingelbard’s comments.

EDIT: Replaced ==null with ==Double.NaN as my code was not performing as advertised…

Ah well… I had a suspicion ordering might be an issue after posting that last script.

I think the column order is just undefined currently… it doesn’t make any effort to maintain the order. This probably shouldn’t be the case, especially since the need to merge results is becoming increasingly clear (I suspect the limitation predates the measurement exporter & was my fault originally).

Thinking about it a bit more, I can’t come up with a reason why I’d ever want forceColumnNames(false)… so ultimately it would make sense to redefine the method so that the included columns are always including and always in order.

If there is any benefit in QuPath stripping empty columns, then that could be an additional option – but by default it is turned off.

What do you think?

Ultimately I think exporting ought to be revisited for v0.3.0, including this as part of it.

1 Like

Hi Pete,

Yes I saw the post and I agree.

I agree, and with your comment on merging results being an important aspect as well.

1 Like

Is it possible to export custom metadata property column for images while using the measurement exporter ? I have added metada named “genotype” per image to group images in the project. It would be great if that show up in one of the column.

I am not sure if this would be useful for others and worth coding in as a feature in measurement exporter.

2 Likes

Hi all,

Thanks for the feedback! It seems sensible to always include the columns specified in includeOnlyColumns(). I will have a look into that soon.
Unfortunately, the method is linked to other parts of the code (such as some methods in QP…) so I want to be sure that no other relevant parts of the code will be negatively affected by some change. I will post an update when I’ll get a closer look at the MeasurementExporter!

2 Likes

This is what I have usually done, and once you finish exporting the data, you can quickly delete the temporary project. It is basically a pointer to each of the real projects. I can definitely see the use case for populating all columns (create a recursive formula using the “base” classes, similar to the multiplex classifier script from a while back), but this is a very quick intermediate fix, since you don’t need to remake the projects!

It’s really nice to be able to quickly grab 50 project files (search on "*.qpproj " from the parent directory), throw them together, and run a quick export script.

1 Like

Is it possible to export custom metadata property column for images while using the measurement exporter ? I have added metada named “genotype” per image to group images in the project. It would be great if that show up in one of the column.

I’ve got a workaround for this:

To extract a list of image metadata values from the project file you can use Download jq (on Windows rename jq-win64.exe to jq.exe and put it in the project directory or add it to the PATH)

Then you can run a command from the terminal to get a list of values, which can be copied into a spreadsheet containing previously exported measurements.

jq ".images[].metadata.genotype" project.qpproj

On Windows you can add this followed by pause in a .bat file too.

This relies on the images in project.qpproj being in the same order as in the export, which does seem to be the case. This is easier to check with another command: jq ".images[] | [.imageName, .metadata.genotype]" project.qpproj

(Btw, if you’re writing a script then custom image metadata can be accessed as getProjectEntry().getMetadataMap()['genotype']. )

1 Like