pythonsubprocessqupath

Python subprocess.run argument passing


I'm using subprocess.run to run a script in QuPath. My python code is below, and the QuPath script is below that. The script runs but the arguments are not passed. The first code block in the script prints the recognized arguments.

When the python code is run, the QuPath script executes successfully but reports no passed arguments, and the script returns because it can't find a project path. If the 'script' argument is placed first, the result is the same. Thoughts appreciated!

Python code:

qupath_executable = "C:/path/to/QuPath executable"
script_path = "C:/path/to/script.groovy"
project_path = "C:/path/to/project.qpproj"

command = [
    qupath_executable,
    "-p", project_path,
    "script", script_path,
]
result = subprocess.run(command, capture_output=True, text=True, shell=True)
if result.returncode == 0:
    print("Script executed successfully.")
    print(result.stdout)
else:
    print("Script execution failed.")
    print(result.stderr)

QuPath Script:

import java.awt.BasicStroke
import java.awt.Graphics2D
import qupath.ext.stardist.StarDist2D
import qupath.lib.images.servers.ImageServer
import qupath.lib.images.servers.ImageServerProvider

// Print all arguments
println "Arguments passed to the script:"
args.eachWithIndex { arg, index ->
    println "Argument ${index}: ${arg}"
}


// Retrieve the project path from the arguments
def projectPath = null
for (int i = 0; i < args.length; i++) {
    println "Examining argument ${i}: ${args[i]}"
    if (args[i] == "-p" && i + 1 < args.length) {
        projectPath = args[i + 1]
        break
    }
}

if (!projectPath) {
    println "Error: Project path not specified."
    return
}

def annotationOutputPath = projectPath + '/Annotations'
def annotatedImageOutputPath = projectPath + '/AnnotatedImages'
new File(annotationOutputPath).mkdirs()
new File(annotatedImageOutputPath).mkdirs()

def pathModel = projectPath + '/Models/he_heavy_augment.pb'

def stardist = StarDist2D.builder(pathModel)
      .threshold(0.5)              // Prediction threshold
      .normalizePercentiles(1, 99) // Percentile normalization
      .pixelSize(0.5)              // Resolution for detection
      .build()

println 'StarDist model initialized.'

def imageFiles = new File(projectPath + '/Images').listFiles().findAll { it.name.endsWith('.png') || it.name.endsWith('.jpg') }

if (imageFiles.isEmpty()) {
    println "No images found in the project directory."
    return
}

imageFiles.each { file ->
    println "Processing image: ${file.name}"
    
    def server = ImageServerProvider.buildServer(file)
    def imageData = server.readImageData()
    
    def pathObjects = getSelectedObjects()
    if (pathObjects.isEmpty()) {
        println "No objects selected for image: ${file.name}"
        return
    }
    println 'Selected objects: ' + pathObjects.size()
    stardist.detectObjects(imageData, pathObjects)
    println 'Object detection complete for image: ' + file.name
    
    def annotations = imageData.getHierarchy().getAnnotationObjects()
    println 'Number of annotations: ' + annotations.size()
    def json = PathIO.writeObjectToJson(annotations)
    def annotationFile = new File(annotationOutputPath, file.name + '_annotations.json')
    annotationFile.text = json
    println 'Annotations saved to ' + annotationFile.getPath()
    
    def region = RegionRequest.createInstance(server.getPath(), 1, 0, 0, server.getWidth(), server.getHeight())
    def img = ImageIO.read(server.readBufferedImage(region))
    def g2d = img.createGraphics()
    g2d.setColor(Color.RED)
    g2d.setStroke(new BasicStroke(2))
    for (annotation in annotations) {
        def roi = annotation.getROI()
        def shape = roi.getShape()
        g2d.draw(shape)
    }
    g2d.dispose()
    def annotatedImageFile = new File(annotatedImageOutputPath, file.name + '_annotated.png')
    ImageIO.write(img, 'PNG', annotatedImageFile)
    println 'Annotated image saved to ' + annotatedImageFile.getPath()
}

println 'Processing complete.'

Solution

  • You are using a not recommended combination of arguments passed to subprocess.run. Usually:


    Posix:

    What is exactly happening with args being a sequence and shell=True is explained in the documentation of Popen. The result is the command like this:

    /bin/sh -c upath_executable "-p" project_path "script" script_path
    

    and the shell man page explains it further:

    If the -c option is present, then commands are read from the first non-option argument command_string. If there are arguments after the command_string, the first argument is assigned to $0 and any remaining arguments are assigned to the positional parameters.

    i.e. "-p" project_path "script" script_p is passed to the shell itself, not to the command.


    Windows:

    feel free to fill in the missing part