Compare commits

...

6 Commits

Author SHA1 Message Date
lm f47e3925f3 Rename project to ICRS and add folder browsing 2025-10-17 12:31:21 +02:00
lm d49cd08c23 Ignore root __pycache__ directory 2025-10-17 09:05:11 +02:00
lm 91df0b8ddd Adopt uv-based workflow 2025-10-17 08:58:22 +02:00
lm e3c3fc7254 Rewrite README with concise English overview 2025-10-17 08:55:17 +02:00
lm 0bf947b0c2 Revert "Simplify README"
This reverts commit 91cc96a631.
2025-10-17 08:54:42 +02:00
lm 91cc96a631 Simplify README 2025-10-17 08:54:15 +02:00
9 changed files with 155 additions and 111 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
__pycache__
*.py[cod]
*$py.class

141
README.md
View File

@ -1,118 +1,63 @@
# ColorCalc – Interactive Colour Range Analyzer
# ICRS (Interactive Color Range Analyzer)
ColorCalc is a desktop app for Windows/macOS/Linux that helps you identify and highlight colours in images. It is built with Python, Tkinter, and Pillow and is tuned for full‑HD screens. Instead of focusing on a single hue (like purple), you can target any colour range, tweak saturation/value thresholds, and export overlays in seconds.
---
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
- Two synced previews (original + overlay)
- Hue/Sat/Value sliders with presets and image colour picker
- Exclusion rectangles to ignore regions
- Theme toggle (light/dark) with rounded toolbar buttons
- Folder support with previous/next navigation
- Quick overlay export (PNG) and optional defaults via `config.toml`
- 🎯 **Configurable hue detection** – adjust hue, saturation, and value windows or pick colours directly from the image/preset swatches.
- πŸͺŸ **Responsive dual-preview UI** – original image on the left, processed overlay on the right, sized for 1080p.
- πŸ–±οΈ **Interactive masks** – draw exclusion rectangles to ignore specific regions when calculating results.
- πŸŒ— **Light & dark mode** – rounded toolbar buttons adapt to the current theme; toggle any time.
- 🟨 **Real‑time stats** – centre-aligned labels show match ratios with/without exclusions plus filenames and dimensions.
- πŸ’Ύ **Overlay export** – save PNG overlays blended with the source image for reporting or further editing.
- βš™οΈ **Config file defaults** – populate `config.toml` to ship different starting values across machines.
- 🎨 **Preset palette** – one-click access to common colours (red, cyan, grey, black, …).
---
## Getting Started
### Requirements
- Python 3.11+ (includes the standard `tomllib`; for 3.10 install `tomli`)
- Tkinter (ships with most Python distributions; on Linux install `python3-tk`)
- Pillow (`pip install pillow`)
### Installation
## Requirements
- Python 3.11+ (3.10 works with `tomli`)
- [uv](https://github.com/astral-sh/uv) for dependency management
- Tkinter (install separately on some Linux distros)
## Setup with uv
```bash
git clone https://github.com/<your-org>/ColorCalc.git
cd ColorCalc
python3 -m venv .venv
git clone https://github.com/<your-org>/ICRS.git
cd ICRS
uv venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt # or pip install pillow
uv pip sync # installs Pillow + optional deps from pyproject
uv run icrs # launches the GUI
```
### Launch
To include the optional ttkbootstrap theme pack:
```bash
python3 main.py
uv pip install '.[ui]'
```
The window opens maximized; load an image, tweak sliders, and watch the overlay update in real time.
---
## Usage Tips
- **Pick colours quickly:** use the 🎨 button to open a colour chooser or click directly inside the left preview (enable πŸ–±οΈ first).
- **Fine-tune defaults:** edit `config.toml` and set keys under `[defaults]`. Restart the app to apply changes.
- **Reset & compare:** `πŸ”„ Slider zurΓΌcksetzen` reverts to defaults and clears the status message.
- **Exclude areas:** right-drag on the original preview to mark rectangles; use `↩️` to undo or `🧹` to clear all.
- **Copy stats:** right-click the filename or ratio labels to copy.
- **Export overlay:** `πŸ’Ύ` saves a PNG with your overlay merged onto the source image.
- **Theme switch:** `πŸŒ“` toggles between light/dark; button palettes update instantly.
---
## Project Structure
```
ColorCalc/
β”œβ”€β”€ app/
β”‚ β”œβ”€β”€ app.py # ColorCalcApp composition root
β”‚ β”œβ”€β”€ __init__.py # Package exports
β”‚ β”œβ”€β”€ gui/
β”‚ β”‚ β”œβ”€β”€ color_picker.py # Colour selection & presets
β”‚ β”‚ β”œβ”€β”€ exclusions.py # Exclusion rectangle handlers
β”‚ β”‚ β”œβ”€β”€ theme.py # Theme detection & style updates
β”‚ β”‚ └── ui.py # Tkinter layout and custom widgets
β”‚ └── logic/
β”‚ β”œβ”€β”€ constants.py # Config defaults & preview sizing
β”‚ β”œβ”€β”€ image_processing.py # Loading, overlay creation, stats
β”‚ └── reset.py # Slider reset mixin
β”œβ”€β”€ config.toml # Default HSV/alpha overrides
β”œβ”€β”€ images/ # Optional sample inputs
└── main.py # CLI entry point (`python3 main.py`)
```
---
## Configuration (`config.toml`)
## Workflow
1. Load an image (`πŸ“‚`) or a folder (`πŸ“`).
2. Pick a colour (`🎨` dialog, `πŸ–±οΈ` image click, or preset swatch).
3. Fine‑tune sliders; watch the overlay update on the right.
4. Move through folder images with `⬅️` / `➑️`.
5. Draw exclusions with right drag; reset or save when ready.
## Config Defaults
Optional `config.toml`:
```toml
[defaults]
hue_min = 250.0 # 0..360Β°
hue_min = 250.0
hue_max = 310.0
sat_min = 15.0 # percentage 0..100
sat_min = 15.0
val_min = 15.0
val_max = 100.0
alpha = 120 # overlay opacity 0..255
alpha = 120
```
All values are optional; omit them to fall back to built-in defaults. Hue min/max support wrap-around (e.g. 350 to 20).
## Project Layout
```
app/
app.py # main app assembly
gui/ # UI, theme, picker mixins
logic/ # image ops, defaults, reset
config.toml # optional defaults
main.py # entry point
```
---
## Development Workflow
- Format: project sticks to standard Python style; no formatter enforced.
- Testing: primary quick check is compilation via `python3 -m compileall app main.py`.
- Platform quirks: some theme detection uses Windows registry; errors are swallowed when unavailable.
---
## Contributing
1. Fork + clone.
2. Create a feature branch.
3. Make changes and run `python3 -m compileall app main.py`.
4. Submit a PR with a clear description and screenshots if UI changes are visible.
---
## License
MIT License Β© 2024 ColorCalc contributors. See `LICENSE` (add one if missing) for details.
## Development
- Quick check: `uv run python -m compileall app main.py`
- Contributions welcome; include screenshots for UI tweaks.

View File

@ -1,5 +1,5 @@
"""Application package."""
from .app import ColorCalcApp, start_app
from .app import ICRSApp, start_app
__all__ = ["ColorCalcApp", "start_app"]
__all__ = ["ICRSApp", "start_app"]

View File

@ -8,7 +8,7 @@ from .gui import ColorPickerMixin, ExclusionMixin, ThemeMixin, UIBuilderMixin
from .logic import DEFAULTS, ImageProcessingMixin, ResetMixin
class ColorCalcApp(
class ICRSApp(
ThemeMixin,
UIBuilderMixin,
ImageProcessingMixin,
@ -20,7 +20,7 @@ class ColorCalcApp(
def __init__(self, root: tk.Tk):
self.root = root
self.root.title("ColorCalc β€” Bild + Overlay")
self.root.title("ICRS β€” Interactive Color Range Analyzer")
try:
self.root.state("zoomed")
except Exception:
@ -56,6 +56,8 @@ class ColorCalcApp(
self.preview_img = None
self.preview_tk = None
self.overlay_tk = None
self.image_paths = []
self.current_image_index = -1
# Build UI
self.setup_ui()
@ -66,8 +68,8 @@ class ColorCalcApp(
def start_app() -> None:
"""Entry point used by the CLI script."""
root = tk.Tk()
app = ColorCalcApp(root)
app = ICRSApp(root)
root.mainloop()
__all__ = ["ColorCalcApp", "start_app"]
__all__ = ["ICRSApp", "start_app"]

View File

@ -15,6 +15,9 @@ class UIBuilderMixin:
toolbar.pack(fill=tk.X, padx=12, pady=8)
buttons = [
("πŸ“‚ 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 aus Bild klicken", self.enable_pick_mode),
("πŸ’Ύ Overlay speichern", self.save_overlay),

View File

@ -1,6 +1,6 @@
"""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 .reset import ResetMixin
@ -9,6 +9,7 @@ __all__ = [
"DEFAULTS",
"IMAGES_DIR",
"PREVIEW_MAX_SIZE",
"SUPPORTED_IMAGE_EXTENSIONS",
"ImageProcessingMixin",
"ResetMixin",
]

View File

@ -29,6 +29,8 @@ _DEFAULTS_BASE = {
"alpha": 120,
}
SUPPORTED_IMAGE_EXTENSIONS = (".webp", ".png", ".jpg", ".jpeg", ".bmp")
_DEFAULT_TYPES: dict[str, Callable[[Any], Any]] = {
"hue_min": float,
"hue_max": float,

View File

@ -4,13 +4,13 @@ from __future__ import annotations
import colorsys
from pathlib import Path
from typing import Iterable, Tuple
from typing import Iterable, Sequence, Tuple
from tkinter import filedialog, messagebox
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:
@ -22,6 +22,9 @@ class ImageProcessingMixin:
preview_tk: ImageTk.PhotoImage | None
overlay_tk: ImageTk.PhotoImage | None
image_paths: list[Path]
current_image_index: int
def load_image(self) -> None:
default_dir = IMAGES_DIR if IMAGES_DIR.exists() else Path.cwd()
path = filedialog.askopenfilename(
@ -31,21 +34,88 @@ class ImageProcessingMixin:
)
if not path:
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:
image = Image.open(path).convert("RGBA")
except Exception as exc:
messagebox.showerror("Fehler", f"Bild konnte nicht geladen werden: {exc}")
return
self.image_path = path
self.orig_img = image
self.exclude_rects = []
self._rubber_start = None
self._rubber_id = None
self.pick_mode = False
self.prepare_preview()
self.update_preview()
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_default_text = status_text
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:
if self.orig_img is None:

20
pyproject.toml Normal file
View File

@ -0,0 +1,20 @@
[project]
name = "icrs"
version = "0.1.0"
description = "Interactive Color Range Analyzer (ICRS) for Tkinter"
readme = "README.md"
authors = [{ name = "ICRS contributors" }]
license = { text = "MIT" }
requires-python = ">=3.10"
dependencies = [
"pillow>=10.0.0",
]
[project.optional-dependencies]
ui = ["ttkbootstrap>=1.10.0"]
[project.scripts]
icrs = "app.app:start_app"
[tool.uv]
package = false