Feature: Settings Import/Export and UI Polish
- Implemented JSON-based settings import/export with smart scaling - Locked image overlay color to Red (#ff0000) permanently - Decoupled analyzer target color from display mask color - Added menu separators for better organization - Fixed overlay synchronization and scaling bugs during import
This commit is contained in:
parent
c278ddf458
commit
acfcf99d15
|
|
@ -59,6 +59,13 @@
|
|||
"dialog.no_image_loaded" = "Kein Bild geladen."
|
||||
"dialog.no_preview_available" = "Keine Preview vorhanden."
|
||||
"dialog.overlay_saved" = "Overlay gespeichert: {path}"
|
||||
"dialog.json_filter" = "JSON-Dateien (*.json)"
|
||||
"dialog.export_settings_title" = "Einstellungen als JSON exportieren"
|
||||
"dialog.import_settings_title" = "Einstellungen aus JSON importieren"
|
||||
"status.settings_exported" = "Einstellungen exportiert: {path}"
|
||||
"status.settings_imported" = "Einstellungen importiert."
|
||||
"toolbar.export_settings" = "Einstellungen exportieren (JSON)"
|
||||
"toolbar.import_settings" = "Einstellungen importieren (JSON)"
|
||||
"dialog.export_stats_title" = "Ordner-Statistiken exportieren (CSV)"
|
||||
"dialog.csv_filter" = "CSV-Dateien (*.csv)"
|
||||
"status.drag_drop" = "Bild oder Ordner hier ablegen."
|
||||
|
|
|
|||
|
|
@ -59,6 +59,13 @@
|
|||
"dialog.no_image_loaded" = "No image loaded."
|
||||
"dialog.no_preview_available" = "No preview available."
|
||||
"dialog.overlay_saved" = "Overlay saved: {path}"
|
||||
"dialog.json_filter" = "JSON Files (*.json)"
|
||||
"dialog.export_settings_title" = "Export settings to JSON"
|
||||
"dialog.import_settings_title" = "Import settings from JSON"
|
||||
"status.settings_exported" = "Settings exported: {path}"
|
||||
"status.settings_imported" = "Settings imported."
|
||||
"toolbar.export_settings" = "Export settings (JSON)"
|
||||
"toolbar.import_settings" = "Import settings (JSON)"
|
||||
"dialog.export_stats_title" = "Export Folder Statistics (CSV)"
|
||||
"dialog.csv_filter" = "CSV Files (*.csv)"
|
||||
"status.drag_drop" = "Drop an image or folder here to open it."
|
||||
|
|
|
|||
|
|
@ -316,8 +316,10 @@ class QtImageProcessor:
|
|||
return self._to_pixmap(self.preview_img)
|
||||
|
||||
def overlay_pixmap(self) -> QtGui.QPixmap:
|
||||
if self.preview_img is None or self.overlay_img is None:
|
||||
if self.preview_img is None:
|
||||
return QtGui.QPixmap()
|
||||
if self.overlay_img is None:
|
||||
return self.preview_pixmap()
|
||||
merged = Image.alpha_composite(self.preview_img.convert("RGBA"), self.overlay_img)
|
||||
return self._to_pixmap(merged)
|
||||
|
||||
|
|
@ -331,7 +333,7 @@ class QtImageProcessor:
|
|||
|
||||
# exclusions -------------------------------------------------------------
|
||||
|
||||
def set_exclusions(self, shapes: list[dict[str, object]]) -> None:
|
||||
def set_exclusions(self, shapes: list[dict[str, object]], ref_size: Tuple[int, int] | None = None) -> None:
|
||||
copied: list[dict[str, object]] = []
|
||||
for shape in shapes:
|
||||
kind = shape.get("kind")
|
||||
|
|
@ -342,7 +344,10 @@ class QtImageProcessor:
|
|||
pts = shape.get("points", [])
|
||||
copied.append({"kind": "polygon", "points": [(int(x), int(y)) for x, y in pts]})
|
||||
self.exclude_shapes = copied
|
||||
if self.preview_img:
|
||||
|
||||
if ref_size:
|
||||
self.exclude_ref_size = ref_size
|
||||
elif self.preview_img:
|
||||
self.exclude_ref_size = self.preview_img.size
|
||||
else:
|
||||
self.exclude_ref_size = None
|
||||
|
|
@ -382,6 +387,8 @@ class QtImageProcessor:
|
|||
self.overlay_r = int(hex_code[1:3], 16)
|
||||
self.overlay_g = int(hex_code[3:5], 16)
|
||||
self.overlay_b = int(hex_code[5:7], 16)
|
||||
if self.preview_img:
|
||||
self._rebuild_overlay()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import time
|
|||
import urllib.request
|
||||
import urllib.error
|
||||
import csv
|
||||
import json
|
||||
import concurrent.futures
|
||||
from pathlib import Path
|
||||
from typing import Callable, Dict, List, Tuple
|
||||
|
|
@ -19,6 +20,7 @@ from .image_processor import QtImageProcessor
|
|||
from .pattern_puller import PatternPullerDialog
|
||||
|
||||
DEFAULT_COLOR = "#763e92"
|
||||
DEFAULT_OVERLAY_HEX = "#ff0000"
|
||||
|
||||
PRESET_COLORS: List[Tuple[str, str]] = [
|
||||
("palette.swatch.red", "#ff3b30"),
|
||||
|
|
@ -534,8 +536,8 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
|||
self.processor = QtImageProcessor()
|
||||
self.processor.set_defaults(defaults)
|
||||
self.processor.reset_exclusions_on_switch = reset_exclusions
|
||||
if overlay_color:
|
||||
self.processor.set_overlay_color(overlay_color)
|
||||
# Always use red for the overlay regardless of the target color
|
||||
self.processor.set_overlay_color(DEFAULT_OVERLAY_HEX)
|
||||
|
||||
self.content_layout = QtWidgets.QVBoxLayout(self.content)
|
||||
self.content_layout.setContentsMargins(24, 0, 24, 24)
|
||||
|
|
@ -615,9 +617,13 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
|||
# File Menu
|
||||
file_menu = self.menu_bar.addMenu(self._t("menu.file"))
|
||||
file_menu.addAction("🖼 " + self._t("toolbar.open_image"), lambda: self._invoke_action("open_image"), "Ctrl+O")
|
||||
file_menu.addSeparator()
|
||||
file_menu.addAction("📂 " + self._t("toolbar.open_folder"), lambda: self._invoke_action("open_folder"), "Ctrl+Shift+O")
|
||||
file_menu.addAction("📊 " + self._t("toolbar.export_folder"), lambda: self._invoke_action("export_folder"))
|
||||
file_menu.addSeparator()
|
||||
file_menu.addAction("📤 " + self._t("toolbar.export_settings"), lambda: self._invoke_action("export_settings"), "Ctrl+E")
|
||||
file_menu.addAction("📥 " + self._t("toolbar.import_settings"), lambda: self._invoke_action("import_settings"), "Ctrl+I")
|
||||
file_menu.addSeparator()
|
||||
file_menu.addAction("💾 " + self._t("toolbar.save_overlay"), lambda: self._invoke_action("save_overlay"), "Ctrl+S")
|
||||
|
||||
# Edit Menu
|
||||
|
|
@ -750,6 +756,8 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
|||
"choose_color": self.choose_color,
|
||||
"pick_from_image": self.pick_from_image,
|
||||
"save_overlay": self.save_overlay,
|
||||
"export_settings": self.export_settings,
|
||||
"import_settings": self.import_settings,
|
||||
"toggle_free_draw": self.toggle_free_draw,
|
||||
"clear_excludes": self.clear_exclusions,
|
||||
"undo_exclude": self.undo_exclusion,
|
||||
|
|
@ -806,6 +814,93 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
|||
self._current_image_path = loaded_path
|
||||
self._refresh_views()
|
||||
|
||||
def export_settings(self) -> None:
|
||||
item_name = ""
|
||||
if self._current_image_path:
|
||||
# Try to get folder name first, otherwise file name
|
||||
if self._current_image_path.parent.name and self._current_image_path.parent.name != "images":
|
||||
item_name = self._current_image_path.parent.name
|
||||
else:
|
||||
item_name = self._current_image_path.stem
|
||||
|
||||
default_filename = f"icra_settings_{item_name}.json" if item_name else "icra_settings.json"
|
||||
|
||||
default_dir = str(Path("images").absolute()) if Path("images").exists() else ""
|
||||
path_str, _ = QtWidgets.QFileDialog.getSaveFileName(
|
||||
self,
|
||||
self._t("dialog.export_settings_title"),
|
||||
str(Path(default_dir) / default_filename),
|
||||
self._t("dialog.json_filter")
|
||||
)
|
||||
if not path_str:
|
||||
return
|
||||
|
||||
settings = {
|
||||
"hue_min": self.processor.hue_min,
|
||||
"hue_max": self.processor.hue_max,
|
||||
"sat_min": self.processor.sat_min,
|
||||
"val_min": self.processor.val_min,
|
||||
"val_max": self.processor.val_max,
|
||||
"alpha": self.processor.alpha,
|
||||
"current_color": self._current_color,
|
||||
"exclude_ref_size": self.processor.exclude_ref_size,
|
||||
"shapes": self.image_view.shapes
|
||||
}
|
||||
|
||||
try:
|
||||
with open(path_str, "w", encoding="utf-8") as f:
|
||||
json.dump(settings, f, indent=4)
|
||||
self.status_label.setText(self._t("status.settings_exported", path=Path(path_str).name))
|
||||
except Exception as e:
|
||||
QtWidgets.QMessageBox.warning(self, self._t("dialog.error_title"), str(e))
|
||||
|
||||
def import_settings(self) -> None:
|
||||
default_dir = str(Path("images").absolute()) if Path("images").exists() else ""
|
||||
path_str, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
self,
|
||||
self._t("dialog.import_settings_title"),
|
||||
default_dir,
|
||||
self._t("dialog.json_filter")
|
||||
)
|
||||
if not path_str:
|
||||
return
|
||||
|
||||
try:
|
||||
with open(path_str, "r", encoding="utf-8") as f:
|
||||
settings = json.load(f)
|
||||
|
||||
# 1. Apply color (UI ONLY)
|
||||
if "current_color" in settings:
|
||||
self._current_color = settings["current_color"]
|
||||
# Specifically NOT setting processor color to keep it RED
|
||||
self._update_color_display(self._current_color, self._t("palette.current"))
|
||||
|
||||
# 2. Apply slider values
|
||||
keys = ["hue_min", "hue_max", "sat_min", "val_min", "val_max", "alpha"]
|
||||
for key in keys:
|
||||
if key in settings:
|
||||
setattr(self.processor, key, settings[key])
|
||||
|
||||
# 3. Apply shapes and reference size
|
||||
ref_size = None
|
||||
if "exclude_ref_size" in settings and settings["exclude_ref_size"]:
|
||||
ref_size = tuple(settings["exclude_ref_size"])
|
||||
|
||||
if "shapes" in settings:
|
||||
self.image_view.set_shapes(settings["shapes"])
|
||||
self.processor.set_exclusions(settings["shapes"], ref_size=ref_size)
|
||||
else:
|
||||
# Force rebuild even if no shapes to pick up sliders/color
|
||||
if self.processor.preview_img:
|
||||
self.processor._rebuild_overlay()
|
||||
|
||||
self._sync_sliders_from_processor()
|
||||
self._refresh_views()
|
||||
self.status_label.setText(self._t("status.settings_imported"))
|
||||
|
||||
except Exception as e:
|
||||
QtWidgets.QMessageBox.warning(self, self._t("dialog.error_title"), str(e))
|
||||
|
||||
def export_folder(self) -> None:
|
||||
if not self.processor.preview_paths:
|
||||
QtWidgets.QMessageBox.information(self, self._t("dialog.info_title"), self._t("dialog.no_image_loaded"))
|
||||
|
|
@ -923,6 +1018,8 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
|||
self._current_color = hex_code
|
||||
self.current_color_swatch.setStyleSheet(f"background-color: {hex_code}; border-radius: 6px;")
|
||||
self.current_color_label.setText(f"({hex_code})")
|
||||
# Do NOT call self.processor.set_overlay_color here to keep overlay RED
|
||||
if label:
|
||||
self.status_label.setText(f"{label}: {hex_code}")
|
||||
|
||||
def _on_slider_change(self, key: str, value: int) -> None:
|
||||
|
|
|
|||
Loading…
Reference in New Issue