From c4ad5ec7c1671d8c34b3706fcfe2dd39e12b841f Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 24 Feb 2026 13:24:15 +0000 Subject: [PATCH] feat(params): implement class AutoslicerParams --- scanslicer/params.py | 232 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) diff --git a/scanslicer/params.py b/scanslicer/params.py index b5bfebd..af8eae5 100644 --- a/scanslicer/params.py +++ b/scanslicer/params.py @@ -30,3 +30,235 @@ class Parameter: # reset: function definition for ScanSlicer behavior. def reset(self): self.value = self.default + + +# AutoslicerParams: class definition for ScanSlicer behavior. +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 = {} + + # __init__: function definition for ScanSlicer behavior. + 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() + + # items: function definition for ScanSlicer behavior. + def items(self): + for name, *_ in self.SPECS: + yield name, getattr(self, name) + + @classmethod + # defaults_path: function definition for ScanSlicer behavior. + 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 + # presets_path: function definition for ScanSlicer behavior. + 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") + + # to_dict: function definition for ScanSlicer behavior. + def to_dict(self): + return {name: parameter.get() for name, parameter in self.items()} + + # apply_values: function definition for ScanSlicer behavior. + 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()) + + # reset_to_factory_defaults: function definition for ScanSlicer behavior. + def reset_to_factory_defaults(self): + for _, parameter in self.items(): + parameter.reset() + + # load_user_defaults: function definition for ScanSlicer behavior. + 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 + + # load_defaults: function definition for ScanSlicer behavior. + def load_defaults(self): + if self.load_user_defaults(): + return "saved" + + self.reset_to_factory_defaults() + return "factory" + + # save_user_defaults: function definition for ScanSlicer behavior. + 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 + + # load_user_presets: function definition for ScanSlicer behavior. + 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 + + # save_user_presets: function definition for ScanSlicer behavior. + 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 + + # preset_names: function definition for ScanSlicer behavior. + 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 + + # save_named_preset: function definition for ScanSlicer behavior. + 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 + + # delete_named_preset: function definition for ScanSlicer behavior. + 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 + + # apply_preset: function definition for ScanSlicer behavior. + 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