Select to view content in your preferred language

Tkinter in ArcMap opens multiple windows, does not update values and executes button code without interaction

739
1
Jump to solution
01-11-2023 07:26 AM
Manu
by
Regular Contributor

I need to accomplish the following in a Python Toolbox for ArcMap:

  1. Calculate NDVI and create a mask using a given threshold value
  2. Ask user to inspect the results. Now, the used must have the option to adjust the threshold value using a slider, or to accept it

I wrote the following code, which should work from the Python terminal in ArcMap (however, I had to adjust the code in order to use it in a Python Toolbox; moreover, I removed other Tools, classes, and functions not related to the issue):

 

### Modules
import os, sys, re, math, time, shutil
import arcpy as ap
import numpy as np
import pandas as pd
import tempfile as tmp
import pickle as pk
import tkinter as tk
from tkinter import ttk
from ast import literal_eval

## Interactive user input related functions
def create_mask(value_array, threshold, blc, x_size, y_size, path = None):
    msk = ap.NumPyArrayToRaster(value_array.astype(float), blc,
                                x_size, y_size,
                                value_to_nodata = 0) 
    out_path = "in_memory/mask" if path is None else path
    msk.save(out_path)

def get_current_value(cv):
    return "{: .2f}".format(cv.get())

def slider_changed(event, cv):
    value_label.configure(text = get_current_value(cv))

def inspect(value_array, blc, x_size, y_size, cv):
    if type(cv) != float:
        thresh = cv.get()
    else:
        thresh = cv
    try:### Here, I had to use try because the Toolbox immediately executed this code and produced errors
        create_mask(value_array, thresh, blc, x_size, y_size)
    except:
        print("Fail.")

