-
-
Save petebankhead/2ef44747f979607e23f50f278aeffd22 to your computer and use it in GitHub Desktop.
| /** | |
| * 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() |
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)yes it works. Fantastic! Thank you Pete.
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)
@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.
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