Rename project to ICRS and add folder browsing
This commit is contained in:
parent
d49cd08c23
commit
f47e3925f3
16
README.md
16
README.md
|
|
@ -1,12 +1,13 @@
|
||||||
# ColorCalc
|
# ICRS (Interactive Color Range Analyzer)
|
||||||
|
|
||||||
ColorCalc is a small Tkinter tool for analysing colour ranges in images. You load a picture, pick or click a reference colour, adjust hue/saturation/value sliders, and the app marks matching pixels while showing quick stats.
|
ICRS is a small Tkinter tool for analysing colour ranges in images. You load a picture, pick or click a reference colour, adjust hue/saturation/value sliders, and the app marks matching pixels while showing quick stats.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Two synced previews (original + overlay)
|
- Two synced previews (original + overlay)
|
||||||
- Hue/Sat/Value sliders with presets and image colour picker
|
- Hue/Sat/Value sliders with presets and image colour picker
|
||||||
- Exclusion rectangles to ignore regions
|
- Exclusion rectangles to ignore regions
|
||||||
- Theme toggle (light/dark) with rounded toolbar buttons
|
- Theme toggle (light/dark) with rounded toolbar buttons
|
||||||
|
- Folder support with previous/next navigation
|
||||||
- Quick overlay export (PNG) and optional defaults via `config.toml`
|
- Quick overlay export (PNG) and optional defaults via `config.toml`
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
@ -16,12 +17,12 @@ ColorCalc is a small Tkinter tool for analysing colour ranges in images. You loa
|
||||||
|
|
||||||
## Setup with uv
|
## Setup with uv
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/<your-org>/ColorCalc.git
|
git clone https://github.com/<your-org>/ICRS.git
|
||||||
cd ColorCalc
|
cd ICRS
|
||||||
uv venv
|
uv venv
|
||||||
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
||||||
uv pip sync # installs Pillow + optional deps from pyproject
|
uv pip sync # installs Pillow + optional deps from pyproject
|
||||||
uv run colorcalc # launches the GUI
|
uv run icrs # launches the GUI
|
||||||
```
|
```
|
||||||
To include the optional ttkbootstrap theme pack:
|
To include the optional ttkbootstrap theme pack:
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -29,10 +30,11 @@ uv pip install '.[ui]'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
1. Load an image (`π`).
|
1. Load an image (`π`) or a folder (`π`).
|
||||||
2. Pick a colour (`π¨` dialog, `π±οΈ` image click, or preset swatch).
|
2. Pick a colour (`π¨` dialog, `π±οΈ` image click, or preset swatch).
|
||||||
3. Fineβtune sliders; watch the overlay update on the right.
|
3. Fineβtune sliders; watch the overlay update on the right.
|
||||||
4. Draw exclusions with right drag; reset or save when ready.
|
4. Move through folder images with `β¬
οΈ` / `β‘οΈ`.
|
||||||
|
5. Draw exclusions with right drag; reset or save when ready.
|
||||||
|
|
||||||
## Config Defaults
|
## Config Defaults
|
||||||
Optional `config.toml`:
|
Optional `config.toml`:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"""Application package."""
|
"""Application package."""
|
||||||
|
|
||||||
from .app import ColorCalcApp, start_app
|
from .app import ICRSApp, start_app
|
||||||
|
|
||||||
__all__ = ["ColorCalcApp", "start_app"]
|
__all__ = ["ICRSApp", "start_app"]
|
||||||
|
|
|
||||||
10
app/app.py
10
app/app.py
|
|
@ -8,7 +8,7 @@ from .gui import ColorPickerMixin, ExclusionMixin, ThemeMixin, UIBuilderMixin
|
||||||
from .logic import DEFAULTS, ImageProcessingMixin, ResetMixin
|
from .logic import DEFAULTS, ImageProcessingMixin, ResetMixin
|
||||||
|
|
||||||
|
|
||||||
class ColorCalcApp(
|
class ICRSApp(
|
||||||
ThemeMixin,
|
ThemeMixin,
|
||||||
UIBuilderMixin,
|
UIBuilderMixin,
|
||||||
ImageProcessingMixin,
|
ImageProcessingMixin,
|
||||||
|
|
@ -20,7 +20,7 @@ class ColorCalcApp(
|
||||||
|
|
||||||
def __init__(self, root: tk.Tk):
|
def __init__(self, root: tk.Tk):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.root.title("ColorCalc β Bild + Overlay")
|
self.root.title("ICRS β Interactive Color Range Analyzer")
|
||||||
try:
|
try:
|
||||||
self.root.state("zoomed")
|
self.root.state("zoomed")
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -56,6 +56,8 @@ class ColorCalcApp(
|
||||||
self.preview_img = None
|
self.preview_img = None
|
||||||
self.preview_tk = None
|
self.preview_tk = None
|
||||||
self.overlay_tk = None
|
self.overlay_tk = None
|
||||||
|
self.image_paths = []
|
||||||
|
self.current_image_index = -1
|
||||||
|
|
||||||
# Build UI
|
# Build UI
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
|
|
@ -66,8 +68,8 @@ class ColorCalcApp(
|
||||||
def start_app() -> None:
|
def start_app() -> None:
|
||||||
"""Entry point used by the CLI script."""
|
"""Entry point used by the CLI script."""
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
app = ColorCalcApp(root)
|
app = ICRSApp(root)
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["ColorCalcApp", "start_app"]
|
__all__ = ["ICRSApp", "start_app"]
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ class UIBuilderMixin:
|
||||||
toolbar.pack(fill=tk.X, padx=12, pady=8)
|
toolbar.pack(fill=tk.X, padx=12, pady=8)
|
||||||
buttons = [
|
buttons = [
|
||||||
("π Bild laden", self.load_image),
|
("π Bild laden", self.load_image),
|
||||||
|
("π Ordner laden", self.load_folder),
|
||||||
|
("β¬
οΈ Bild zurΓΌck", self.show_previous_image),
|
||||||
|
("β‘οΈ NΓ€chstes Bild", self.show_next_image),
|
||||||
("π¨ Farbe wΓ€hlen", self.choose_color),
|
("π¨ Farbe wΓ€hlen", self.choose_color),
|
||||||
("π±οΈFarbe aus Bild klicken", self.enable_pick_mode),
|
("π±οΈFarbe aus Bild klicken", self.enable_pick_mode),
|
||||||
("πΎ Overlay speichern", self.save_overlay),
|
("πΎ Overlay speichern", self.save_overlay),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"""Logic utilities and mixins for processing and configuration."""
|
"""Logic utilities and mixins for processing and configuration."""
|
||||||
|
|
||||||
from .constants import BASE_DIR, DEFAULTS, IMAGES_DIR, PREVIEW_MAX_SIZE
|
from .constants import BASE_DIR, DEFAULTS, IMAGES_DIR, PREVIEW_MAX_SIZE, SUPPORTED_IMAGE_EXTENSIONS
|
||||||
from .image_processing import ImageProcessingMixin
|
from .image_processing import ImageProcessingMixin
|
||||||
from .reset import ResetMixin
|
from .reset import ResetMixin
|
||||||
|
|
||||||
|
|
@ -9,6 +9,7 @@ __all__ = [
|
||||||
"DEFAULTS",
|
"DEFAULTS",
|
||||||
"IMAGES_DIR",
|
"IMAGES_DIR",
|
||||||
"PREVIEW_MAX_SIZE",
|
"PREVIEW_MAX_SIZE",
|
||||||
|
"SUPPORTED_IMAGE_EXTENSIONS",
|
||||||
"ImageProcessingMixin",
|
"ImageProcessingMixin",
|
||||||
"ResetMixin",
|
"ResetMixin",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ _DEFAULTS_BASE = {
|
||||||
"alpha": 120,
|
"alpha": 120,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SUPPORTED_IMAGE_EXTENSIONS = (".webp", ".png", ".jpg", ".jpeg", ".bmp")
|
||||||
|
|
||||||
_DEFAULT_TYPES: dict[str, Callable[[Any], Any]] = {
|
_DEFAULT_TYPES: dict[str, Callable[[Any], Any]] = {
|
||||||
"hue_min": float,
|
"hue_min": float,
|
||||||
"hue_max": float,
|
"hue_max": float,
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ from __future__ import annotations
|
||||||
|
|
||||||
import colorsys
|
import colorsys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterable, Tuple
|
from typing import Iterable, Sequence, Tuple
|
||||||
|
|
||||||
from tkinter import filedialog, messagebox
|
from tkinter import filedialog, messagebox
|
||||||
|
|
||||||
from PIL import Image, ImageDraw, ImageTk
|
from PIL import Image, ImageDraw, ImageTk
|
||||||
|
|
||||||
from .constants import IMAGES_DIR, PREVIEW_MAX_SIZE
|
from .constants import IMAGES_DIR, PREVIEW_MAX_SIZE, SUPPORTED_IMAGE_EXTENSIONS
|
||||||
|
|
||||||
|
|
||||||
class ImageProcessingMixin:
|
class ImageProcessingMixin:
|
||||||
|
|
@ -22,6 +22,9 @@ class ImageProcessingMixin:
|
||||||
preview_tk: ImageTk.PhotoImage | None
|
preview_tk: ImageTk.PhotoImage | None
|
||||||
overlay_tk: ImageTk.PhotoImage | None
|
overlay_tk: ImageTk.PhotoImage | None
|
||||||
|
|
||||||
|
image_paths: list[Path]
|
||||||
|
current_image_index: int
|
||||||
|
|
||||||
def load_image(self) -> None:
|
def load_image(self) -> None:
|
||||||
default_dir = IMAGES_DIR if IMAGES_DIR.exists() else Path.cwd()
|
default_dir = IMAGES_DIR if IMAGES_DIR.exists() else Path.cwd()
|
||||||
path = filedialog.askopenfilename(
|
path = filedialog.askopenfilename(
|
||||||
|
|
@ -31,21 +34,88 @@ class ImageProcessingMixin:
|
||||||
)
|
)
|
||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
self.image_path = Path(path)
|
self._set_image_collection([Path(path)], 0)
|
||||||
|
|
||||||
|
def load_folder(self) -> None:
|
||||||
|
default_dir = IMAGES_DIR if IMAGES_DIR.exists() else Path.cwd()
|
||||||
|
directory = filedialog.askdirectory(
|
||||||
|
title="Ordner mit Bildern wΓ€hlen",
|
||||||
|
initialdir=str(default_dir),
|
||||||
|
)
|
||||||
|
if not directory:
|
||||||
|
return
|
||||||
|
folder = Path(directory)
|
||||||
|
if not folder.exists():
|
||||||
|
messagebox.showerror("Fehler", "Der Ordner wurde nicht gefunden.")
|
||||||
|
return
|
||||||
|
image_files = sorted(
|
||||||
|
(
|
||||||
|
path
|
||||||
|
for path in folder.iterdir()
|
||||||
|
if path.suffix.lower() in SUPPORTED_IMAGE_EXTENSIONS and path.is_file()
|
||||||
|
),
|
||||||
|
key=lambda item: item.name.lower(),
|
||||||
|
)
|
||||||
|
if not image_files:
|
||||||
|
messagebox.showinfo("Info", "Keine unterstΓΌtzten Bilder im Ordner gefunden.")
|
||||||
|
return
|
||||||
|
self._set_image_collection(image_files, 0)
|
||||||
|
|
||||||
|
def show_next_image(self, event=None) -> None:
|
||||||
|
if not getattr(self, "image_paths", None):
|
||||||
|
return
|
||||||
|
next_index = getattr(self, "current_image_index", -1) + 1
|
||||||
|
if next_index < len(self.image_paths):
|
||||||
|
self._display_image_by_index(next_index)
|
||||||
|
|
||||||
|
def show_previous_image(self, event=None) -> None:
|
||||||
|
if not getattr(self, "image_paths", None):
|
||||||
|
return
|
||||||
|
prev_index = getattr(self, "current_image_index", -1) - 1
|
||||||
|
if prev_index >= 0:
|
||||||
|
self._display_image_by_index(prev_index)
|
||||||
|
|
||||||
|
def _set_image_collection(self, paths: Sequence[Path], start_index: int) -> None:
|
||||||
|
self.image_paths = list(paths)
|
||||||
|
if not self.image_paths:
|
||||||
|
return
|
||||||
|
self.current_image_index = -1
|
||||||
|
self._display_image_by_index(max(0, start_index))
|
||||||
|
|
||||||
|
def _display_image_by_index(self, index: int) -> None:
|
||||||
|
if not self.image_paths:
|
||||||
|
return
|
||||||
|
if index < 0 or index >= len(self.image_paths):
|
||||||
|
return
|
||||||
|
path = self.image_paths[index]
|
||||||
|
if not path.exists():
|
||||||
|
messagebox.showerror("Fehler", f"Datei nicht gefunden: {path}")
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
image = Image.open(path).convert("RGBA")
|
image = Image.open(path).convert("RGBA")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
messagebox.showerror("Fehler", f"Bild konnte nicht geladen werden: {exc}")
|
messagebox.showerror("Fehler", f"Bild konnte nicht geladen werden: {exc}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.image_path = path
|
||||||
self.orig_img = image
|
self.orig_img = image
|
||||||
|
self.exclude_rects = []
|
||||||
|
self._rubber_start = None
|
||||||
|
self._rubber_id = None
|
||||||
|
self.pick_mode = False
|
||||||
|
|
||||||
self.prepare_preview()
|
self.prepare_preview()
|
||||||
self.update_preview()
|
self.update_preview()
|
||||||
|
|
||||||
dimensions = f"{self.orig_img.width}x{self.orig_img.height}"
|
dimensions = f"{self.orig_img.width}x{self.orig_img.height}"
|
||||||
status_text = f"Geladen: {self.image_path.name} β {dimensions}"
|
suffix = f" [{index + 1}/{len(self.image_paths)}]" if len(self.image_paths) > 1 else ""
|
||||||
|
status_text = f"Geladen: {path.name} β {dimensions}{suffix}"
|
||||||
self.status.config(text=status_text)
|
self.status.config(text=status_text)
|
||||||
self.status_default_text = status_text
|
self.status_default_text = status_text
|
||||||
if hasattr(self, "filename_label"):
|
if hasattr(self, "filename_label"):
|
||||||
self.filename_label.config(text=f"{self.image_path.name} β {dimensions}")
|
self.filename_label.config(text=f"{path.name} β {dimensions}{suffix}")
|
||||||
|
|
||||||
|
self.current_image_index = index
|
||||||
|
|
||||||
def save_overlay(self) -> None:
|
def save_overlay(self) -> None:
|
||||||
if self.orig_img is None:
|
if self.orig_img is None:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
[project]
|
[project]
|
||||||
name = "colorcalc"
|
name = "icrs"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Interactive colour range analyser for Tkinter"
|
description = "Interactive Color Range Analyzer (ICRS) for Tkinter"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [{ name = "ColorCalc contributors" }]
|
authors = [{ name = "ICRS contributors" }]
|
||||||
license = { text = "MIT" }
|
license = { text = "MIT" }
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|
@ -14,7 +14,7 @@ dependencies = [
|
||||||
ui = ["ttkbootstrap>=1.10.0"]
|
ui = ["ttkbootstrap>=1.10.0"]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
colorcalc = "app.app:start_app"
|
icrs = "app.app:start_app"
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
package = false
|
package = false
|
||||||
|
|
|
||||||
Loadingβ¦
Reference in New Issue