### ---------------------------------------------------------------------------
# Mask UAV orthophoto by NDVI (or similar) threshold
class Mask_orthophoto(object):
    def __init__(self):
        """Define the tool (tool name is the name of the class)."""
        self.label = "Mask_orthophoto"
        self.description = "Mask orthophoto according to NDVI or a" + \
            " similar index to remove vegetation."
        self.canRunInBackground = False

    def getParameterInfo(self):
        """Define parameter definitions"""
        mxd = ap.mapping.MapDocument("CURRENT")
        loaded_rasters = [l for l in ap.mapping.ListLayers(mxd) if \
            l.isRasterLayer]
        
        # Input Raster object
        in_raster = ap.Parameter(
            displayName = "Input raster object",
            name = "InRaster",
            datatype = ["GPRasterLayer", "DERasterDataset"],
            parameterType = "Required",
            direction = "Input")
        in_raster.value = loaded_rasters[0].dataSource if \
            len(loaded_rasters) != 0 else None
    
        # Vegetation index
        veg_index = ap.Parameter(
            displayName = "Vegetation index",
            name = "VegIndex",
            datatype = "GPString",
            parameterType = "Required",
            direction = "Input")
        veg_index.filter.list = index_ranges.keys()
        veg_index.value = veg_index.filter.list[0]
        
        # Value threshold
        threshold = ap.Parameter(
            displayName = "Value threshold",
            name = "Threshold",
            datatype = "GPDouble",
            parameterType = "Required",
            direction = "Input")
        threshold.value = 0.2
        threshold.filter.type = "Range"
        threshold.filter.list = index_ranges[veg_index.valueAsText][0]
        
        ## Input raster bands -------------------------------------------------
        ### Blue
        band_0 = ap.Parameter(
            displayName = "Blue band",
            name = "b0",
            datatype = "GPString",
            parameterType = "Required",
            direction = "Input")
        band_0.filter.type = "ValueList"
        
        ### Red
        band_1 = ap.Parameter(
            displayName = "Red band",
            name = "b1",
            datatype = "GPString",
            parameterType = "Required",
            direction = "Input")
        band_1.filter.type = "ValueList"
        
        ### NIR
        band_2 = ap.Parameter(
            displayName = "NIR band",
            name = "b2",
            datatype = "GPString",
            parameterType = "Required",
            direction = "Input")
        band_2.filter.type = "ValueList"
        
        ### Enable/disable parameters based on vegetation index selection
        bands = [w is not None for w in index_ranges[veg_index.valueAsText][2]]
        for p in range(3):
            [band_0, band_1, band_2][p].enabled = bands[p]
            [band_0, band_1, band_2][p].parameterType = ["Optional", \
                                         "Required"][int(bands[p])]
        ## --------------------------------------------------------------------
        
        # Output raster
        out_raster = ap.Parameter(
            displayName = "Output raster",
            name = "OutRaster",
            datatype = "DERasterDataset",
            parameterType = "Required",
            direction = "Output")
        [in_file, in_extension] = os.path.splitext(in_raster.valueAsText)
        out_raster.value = in_file + "_masked" + in_extension
        
        params = [in_raster, veg_index, threshold, band_0, band_1, \
            band_2, out_raster]
        return params

    def isLicensed(self):
        """Set whether tool is licensed to execute."""
        return True

    def updateParameters(self, params):
        """Modify the values and properties of parameters before internal
        validation is performed. This method is called whenever a parameter
        has been changed."""
        ### Select vegetation index and threshold
        veg_idx = params[1].valueAsText
        latching_relay = veg_idx if not "latching_relay" in locals() \
            else latching_relay
        if params[1].altered:
            params[2].filter.list = index_ranges[params[1].valueAsText][0]
            if not latching_relay == veg_idx:
                latching_relay = veg_idx
                params[2].value = index_ranges[veg_idx][1]
        if params[2].altered and not (params[2].filter.list[0] <= \
                params[2].value <= params[2].filter.list[1]):
            min, max = params[2].filter.list[0], params[2].filter.list[1]
            params[2].value = min if params[2].value < min else max
        ### Enable band parameters based on vegetation index selection
        bands = [w is not None for w in index_ranges[veg_idx][2]]
        for n in range(3, 6):
            params[n].enabled = bands[n - 3]
            params[n].parameterType = ["Optional", \
                                        "Required"][int(bands[n - 3])]
        ### Autoselect bands according to wavelengths
        if params[0].value is not None:
            try:
                p_in_raster = params[0].value.dataSource
            except:
                p_in_raster = params[0].valueAsText
            [fname, ext] = os.path.splitext(p_in_raster)
            p_in_raster = fname + ".hdr" if ext == ".bsq" else p_in_raster
            img = spy.open_image(p_in_raster)
            wavelengths = img.bands.centers
            wl_unit = " " + img.bands.band_unit
            filterList = [str(wl) + wl_unit for wl in wavelengths]
            for n in range(3, 6):
                if bands[n - 3]:
                    wl = index_ranges[veg_idx][2][n - 3]
                    delta = list(np.absolute(np.array(wavelengths) - wl))
                    band = delta.index(np.min(delta))
                    params[n].filter.list = filterList
                    if params[n].value not in filterList:
                        params[n].value = str(wavelengths[band]) + wl_unit
        return

    def updateMessages(self, params):
        """Modify the messages created by internal validation for each tool
        parameter.  This method is called after internal validation."""
        return

    def execute(self, params, messages):
        """The source code of the tool."""
        mxd = ap.mapping.MapDocument("CURRENT")
        # Retrieve data set paths
        try:
            p_in_raster = params[0].value.dataSource
        except:
            p_in_raster = params[0].valueAsText
        [fname_in, ext_in] = os.path.splitext(p_in_raster)
        p_in_header = fname_in + ".hdr" if ext_in == ".bsq" else p_in_raster
        p_in_raster = fname_in + ".bsq" if ext_in == ".hdr" else p_in_raster
        
        # Vegetation index parameter
        vIndex = params[1].valueAsText
        
        # Threshold
        threshold = params[2].value
        min, max = params[2].filter.list[0], params[2].filter.list[1]
        
        # Get image information
        ap.AddMessage("Gathering image information...")
        img_hdr = EnviHDR(p_in_raster)
        no_data_value = img_hdr.tags["data_ignore_value"]
        img = spy.open_image(p_in_header)
        wavelengths = img.bands.centers
        wl_unit = " " + img.bands.band_unit
        
        # Get parameters for output raster
        p_out_raster = params[6].valueAsText
        [fname_out, ext_out] = os.path.splitext(p_out_raster)
        pt = ap.Point(img_hdr.tags["map_info"][3],
                        img_hdr.tags["map_info"][4])
        [px_size_x, px_size_y] = img_hdr.tags["map_info"][5:7]
        px_size_x, px_size_y = float(px_size_x), float(px_size_y)
        
        # Read selected channels
        ap.AddMessage("Reading selected channels...")
        band_selection = [params[n].valueAsText for n in range(3, 6) if \
                            index_ranges[vIndex][2][n - 3] is not None]
        b_list = [str(wl) + wl_unit for wl in wavelengths]
        b_indices = [b_list.index(b) for b in band_selection]
        
        # Vegetation index calculation
        ap.AddMessage("Calculating {0}...".format(vIndex))
        vegEq = vegIdxEqs[vIndex]
        bands = [img.read_band(idx) for idx in b_indices]
        
        # Start loop for vegetation index input
        ap.AddMessage("Creating mask for {0} < {1}" \
                        .format(vIndex, threshold))
        mask = vegEq(*bands) < threshold
        inspect(value_array = mask, blc = pt,
                x_size = px_size_x, y_size = px_size_y,
                cv = threshold)
        
        ### Pop-up window to adjust threshold
        # root window
        application_window = tk.Tk()
        application_window.geometry("300x70")
        application_window.resizable(False, False)
        application_window.title("Adjust threshold")
        application_window.columnconfigure(0, weight = 1)
        application_window.columnconfigure(1, weight = 3)
        # slider current value
        current_value = tk.DoubleVar()
        current_value.set(threshold)
        # label for the slider
        slider_label = ttk.Label(
            application_window,
            text = "Adjust {0} threshold:".format(vIndex)
        )
        
        slider_label.grid(
            column = 0,
            row = 0,
            sticky = "w"
        )
        
        #  slider
        slider = ttk.Scale(
            application_window,
            from_ = min,
            to = max,
            orient = "horizontal",
            command = slider_changed,
            variable = current_value
        )
        
        slider.grid(
            column = 1,
            row = 0,
            sticky = "we"
        )
        
        # value label
        value_label = ttk.Label(
            application_window,
            text = get_current_value(cv = current_value)
        )
        
        value_label.grid(
            row = 1,
            column = 1,
            sticky = "n"
        )
        def agree(value_array, blc, x_size, y_size, path, cv):
            if type(cv) != float:
                thresh = cv.get()
            else:
                thresh = cv
            try:
                create_mask(value_array, thresh, blc, x_size, y_size, path = path)
            except:
                print("Fail.")
            application_window.destroy()
        
        ttk.Button(application_window, text = "Inspect threshold", command = inspect(value_array = mask, blc = pt, x_size = px_size_x, y_size = px_size_y, cv = current_value)).grid(
            row = 3,
            column = 0,
            columnspan = 1,
            sticky = "n"
        )
        ttk.Button(application_window, text = "Apply threshold", command = agree(value_array = mask, blc = pt, x_size = px_size_x, y_size = px_size_y, path = p_out_raster, cv = current_value)).grid(
            row = 3,
            column = 1,
            columnspan = 2,
            sticky = "n"
        )
        application_window.mainloop()
        ### End pop-up window
        # Output
        ap.AddMessage("Mask successfully created.")
        return

 

