I need to accomplish the following in a Python Toolbox for ArcMap:
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)?
Solved! Go to Solution.
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()
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()