- Remove 257 redundant per-definition comment stubs across the package - Drop dead nonlocal declarations in autoslicer_detection.py - Document the swapped flag and swap_left_right in architecture docs - Remove unsupported 'legacy override' claims from autotune/runtime docs
242 lines
8.7 KiB
Python
242 lines
8.7 KiB
Python
import json
|
|
import os
|
|
|
|
|
|
class Parameter:
|
|
def __init__(self, default, min, max, step, label, label_secondary=""):
|
|
self.default = default
|
|
self.min = min
|
|
self.max = max
|
|
self.step = step
|
|
self.label = label
|
|
self.label_secondary = label_secondary
|
|
self.value = default
|
|
|
|
def get(self):
|
|
return self.value
|
|
|
|
def set(self, value):
|
|
int_value = int(value)
|
|
if int_value < self.min:
|
|
int_value = self.min
|
|
if int_value > self.max:
|
|
int_value = self.max
|
|
self.value = int_value
|
|
|
|
def reset(self):
|
|
self.value = self.default
|
|
|
|
|
|
class AutoslicerParams:
|
|
SPECS = [
|
|
("detect_pipeline", 0, 0, 1, 1, "Edge Detector", "(0=Threshold, 1=Canny)"),
|
|
("clahe_clip", 0, 0, 40, 1, "Auto Contrast", "(0=disabled)"),
|
|
("gaussian", 0, 0, 100, 1, "Noise Reduction", "(0=disabled)"),
|
|
("bw_method", 0, 0, 2, 1, "Detection Mode", "(0=Simple, 1=Gauss, 2=Otsu)"),
|
|
("bw_thresh", 210, 0, 255, 5, "Brightness Cutoff"),
|
|
("bw_gauss", 64, 0, 1000, 2, "Contrast Window"),
|
|
("bw_adaptive_c", 2, -20, 20, 1, "Adaptive C (Gauss)"),
|
|
("canny_low", 50, 0, 255, 5, "Canny Low Threshold"),
|
|
("canny_high", 150, 0, 255, 5, "Canny High Threshold"),
|
|
("invert_mode", 0, 0, 2, 1, "Invert Detection", "(0=Auto, 1=Off, 2=On)"),
|
|
("morph_op", 1, 0, 4, 1, "Morph Operation", "(0=None, 1=Dilate, 2=Erode, 3=Open, 4=Close)"),
|
|
("contour_approx_pct", 0, 0, 50, 1, "Contour Smoothing", "(epsilon %, x10)"),
|
|
("bbox_min_size", 2, 0, 100, 1, "Min Photo Size ", "(% total)"),
|
|
("bbox_max_size", 90, 1, 100, 1, "Max Photo Size ", "(% total)"),
|
|
("bbox_fill_thresh", 10, 0, 100, 1, "Fill Ratio"),
|
|
("expected_count", 0, 0, 20, 1, "Expected Photos", "(0=auto)"),
|
|
("aspect_ratio_min", 100, 100, 1000, 5, "Min Aspect Ratio", "(x100, long/short)"),
|
|
("aspect_ratio_max", 400, 100, 1000, 5, "Max Aspect Ratio", "(x100, long/short)"),
|
|
("border_ignore", 0, 0, 20, 1, "Ignore Border Margin", "(% of width/height)"),
|
|
("iou_merge_thresh", 30, 0, 100, 1, "Merge Overlap Threshold", "(IoU %)"),
|
|
("dilate_kernel", 30, 0, 500, 1, "Edge Expansion", "(0=disabled)"),
|
|
]
|
|
PRESETS = {}
|
|
|
|
def __init__(self):
|
|
for spec in self.SPECS:
|
|
if len(spec) == 6:
|
|
name, default, min_value, max_value, step, label = spec
|
|
label_secondary = ""
|
|
elif len(spec) == 7:
|
|
name, default, min_value, max_value, step, label, label_secondary = spec
|
|
else:
|
|
raise ValueError("Invalid parameter spec: expected 6 or 7 values")
|
|
|
|
setattr(
|
|
self,
|
|
name,
|
|
Parameter(default, min_value, max_value, step, label, label_secondary),
|
|
)
|
|
self.user_presets = self.load_user_presets()
|
|
self.load_user_defaults()
|
|
|
|
def items(self):
|
|
for name, *_ in self.SPECS:
|
|
yield name, getattr(self, name)
|
|
|
|
@classmethod
|
|
def defaults_path(cls):
|
|
override = os.environ.get("SCANSLICER_DEFAULTS_PATH")
|
|
if override:
|
|
return override
|
|
|
|
if os.path.exists("/.dockerenv") and os.path.isdir("/work"):
|
|
return os.path.join("/work", ".scanslicer", "defaults.json")
|
|
|
|
config_home = os.environ.get("XDG_CONFIG_HOME")
|
|
if not config_home:
|
|
config_home = os.path.join(os.path.expanduser("~"), ".config")
|
|
return os.path.join(config_home, "scanslicer", "defaults.json")
|
|
|
|
@classmethod
|
|
def presets_path(cls):
|
|
override = os.environ.get("SCANSLICER_PRESETS_PATH")
|
|
if override:
|
|
return override
|
|
|
|
if os.path.exists("/.dockerenv") and os.path.isdir("/work"):
|
|
return os.path.join("/work", ".scanslicer", "presets.json")
|
|
|
|
config_home = os.environ.get("XDG_CONFIG_HOME")
|
|
if not config_home:
|
|
config_home = os.path.join(os.path.expanduser("~"), ".config")
|
|
return os.path.join(config_home, "scanslicer", "presets.json")
|
|
|
|
def to_dict(self):
|
|
return {name: parameter.get() for name, parameter in self.items()}
|
|
|
|
def apply_values(self, values):
|
|
if not isinstance(values, dict):
|
|
return
|
|
|
|
values = dict(values)
|
|
legacy_map = {
|
|
"bbox_min_size_prop": "bbox_min_size",
|
|
"bbox_max_size_prop": "bbox_max_size",
|
|
"morph_kernel": "dilate_kernel",
|
|
}
|
|
for legacy_key, new_key in legacy_map.items():
|
|
if new_key not in values and legacy_key in values:
|
|
values[new_key] = values[legacy_key]
|
|
|
|
for name, parameter in self.items():
|
|
if name in values:
|
|
parameter.set(values[name])
|
|
|
|
if self.aspect_ratio_min.get() > self.aspect_ratio_max.get():
|
|
self.aspect_ratio_max.set(self.aspect_ratio_min.get())
|
|
if self.canny_low.get() > self.canny_high.get():
|
|
self.canny_high.set(self.canny_low.get())
|
|
|
|
def reset_to_factory_defaults(self):
|
|
for _, parameter in self.items():
|
|
parameter.reset()
|
|
|
|
def load_user_defaults(self):
|
|
path = self.defaults_path()
|
|
if not os.path.isfile(path):
|
|
return False
|
|
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as fh:
|
|
loaded = json.load(fh)
|
|
except (OSError, json.JSONDecodeError, TypeError, ValueError):
|
|
return False
|
|
|
|
if not isinstance(loaded, dict):
|
|
return False
|
|
|
|
self.apply_values(loaded)
|
|
return True
|
|
|
|
def load_defaults(self):
|
|
if self.load_user_defaults():
|
|
return "saved"
|
|
|
|
self.reset_to_factory_defaults()
|
|
return "factory"
|
|
|
|
def save_user_defaults(self):
|
|
path = self.defaults_path()
|
|
directory = os.path.dirname(path)
|
|
os.makedirs(directory, exist_ok=True)
|
|
with open(path, "w", encoding="utf-8") as fh:
|
|
json.dump(self.to_dict(), fh, indent=2, sort_keys=True)
|
|
return path
|
|
|
|
def load_user_presets(self):
|
|
path = self.presets_path()
|
|
if not os.path.isfile(path):
|
|
return {}
|
|
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as fh:
|
|
loaded = json.load(fh)
|
|
except (OSError, json.JSONDecodeError, TypeError, ValueError):
|
|
return {}
|
|
|
|
if not isinstance(loaded, dict):
|
|
return {}
|
|
|
|
cleaned = {}
|
|
for name, values in loaded.items():
|
|
if not isinstance(name, str):
|
|
continue
|
|
normalized_name = name.strip()
|
|
if not normalized_name:
|
|
continue
|
|
if not isinstance(values, dict):
|
|
continue
|
|
cleaned[normalized_name] = values
|
|
return cleaned
|
|
|
|
def save_user_presets(self):
|
|
path = self.presets_path()
|
|
directory = os.path.dirname(path)
|
|
os.makedirs(directory, exist_ok=True)
|
|
with open(path, "w", encoding="utf-8") as fh:
|
|
json.dump(self.user_presets, fh, indent=2, sort_keys=True)
|
|
return path
|
|
|
|
def preset_names(self):
|
|
builtin_names = list(self.PRESETS.keys())
|
|
user_names = sorted(self.user_presets.keys(), key=str.lower)
|
|
return builtin_names + user_names
|
|
|
|
def save_named_preset(self, preset_name):
|
|
normalized_name = str(preset_name).strip()
|
|
if len(normalized_name) == 0:
|
|
raise ValueError("Preset name cannot be empty.")
|
|
if normalized_name.lower() == "custom":
|
|
raise ValueError("Preset name 'Custom' is reserved.")
|
|
if normalized_name in self.PRESETS:
|
|
raise ValueError(f"'{normalized_name}' is a built-in preset name.")
|
|
|
|
self.user_presets[normalized_name] = self.to_dict()
|
|
self.save_user_presets()
|
|
return normalized_name
|
|
|
|
def delete_named_preset(self, preset_name):
|
|
normalized_name = str(preset_name).strip()
|
|
if len(normalized_name) == 0:
|
|
raise ValueError("Preset name cannot be empty.")
|
|
if normalized_name.lower() == "custom":
|
|
raise ValueError("Preset name 'Custom' cannot be deleted.")
|
|
if normalized_name in self.PRESETS:
|
|
raise ValueError(f"'{normalized_name}' is a built-in preset and cannot be deleted.")
|
|
if normalized_name not in self.user_presets:
|
|
raise ValueError(f"Preset '{normalized_name}' was not found.")
|
|
|
|
del self.user_presets[normalized_name]
|
|
self.save_user_presets()
|
|
return normalized_name
|
|
|
|
def apply_preset(self, preset_name):
|
|
values = self.PRESETS.get(preset_name)
|
|
if values is None:
|
|
values = self.user_presets.get(preset_name)
|
|
if values is None:
|
|
return False
|
|
self.apply_values(values)
|
|
return True
|