Unfortunately, the Toolbox appears to run code when not asked for (it tried to run the code that should be triggered by the buttons right away). Also it crashes when I include the buttons. If I comment out the buttons and include only the slider-related code, ArcMap opens multiple slider instances (none of which updates the slider value).

What is the issue with ArcMap (version 10.8)?

 

Tags (2)
0 Kudos
1 Solution

Accepted Solutions
by Anonymous User
Not applicable

What happens in Pro is one of life's greatest mysteries. It looks like it is at the same level as the script process so it executes in sequential order. It should be in its own function/ class so it only executes when it is called/ needed.

These classes are integrated with application code and processes, so the functions might get called multiple times throughout the process of loading and executing.  There was some documentation about the 'flow' of and explanation of the Toolbox class out there somewhere. 

 

def popup(): ### Pop-up window to adjust threshold
        # root window
        application_window = tk.Tk()
        application_window.geometry("300x70")
        application_window.resizable(False, False)
        application_window.title("Adjust threshold")
        application_window.columnconfigure(0, weight = 1)
        application_window.columnconfigure(1, weight = 3)
        # slider current value
        current_value = tk.DoubleVar()
        current_value.set(threshold)
        # label for the slider
        slider_label = ttk.Label(
            application_window,
            text = "Adjust {0} threshold:".format(vIndex)
        )
        
        slider_label.grid(
            column = 0,
            row = 0,
            sticky = "w"
        )
        
        #  slider
        slider = ttk.Scale(
            application_window,
            from_ = min,
            to = max,
            orient = "horizontal",
            command = slider_changed,
            variable = current_value
        )
        
        slider.grid(
            column = 1,
            row = 0,
            sticky = "we"
        )
        
        # value label
        value_label = ttk.Label(
            application_window,
            text = get_current_value(cv = current_value)
        )
        
        value_label.grid(
            row = 1,
            column = 1,
            sticky = "n"
        )
        def agree(value_array, blc, x_size, y_size, path, cv):
            if type(cv) != float:
                thresh = cv.get()
            else:
                thresh = cv
            try:
                create_mask(value_array, thresh, blc, x_size, y_size, path = path)
            except:
                print("Fail.")
            application_window.destroy()
        
        ttk.Button(application_window, text = "Inspect threshold", command = inspect(value_array = mask, blc = pt, x_size = px_size_x, y_size = px_size_y, cv = current_value)).grid(
            row = 3,
            column = 0,
            columnspan = 1,
            sticky = "n"
        )
        ttk.Button(application_window, text = "Apply threshold", command = agree(value_array = mask, blc = pt, x_size = px_size_x, y_size = px_size_y, path = p_out_raster, cv = current_value)).grid(
            row = 3,
            column = 1,
            columnspan = 2,
            sticky = "n"
        )
        application_window.mainloop()

 

