import json import os # Parameter: class definition for ScanSlicer behavior. class Parameter: # __init__: function definition for ScanSlicer behavior. 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 # get: function definition for ScanSlicer behavior. def get(self): return self.value # set: function definition for ScanSlicer behavior. 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 # 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