QuPath: Batch convert 16bit images to 8bit

Hi,
I try to batch convert some multichannel 16bit images to 8bit
based on @petebankhead 's solution

and on @mendel 's approach to get access to the display range information

The images have to be displayed individually before aViewer.getImageDisplay() can be used meaningful.

I tried the following script to display the images sequentially and retrieve the display range.

def aQuPath = QPEx.getQuPath()
def aViewer = aQuPath.getViewer()

def project = getProject()

for (entry in project.getImageList()) {
    def name = entry.getImageName()
    print name
    if (name.contains("#1")){
        def imagedata = entry.readImageData()
        aViewer.setImageData(imagedata)
        def aImageDisplay = aViewer.getImageDisplay()
        def aJson = aImageDisplay.toJSON(true)
    }
    continue
}

But I get an error message

The details are

ERROR: QuPath exception: This operation is permitted on the event thread only; currentThread = richscripteditor-1
    at com.sun.glass.ui.Application.checkEventThread(Application.java:441)
    at com.sun.glass.ui.Window.setTitle(Window.java:898)
    at com.sun.javafx.tk.quantum.WindowStage.setTitle(WindowStage.java:504)
    at javafx.stage.Stage$5.invalidated(Stage.java:736)
    at javafx.beans.property.StringPropertyBase.markInvalid(StringPropertyBase.java:110)
    at javafx.beans.property.StringPropertyBase$Listener.invalidated(StringPropertyBase.java:231)
    at com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
    at javafx.beans.binding.StringBinding.invalidate(StringBinding.java:169)
    at com.sun.javafx.binding.BindingHelperObserver.invalidated(BindingHelperObserver.java:52)
    at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:348)
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
    at javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:106)
    at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:113)
    at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:147)
    at qupath.lib.gui.QuPathGUI.fireImageDataChangedEvent(QuPathGUI.java:3932)
    at qupath.lib.gui.QuPathGUI$MultiviewManager.imageDataChanged(QuPathGUI.java:4477)
    at qupath.lib.gui.viewer.QuPathViewer.fireImageDataChanged(QuPathViewer.java:1581)
    at qupath.lib.gui.viewer.QuPathViewer.setImageData(QuPathViewer.java:1522)
    at qupath.lib.gui.viewer.QuPathViewer$setImageData.call(Unknown Source)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:125)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:139)
    at Script47.run(Script47.groovy:13)
    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:926)
    at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:859)
    at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:782)
    at qupath.lib.gui.scripting.DefaultScriptEditor$2.run(DefaultScriptEditor.java:1271)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
    at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at java.base/java.lang.Thread.run(Unknown Source)

Is there anything that I can do in my script to fix this
or is there an alternative solution for my task?

Any help I would be grateful.

Just to clarify, you are expecting different display ranges to be used per image? (IE the meaning of the values in any 8bit image will not be relevant to any other 8bit image)

As for the script, that’s a threading error. I don’t know why there is a threading error, but you can get rid of it by forcing the script to run on only one thread. This may be problematic if you want to do a lot of processing down the line.

Note that the first line has to be the first line.

guiscript = true
def aQuPath = QPEx.getQuPath()
def aViewer = aQuPath.getViewer()

def project = getProject()
def aJson
for (entry in project.getImageList()) {
    def name = entry.getImageName()
    print name
    if (name.contains("779")){
        def imagedata = entry.readImageData()
        aViewer.setImageData(imagedata)
        def aImageDisplay = aViewer.getImageDisplay()
         aJson = aImageDisplay.toJSON(true)
    }
    continue
}
print aJson
1 Like

If you search for

javafx "This operation is permitted on the event thread only"

you should find results and explanations – it’s not QuPath specific, rather something common in JavaFX (and other GUI) applications. The solution is to wrap any code that interacts with the user interface inside

javafx.application.Platform.runLater {
   // Your code here
}

(where adding guiscript = true as the first line is an alternative QuPath-specific hack to tell QuPath to wrap the entire script inside Platform.runLater).

3 Likes

Yes, that’s correct.

1 Like

guiscript = true has done the magic.
Thank you @Research_Associate and @petebankhead

1 Like

Here is a working code snippet

guiscript = true

import com.google.gson.Gson 
import java.lang.reflect.Type
import com.google.gson.reflect.TypeToken

def aQuPath = QPEx.getQuPath()
def aViewer = aQuPath.getViewer()
def project = getProject()
def sb = new StringBuilder()

for (entry in project.getImageList()) {
    def name = entry.getImageName()
    sb << '\t' + "Image:" + '\t' + name + '\n'
    
    def imagedata = entry.readImageData()
    aViewer.setImageData(imagedata)
    def aImageDisplay = aViewer.getImageDisplay()
    def aJson = aImageDisplay.toJSON(false)

    def Gson gson = new Gson()
    def Type type = new TypeToken<List<MyJsonHelperChannelInfo>>(){}.getType()
    def List<MyJsonHelperChannelInfo> helperList = gson.fromJson(aJson, type)
       
    for (MyJsonHelperChannelInfo helper : helperList) {
        sb << '\t' + "channel:" + '\t' + helper.name
        sb << '\t' + "minDisplay:" + '\t' + helper.minDisplay
        sb << '\t' + "maxDisplay:" + '\t' + helper.maxDisplay
        sb << '\n'
    }

    continue
}
print sb.toString()


class MyJsonHelperChannelInfo {
    private String name;
    private Class<?> cls;
    private Float minDisplay;
    private Float maxDisplay;
    private Integer color;
    private Boolean selected;
}

The output is:

Image:	testimage_1
channel:	CY5 (C1)	minDisplay:	390.57016	maxDisplay:	7738.211
channel:	txRed (C2)	minDisplay:	62.97489	maxDisplay:	1107.2192
channel:	FITC (C3)	minDisplay:	55.45194	maxDisplay:	448.21432
channel:	DAPI (C4)	minDisplay:	172.85158	maxDisplay:	7424.9897
Image:	testimage_2
channel:	Channel 1	minDisplay:	109.58243	maxDisplay:	46746.176
Image:	testimage_3
channel:	CY5 (C1)	minDisplay:	301.17197	maxDisplay:	8073.4585
channel:	txRed (C2)	minDisplay:	50.46047	maxDisplay:	1640.4171
channel:	FITC (C3)	minDisplay:	28.25209	maxDisplay:	433.21548
channel:	DAPI (C4)	minDisplay:	198.26634	maxDisplay:	16413.979
Image:	testimage_4
channel:	Channel 1	minDisplay:	103.36117	maxDisplay:	53444.8

Thanks for your help
and have some nice holidays.

2 Likes