Skip to content

Instantly share code, notes, and snippets.

@petebankhead
Created February 20, 2018 20:11
Show Gist options
  • Select an option

  • Save petebankhead/2ef44747f979607e23f50f278aeffd22 to your computer and use it in GitHub Desktop.

Select an option

Save petebankhead/2ef44747f979607e23f50f278aeffd22 to your computer and use it in GitHub Desktop.
Script to combine the measurement tables exported by QuPath into a single text file
/**
* Script to combine results tables exported by QuPath.
*
* This is particularly intended to deal with the fact that results tables of annotations can produce results
* with different column names, numbers and orders - making them awkward to combine later manually.
*
* It prompts for a directory containing exported text files, and then writes a new file in the same directory.
* The name of the new file can be modified - see the first lines below.
*
* Note: This hasn't been tested very extensively - please check the results carefully, and report any problems so they
* can be fixed!
*
* @author Pete Bankhead
*/
import qupath.lib.gui.QuPathGUI
// Some parameters you might want to change...
String ext = '.txt' // File extension to search for
String delimiter = '\t' // Use tab-delimiter (this is for the *input*, not the output)
String outputName = 'Combined_results.txt' // Name to use for output; use .csv if you really want comma separators
// Prompt for directory containing the results
def dirResults = QuPathGUI.getSharedDialogHelper().promptForDirectory()
if (dirResults == null)
return
def fileResults = new File(dirResults, outputName)
// Get a list of all the files to merge
def files = dirResults.listFiles({
File f -> f.isFile() &&
f.getName().toLowerCase().endsWith(ext) &&
f.getName() != outputName} as FileFilter)
if (files.size() <= 1) {
print 'At least two results files needed to merge!'
return
} else
print 'Will try to merge ' + files.size() + ' files'
// Represent final results as a 'list of maps'
def results = new ArrayList<Map<String, String>>()
// Store all column names that we see - not all files necessarily have all columns
def allColumns = new LinkedHashSet<String>()
allColumns.add('File name')
// Loop through the files
for (file in files) {
// Check if we have anything to read
def lines = file.readLines()
if (lines.size() <= 1) {
print 'No results found in ' + file
continue
}
// Get the header columns
def iter = lines.iterator()
def columns = iter.next().split(delimiter)
allColumns.addAll(columns)
// Create the entries
while (iter.hasNext()) {
def line = iter.next()
if (line.isEmpty())
continue
def map = ['File name': file.getName()]
def values = line.split(delimiter)
// Check if we have the expected number of columns
if (values.size() != columns.size()) {
print String.format('Number of entries (%d) does not match the number of columns (%d)!', columns.size(), values.size())
print('I will stop processing ' + file.getName())
break
}
// Store the results
for (int i = 0; i < columns.size(); i++)
map[columns[i]] = values[i]
results.add(map)
}
}
// Create a new results file - using a comma delimiter if the extension is csv
if (outputName.toLowerCase().endsWith('.csv'))
delimiter = ','
int count = 0
fileResults.withPrintWriter {
def header = String.join(delimiter, allColumns)
it.println(header)
// Add each of the results, with blank columns for missing values
for (result in results) {
for (column in allColumns) {
it.print(result.getOrDefault(column, ''))
it.print(delimiter)
}
it.println()
count++
}
}
// Success! Hopefully...
print 'Done! ' + count + ' result(s) written to ' + fileResults.getAbsolutePath()
@DavidMHaumann
Copy link

awesome Pete!

I tried to change your script in a way, that it reads the txt allways from a project subfolder "measurements".
Therefore i subsituted
def dirResults = QuPathGUI.getSharedDialogHelper().promptForDirectory()

by
def dirResults = buildFilePath(PROJECT_BASE_DIR, "measurements")

but this will always cause a problem in line 30 with the following message:

INFO: Starting script at Fri Feb 23 10:09:36 GMT+01:00 2018
ERROR: Error at line 30: No signature of method: java.lang.String.listFiles() is applicable for argument types: (com.sun.proxy.$Proxy6) values: [Script168$_run_closure1@2ea51fa6]

ERROR: Script error
at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:58)
at org.codehaus.groovy.runtime.callsite.PojoMetaClassSite.call(PojoMetaClassSite.java:49)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:125)
at Script168.run(Script168.groovy:41)
at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:343)
at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:152)
at qupath.lib.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:765)
at qupath.lib.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:695)
at qupath.lib.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:677)
at qupath.lib.scripting.DefaultScriptEditor.access$400(DefaultScriptEditor.java:136)
at qupath.lib.scripting.DefaultScriptEditor$2.run(DefaultScriptEditor.java:1029)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)

Is it possible to run this fantastic script for a defined subfolder in the project folder?
This would avoid the manual step and thus be more convenient.

Best
David

@petebankhead
Copy link
Author

No problem - to list the contents of the directory you need a File, and not a String representing only the path to a file. But it's straightforward to create one from the other.

This should work:

def pathResults = buildFilePath(PROJECT_BASE_DIR, "measurements")
def dirResults = new File(pathResults)

@DavidMHaumann
Copy link

yes it works. Fantastic! Thank you Pete.

@powersas88
Copy link

I keep getting: as far as I can tell I don't customize the script. I just want straight forward exportation of the data in a combined txt instead of 8 different files

INFO: Will try to merge 8 files
ERROR: Error at line 49: No such property: fileResults for class: Script41

ERROR: Script error
at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:65)
at org.codehaus.groovy.runtime.callsite.PogoGetPropertySite.getProperty(PogoGetPropertySite.java:51)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callGroovyObjectGetProperty(AbstractCallSite.java:309)
at Script41.run(Script41.groovy:50)
at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:317)
at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:155)
at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:767)
at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:697)
at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:677)
at qupath.lib.gui.scripting.DefaultScriptEditor$2.run(DefaultScriptEditor.java:1034)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)

@petebankhead
Copy link
Author

@powersas88 I've no idea... the error reports an issue with fileResults not existing at line 49, but fileResults isn't used around line 49 in my script - and anyway it should already have been defined at line 27. So it looks like the script you're running differs in some way...?

I'd need to see exactly the script you run from the script editor to be able to help further.

@powersas88
Copy link

powersas88 commented Jun 13, 2019 via email

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