View solution in original post

1 Reply
by Anonymous User
Not applicable

What happens in Pro is one of life's greatest mysteries. It looks like it is at the same level as the script process so it executes in sequential order. It should be in its own function/ class so it only executes when it is called/ needed.

These classes are integrated with application code and processes, so the functions might get called multiple times throughout the process of loading and executing.  There was some documentation about the 'flow' of and explanation of the Toolbox class out there somewhere. 

 

def popup(): ### Pop-up window to adjust threshold
        # root window
        application_window = tk.Tk()
        application_window.geometry("300x70")
        application_window.resizable(False, False)
        application_window.title("Adjust threshold")
        application_window.columnconfigure(0, weight = 1)
        application_window.columnconfigure(1, weight = 3)
        # slider current value
        current_value = tk.DoubleVar()
        current_value.set(threshold)
        # label for the slider
        slider_label = ttk.Label(
            application_window,
            text = "Adjust {0} threshold:".format(vIndex)
        )
        
        slider_label.grid(
            column = 0,
            row = 0,
            sticky = "w"
        )
        
        #  slider
        slider = ttk.Scale(
            application_window,
            from_ = min,
            to = max,
            orient = "horizontal",
            command = slider_changed,
            variable = current_value
        )
        
        slider.grid(
            column = 1,
            row = 0,
            sticky = "we"
        )
        
        # value label
        value_label = ttk.Label(
            application_window,
            text = get_current_value(cv = current_value)
        )
        
        value_label.grid(
            row = 1,
            column = 1,
            sticky = "n"
        )
        def agree(value_array, blc, x_size, y_size, path, cv):
            if type(cv) != float:
                thresh = cv.get()
            else:
                thresh = cv
            try:
                create_mask(value_array, thresh, blc, x_size, y_size, path = path)
            except:
                print("Fail.")
            application_window.destroy()
        
        ttk.Button(application_window, text = "Inspect threshold", command = inspect(value_array = mask, blc = pt, x_size = px_size_x, y_size = px_size_y, cv = current_value)).grid(
            row = 3,
            column = 0,
            columnspan = 1,
            sticky = "n"
        )
        ttk.Button(application_window, text = "Apply threshold", command = agree(value_array = mask, blc = pt, x_size = px_size_x, y_size = px_size_y, path = p_out_raster, cv = current_value)).grid(
            row = 3,
            column = 1,
            columnspan = 2,
            sticky = "n"
        )
        application_window.mainloop()