Skip to content

Commit 51cb854

Browse files
Refactor: Apply auto raw edit only on raw by default (#27)
* refactor: Remove auto edit photos feature and enable RAW processing by default
1 parent 0c89e48 commit 51cb854

29 files changed

+802
-246
lines changed

DEVELOPER_GUIDE.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The application is structured into two main packages: `core` and `ui`.
1515
- **`model_rotation_detector.py`**: Lazy-loading ONNX orientation detector. Heavy dependencies (onnxruntime / torchvision / Pillow) are imported only on first prediction request. Never gate imports with environment variables—extend the lazy loader if further deferral is needed.
1616
- **`rotation_detector.py`**: Orchestrates batch rotation detection using the model rotation detector (instantiated lazily on demand).
1717
- **`image_processing/`**: Low-level image manipulation, such as RAW processing and rotation.
18+
- **RAW Processing**: RAW images (.arw, .cr2, .cr3, .nef, .dng, etc.) are automatically detected by file extension and receive brightness and contrast adjustments during preview and thumbnail generation. This behavior is built into the image pipeline using internal `is_raw_extension()` detection and applies to all RAW files without requiring external parameters. The system uses `RAW_AUTO_EDIT_BRIGHTNESS_STANDARD = 1.15` for automatic brightness adjustment.
1819
- **`image_orientation_handler.py`**: Handles EXIF-based image orientation correction and composite rotation calculations.
1920
- **`file_scanner.py`**: Scans directories for image files.
2021
- **`image_file_ops.py`**: Handles all file system operations, such as moving, renaming, and deleting files. This is the single source of truth for file manipulations.
@@ -93,7 +94,7 @@ The application is structured into two main packages: `core` and `ui`.
9394

9495
## 3. Coding Conventions
9596

96-
- **Configuration Constants**: All hardcoded values, thresholds, and configurable parameters must be centralized in `src/core/app_settings.py`. This includes UI dimensions, processing thresholds, cache sizes, AI/ML parameters, and other constants. Never use hardcoded values directly in the code - always import from `app_settings.py`.
97+
- **Configuration Constants**: All hardcoded values, thresholds, and configurable parameters must be centralized in `src/core/app_settings.py`. This includes UI dimensions, processing thresholds, cache sizes, AI/ML parameters, RAW processing constants (like `RAW_AUTO_EDIT_BRIGHTNESS_STANDARD`), and other constants. Never use hardcoded values directly in the code - always import from `app_settings.py`.
9798

9899
- **Logging**: Use Python's `logging` module for all logging. **Do not use `print()`**.
99100

@@ -171,12 +172,14 @@ Current layers:
171172
- Integration tests for rotation acceptance (skipped automatically if `sample/` assets absent or GUI constraints unmet).
172173
- Lazy loader tests ensure model instantiation is deferred and disabled mode returns 0.
173174
- About dialog test runs with `block=False` to avoid modal blocking.
175+
- Automatic RAW processing tests validate that file extension detection works correctly and that RAW files receive appropriate processing without external parameters.
174176

175177
Guidelines for new tests:
176178
1. Favor pure function extraction for logic heavy UI code to enable headless unit tests.
177179
2. Use non-blocking dialog patterns (`block=False`) where modal exec would hang CI.
178180
3. Skip GUI-heavy tests gracefully when prerequisites (sample assets, GPU libs) are missing instead of failing.
179181
4. When asserting selection advancement, test the helper function directly with synthetic path lists—only one integration test should cover the end-to-end GUI path.
182+
5. **Critical Windows Testing Requirement**: All test files must start with `import pyexiv2 # noqa: F401 # Must be first to avoid Windows crash with pyexiv2` to prevent access violations in the native library. This import must be the very first import in every test file.
180183

181184

182185
## 7. Adding New Image Feature Pipelines
@@ -217,6 +220,8 @@ When adding new controllers, follow the listed Extension Pattern to maintain con
217220
| Rotation view crashes due to empty model | Guard with early returns; tests should skip rather than fail |
218221
| Adding file operations outside `ImageFileOperations` | Refactor into `ImageFileOperations` to centralize side effects |
219222
| Blocking modal dialogs in CI | Use `block=False` in tests, keep blocking in production UI |
223+
| Test suite fails with Windows access violations | Ensure every test file starts with `import pyexiv2` as the first import |
224+
| RAW processing not working | Check that `is_raw_extension()` detection is used instead of external `apply_auto_edits` parameters |
220225

221226
---
222-
This guide reflects the state after introducing auto-advance rotation acceptance, selection heuristic refactor, lazy model loading, and updated testing patterns.
227+
This guide reflects the state after introducing auto-advance rotation acceptance, selection heuristic refactor, lazy model loading, automatic RAW processing refactor, and updated testing patterns.

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,6 @@ Logs will be saved to `~/.photosort_logs/photosort_app.log`.
231231
**Application Settings**
232232

233233
* **Manage Cache:** `F9`
234-
* **Enable Auto RAW Edits:** `F10`
235234

236235
**Help**
237236

src/core/app_settings.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
PREVIEW_CACHE_SIZE_GB_KEY = "Cache/PreviewCacheSizeGB"
1717
EXIF_CACHE_SIZE_MB_KEY = "Cache/ExifCacheSizeMB" # For EXIF metadata cache
1818
ROTATION_CONFIRM_LOSSY_KEY = "UI/RotationConfirmLossy" # Ask before lossy rotation
19-
AUTO_EDIT_PHOTOS_KEY = "UI/AutoEditPhotos" # Key for auto edit photos setting
2019
RECENT_FOLDERS_KEY = "UI/RecentFolders" # Key for recent folders list
2120
ORIENTATION_MODEL_NAME_KEY = (
2221
"Models/OrientationModelName" # Key for the orientation model file name
@@ -26,7 +25,6 @@
2625
DEFAULT_PREVIEW_CACHE_SIZE_GB = 2.0 # Default to 2 GB for preview cache
2726
DEFAULT_EXIF_CACHE_SIZE_MB = 256 # Default to 256 MB for EXIF cache
2827
DEFAULT_ROTATION_CONFIRM_LOSSY = True # Default to asking before lossy rotation
29-
DEFAULT_AUTO_EDIT_PHOTOS = False # Default auto edit photos setting
3028
MAX_RECENT_FOLDERS = 10 # Max number of recent folders to store
3129
DEFAULT_ORIENTATION_MODEL_NAME = None # Default to None, so we can auto-detect
3230

@@ -167,19 +165,6 @@ def set_rotation_confirm_lossy(confirm: bool):
167165
settings.setValue(ROTATION_CONFIRM_LOSSY_KEY, confirm)
168166

169167

170-
# --- Auto Edit Photos Setting ---
171-
def get_auto_edit_photos() -> bool:
172-
"""Get whether auto edit photos is enabled."""
173-
settings = _get_settings()
174-
return settings.value(AUTO_EDIT_PHOTOS_KEY, DEFAULT_AUTO_EDIT_PHOTOS, type=bool)
175-
176-
177-
def set_auto_edit_photos(enabled: bool):
178-
"""Set whether auto edit photos is enabled."""
179-
settings = _get_settings()
180-
settings.setValue(AUTO_EDIT_PHOTOS_KEY, enabled)
181-
182-
183168
# --- Recent Folders ---
184169
def get_recent_folders() -> list[str]:
185170
"""Gets the list of recent folders from settings."""

src/core/file_scanner.py

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,8 @@ async def _scan_directory_async(self, directory_path):
8585
if ext in SUPPORTED_EXTENSIONS:
8686
full_path = os.path.normpath(os.path.join(root, filename))
8787
# Blur detection would be added here if this method were active
88-
# Assuming self.apply_auto_edits_for_raw_preview is available if this method is used
8988
is_blurred = BlurDetector.is_image_blurred(
90-
full_path,
91-
threshold=self.blur_detection_threshold,
92-
apply_auto_edits_for_raw_preview=getattr(
93-
self, "apply_auto_edits_for_raw_preview", False
94-
), # Fallback
89+
full_path, threshold=self.blur_detection_threshold
9590
)
9691
self.files_found.emit(
9792
[{"path": full_path, "is_blurred": is_blurred}]
@@ -101,21 +96,16 @@ async def _scan_directory_async(self, directory_path):
10196
def scan_directory(
10297
self,
10398
directory_path: str,
104-
apply_auto_edits: bool = False,
10599
perform_blur_detection: bool = False,
106100
blur_threshold: float = 100.0,
107101
):
108102
"""
109103
Starts the directory scanning process.
110104
Optionally detects blur for each image.
111-
apply_auto_edits: bool - Flag for thumbnail preloading AND for RAW preview used in blur detection.
112105
perform_blur_detection: bool - If True, performs blur detection.
113106
blur_threshold: float - Threshold for blur detection if performed.
114107
"""
115108
self._is_running = True
116-
# self.blur_detection_threshold = blur_threshold # Threshold is passed directly to is_image_blurred if needed
117-
# Store apply_auto_edits for use in async or other methods if needed
118-
self.apply_auto_edits_for_raw_preview = apply_auto_edits
119109
all_file_data = [] # Collect all file data (path and blur status)
120110
thumbnail_paths_only = [] # For ImageHandler.preload_thumbnails
121111

@@ -150,9 +140,7 @@ def scan_directory(
150140
)
151141
try:
152142
is_blurred = BlurDetector.is_image_blurred(
153-
full_path,
154-
threshold=blur_threshold,
155-
apply_auto_edits_for_raw_preview=apply_auto_edits,
143+
full_path, threshold=blur_threshold
156144
)
157145
except Exception as e:
158146
# Do not break scanning on blur detection failure; mark unknown
@@ -181,14 +169,10 @@ def scan_directory(
181169
p for p in thumbnail_paths_only if os.path.isfile(p)
182170
]
183171
if existing_for_thumbs:
184-
logger.info(
185-
f"Preloading {len(existing_for_thumbs)} thumbnails (Auto-Edits: {apply_auto_edits})."
186-
)
172+
logger.info(f"Preloading {len(existing_for_thumbs)} thumbnails.")
187173

188174
try:
189-
self.image_pipeline.preload_thumbnails(
190-
existing_for_thumbs, apply_auto_edits=apply_auto_edits
191-
)
175+
self.image_pipeline.preload_thumbnails(existing_for_thumbs)
192176
except Exception as e:
193177
logger.error(f"Thumbnail preloading failed: {e}", exc_info=True)
194178
else:

src/core/image_features/model_rotation_detector.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ def predict_rotation_angle(
4343
self,
4444
image_path: str,
4545
image: Optional[object] = None,
46-
apply_auto_edits: bool = False,
4746
) -> int: ...
4847

4948

@@ -79,13 +78,12 @@ def predict_rotation_angle(
7978
self,
8079
image_path: str,
8180
image: Optional[object] = None,
82-
apply_auto_edits: bool = False,
8381
) -> int:
8482
if not self._ensure_session_loaded():
8583
return 0
8684

8785
if image is None:
88-
image = self._load_image(image_path, apply_auto_edits=apply_auto_edits)
86+
image = self._load_image(image_path)
8987

9088
if image is None:
9189
return 0
@@ -202,13 +200,14 @@ def _resolve_model_path(self) -> Optional[str]:
202200
return None
203201
return model_path
204202

205-
def _load_image(self, path: str, apply_auto_edits: bool):
203+
def _load_image(self, path: str):
206204
try:
207205
norm = os.path.normpath(path)
208206
_, ext = os.path.splitext(norm)
209207
if is_raw_extension(ext):
208+
# Always apply auto-edits for RAW files in rotation detection
210209
return RawImageProcessor.load_raw_as_pil(
211-
norm, half_size=True, apply_auto_edits=apply_auto_edits
210+
norm, half_size=True, apply_auto_edits=True
212211
)
213212
from PIL import Image, ImageOps # type: ignore
214213

src/core/image_features/rotation_detector.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ def _detect_rotation_task(
2929
self,
3030
image_path: str,
3131
result_callback: Optional[Callable[[str, int], None]],
32-
apply_auto_edits: bool,
3332
) -> None:
3433
"""
3534
Worker task for detecting rotation for a single image.
@@ -38,15 +37,15 @@ def _detect_rotation_task(
3837
"""
3938
try:
4039
# Get the image with EXIF orientation already applied by the pipeline.
40+
# Auto-edits are automatically applied for RAW files by the pipeline.
4141
pil_image = self.image_pipeline.get_pil_image_for_processing(
4242
image_path,
43-
apply_auto_edits=apply_auto_edits,
4443
use_preloaded_preview_if_available=False,
4544
apply_exif_transpose=True, # This is the key change.
4645
)
4746

4847
final_suggested_rotation = self.model_detector.predict_rotation_angle(
49-
image_path, image=pil_image, apply_auto_edits=apply_auto_edits
48+
image_path, image=pil_image
5049
)
5150

5251
if result_callback:
@@ -71,10 +70,10 @@ def detect_rotation_in_batch(
7170
) -> None:
7271
"""
7372
Detects rotation suggestions for a batch of images in parallel.
73+
Auto-edits are automatically applied for RAW files.
7474
"""
7575
total_files = len(image_paths)
7676
processed_count = 0
77-
apply_auto_edits = kwargs.get("apply_auto_edits", False)
7877

7978
effective_num_workers = (
8079
num_workers if num_workers is not None else DEFAULT_NUM_WORKERS
@@ -87,9 +86,7 @@ def detect_rotation_in_batch(
8786
max_workers=effective_num_workers
8887
) as executor:
8988
futures_map: Dict[concurrent.futures.Future, str] = {
90-
executor.submit(
91-
self._detect_rotation_task, path, result_callback, apply_auto_edits
92-
): path
89+
executor.submit(self._detect_rotation_task, path, result_callback): path
9390
for path in image_paths
9491
if not (should_continue_callback and not should_continue_callback())
9592
}

0 commit comments

Comments
 (0)