Skip to content

Improve auto white balance#74

Merged
AlanRockefeller merged 7 commits intomainfrom
test
Apr 12, 2026
Merged

Improve auto white balance#74
AlanRockefeller merged 7 commits intomainfrom
test

Conversation

@AlanRockefeller
Copy link
Copy Markdown
Owner

@AlanRockefeller AlanRockefeller commented Apr 12, 2026

Summary by CodeRabbit

  • New Features

    • Faster white-balance-only save path with cached LUTs, improved WB estimation, and more reliable preview refresh.
    • Structured metadata snapshots and undo entries for edit saves; more robust undo/restore for edits.
  • Bug Fixes

    • Preserve metadata and undo integrity when switching folders during async saves.
    • Fix crop/rotation UI state and clamp out-of-range crop start coordinates.
  • Tests

    • Added tests for auto white balance, save/undo behavior, and directory-switch safety.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 64a9665f-8c69-4b4d-9a7c-ac050119d216

📥 Commits

Reviewing files that changed from the base of the PR and between 7a39935 and 95c36e8.

📒 Files selected for processing (2)
  • faststack/app.py
  • faststack/tests/test_editor_reopening.py
✅ Files skipped from review due to trivial changes (1)
  • faststack/app.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • faststack/tests/test_editor_reopening.py

Walkthrough

Freezes save context for async saves, records/restores metadata snapshots for edit saves and undos, adds a uint8 white-balance estimator and fast-path save with LUT caching, refactors uint8 save logic, tightens crop/rotation UI state, and adds tests for AWB, save fast-path, sidecar rebinding, and save-completion edge cases.

Changes

Cohort / File(s) Summary
App save / undo / metadata
faststack/app.py
Freeze save context (metadata path, directory key, save_sidecar) at save time; handle (saved_path, backup_path) on completion; update captured sidecar metadata; record structured "save_edit" undo payloads; unify undo branch for save/edit/awb/levels/crop; add helpers to capture/restore metadata and mark sidecar edits.
ImageEditor: AWB, histograms, and uint8 save
faststack/imaging/editor.py
Add estimate_auto_white_balance() and cached _cached_u8_wb_lut; replace percentile logic with histogram-based _u8_percentile_from_hist(); centralize atomic uint8 save via _save_u8_pil_image(); refactor save_image_uint8_levels(); add save_image_uint8_white_balance() fast-path using per-channel LUTs and sRGB↔linear conversions.
QML crop / rotation state
faststack/qml/Components.qml
Add clearPendingRotation(); ensure pending rotation and isRotating are cleared on crop exit/cancel/escape and controller-driven cancels; clamp beginNewCrop mouse coords to [0,1000] to avoid out-of-range starts.
Tests: AWB, lifecycle, reopening
faststack/tests/test_auto_white_balance.py, faststack/tests/test_editor_lifecycle_and_safety.py, faststack/tests/test_editor_reopening.py
Add unit tests covering AWB estimation/correction and uint8 AWB save fast-path, sidecar rebinding on directory switch, _on_save_finished behavior when directory changed, undo metadata restoration, and malformed save-result handling.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI
    participant App as AppController
    participant Editor as ImageEditor
    participant Sidecar as SidecarManager

    User->>UI: trigger save
    UI->>App: save_edited_image()
    App->>App: freeze save context (metadata_path, save_sidecar, directory key)
    App->>Editor: perform save (uint8 fast-path or general save)
    Editor-->>App: return (saved_path, backup_path) or unexpected payload
    App->>App: _on_save_finished(success, result, frozen_context)
    alt result shaped as expected
        App->>Sidecar: save_sidecar.update_metadata(metadata_path, edited=True, edited_date=...)
        Sidecar-->>App: ack
        App->>App: append undo record ("save_edit", {saved_path, backup_path, metadata_before, sidecar})
        App->>UI: update status (saved)
    else unexpected payload
        App->>UI: update_status_message("Save callback malformed", timeout=5000)
    end

    User->>UI: trigger undo
    UI->>App: undo_delete()
    App->>App: pop undo record and parse payload
    App->>Sidecar: sidecar.metadata_key_for_path(metadata_path) -> restore metadata snapshot
    Sidecar-->>App: ack
    App->>UI: update status (undo applied)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Test #40: Overlaps AppController background-save and save-completion flow changes (sidecar update and undo handling).
  • Performance improvements, modernize Settings dialog #30: Modifies ImageEditor adjustment logic and save paths (auto-levels/white-balance), directly related to estimator and save fast-path.
  • fix some bugs #31: Changes undo handling in app.py, related to the unified undo payload and parsing adjustments.
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title 'Improve auto white balance' is partially related to the changeset but does not capture the main scope of changes, which includes metadata/undo handling refactoring, save path fixes, and crop/rotation UI improvements alongside AWB algorithm improvements. Consider a more comprehensive title such as 'Refactor image editing pipeline with metadata undo tracking and improve AWB' to better reflect the full scope of changes across save handling, undo system, and UI logic.
Docstring Coverage ⚠️ Warning Docstring coverage is 68.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2edad69bbe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread faststack/app.py
Comment on lines +2040 to +2044
self.undo_history.append(
(
"save_edit",
self._build_edit_undo_data(
saved_path,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip cross-directory save undo entries

_on_save_finished unconditionally records a save_edit undo action even when the user has already switched to a different base folder. Because _switch_to_directory(..., update_base_directory=True) clears undo_history, this callback can repopulate the new folder’s undo stack with paths from the old folder; pressing Undo then restores/overwrites files outside the currently opened directory. Please only enqueue this undo entry when the save belongs to the active directory/session (or explicitly discard it after a base-folder switch).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
faststack/imaging/editor.py (1)

1496-1514: Consider removing redundant int() casts.

math.ceil() and math.floor() already return int in Python 3. The explicit int() casts on lines 1508 and 1510 are unnecessary.

♻️ Suggested simplification
         if method == "higher":
-            target_index = int(math.ceil(rank))
+            target_index = math.ceil(rank)
         else:
-            target_index = int(math.floor(rank))
+            target_index = math.floor(rank)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/imaging/editor.py` around lines 1496 - 1514, In
_u8_percentile_from_hist, remove the redundant int() casts around
math.ceil(rank) and math.floor(rank) when assigning target_index (they already
return ints in Python 3); update the branches using method == "higher" / else to
assign target_index = math.ceil(rank) and target_index = math.floor(rank)
respectively, leaving the rest of the function (cdf, np.searchsorted, clamping)
unchanged so type behavior remains correct.
faststack/app.py (1)

8117-8120: Let preview completion own the AWB refresh.

_apply_preview_result() already emits currentImageSourceChanged and schedules the histogram once the new preview is ready. Doing both here first is extra work, and on slower renders it can briefly compute the histogram from the pre-AWB preview before the real one lands.

Also applies to: 8180-8183

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/app.py` around lines 8117 - 8120, The code currently calls
self.ui_state.currentImageSourceChanged.emit() and self.update_histogram()
before kicking the preview worker, which duplicates work because
_apply_preview_result() already emits currentImageSourceChanged and schedules
the histogram when the real preview is ready; remove the preemptive calls to
self.ui_state.currentImageSourceChanged.emit() and self.update_histogram() in
the block containing self._kick_preview_worker() so that preview completion
(_apply_preview_result) owns the AWB/preview refresh, and make the same removal
in the analogous section around the other occurrence (the block referenced at
lines ~8180-8183) to avoid computing histogram from the pre-AWB preview.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@faststack/app.py`:
- Around line 5895-5937: The helpers assume the image metadata key is the saved
file path; change the undo payload and parsing to carry an explicit main-image
metadata_path and use that when marking/restoring metadata: update
_build_edit_undo_data to include a metadata_path (string) in the returned dict,
update _parse_edit_undo_data to extract and return metadata_path (add it to the
returned tuple signature), and then modify callers to pass that metadata_path
into _mark_image_edited_in_sidecar (use metadata_path instead of saved_path) and
into the metadata restore routine (e.g., _restore_metadata_snapshot) so sidecar
updates/read use the original image identity rather than the physical
saved/backup path; keep legacy tuple handling backward-compatible by returning
None for metadata_path when the old tuple format is encountered.

---

Nitpick comments:
In `@faststack/app.py`:
- Around line 8117-8120: The code currently calls
self.ui_state.currentImageSourceChanged.emit() and self.update_histogram()
before kicking the preview worker, which duplicates work because
_apply_preview_result() already emits currentImageSourceChanged and schedules
the histogram when the real preview is ready; remove the preemptive calls to
self.ui_state.currentImageSourceChanged.emit() and self.update_histogram() in
the block containing self._kick_preview_worker() so that preview completion
(_apply_preview_result) owns the AWB/preview refresh, and make the same removal
in the analogous section around the other occurrence (the block referenced at
lines ~8180-8183) to avoid computing histogram from the pre-AWB preview.

In `@faststack/imaging/editor.py`:
- Around line 1496-1514: In _u8_percentile_from_hist, remove the redundant int()
casts around math.ceil(rank) and math.floor(rank) when assigning target_index
(they already return ints in Python 3); update the branches using method ==
"higher" / else to assign target_index = math.ceil(rank) and target_index =
math.floor(rank) respectively, leaving the rest of the function (cdf,
np.searchsorted, clamping) unchanged so type behavior remains correct.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1935eb0b-dfb2-40d6-b321-5eb4ddf65765

📥 Commits

Reviewing files that changed from the base of the PR and between 40fcf73 and 2edad69.

📒 Files selected for processing (6)
  • faststack/app.py
  • faststack/imaging/editor.py
  • faststack/qml/Components.qml
  • faststack/tests/test_auto_white_balance.py
  • faststack/tests/test_editor_lifecycle_and_safety.py
  • faststack/tests/test_editor_reopening.py

Comment thread faststack/app.py
Comment thread faststack/app.py
…a way that matches the actual workflow: the saved file can be a developed or restored variant, while the metadata belongs to the owning image entry.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
faststack/app.py (1)

5980-6000: ⚠️ Potential issue | 🟠 Major

Undo is still restoring the whole metadata record.

This helper replaces or deletes the entire EntryMetadata entry, even though these edit flows only mutate edited and edited_date. If the user toggles favorite, uploaded, todo, etc. after the save, undoing the edit will silently wipe those newer changes. Restore only the fields this action owns, and only drop the entry when it did not exist before and the remaining metadata is all-default.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@faststack/app.py`:
- Around line 8001-8004: The fast-path AWB save uses the main image filepath
when calling image_editor.load_image(cached_preview=..., preview_only=True)
causing developed-variant saves to overwrite the main image; update the load to
mirror execute_crop() by resolving the currently viewed variant (use
_get_save_target_path_for_current_view or the variant path when
view_override_kind == "developed") and call get_decoded_image/current variant
path so image_editor.load_image loads the viewed variant as the source instead
of the main filepath, ensuring saves write to the correct developed variant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a08e694-05a1-4422-bece-c01cf3e2de8b

📥 Commits

Reviewing files that changed from the base of the PR and between 2edad69 and f676420.

📒 Files selected for processing (3)
  • faststack/app.py
  • faststack/imaging/editor.py
  • faststack/tests/test_editor_reopening.py

Comment thread faststack/app.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
faststack/app.py (1)

5520-5524: Avoid broad exception catch in undo restore flow.

Catching all exceptions here can mask programming errors and make undo failures harder to diagnose.

🧭 Narrow exception scope
-            except Exception as e:
+            except (OSError, RuntimeError, ValueError) as e:
                 self.update_status_message(f"Undo failed: {e}")
                 if Path(backup_path).exists():
                     self.undo_history.append((action_type, action_data, timestamp))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/app.py` around lines 5520 - 5524, The current undo restore block is
using a broad "except Exception as e" which can hide programming bugs; change it
to catch only expected recovery errors (e.g., FileNotFoundError, OSError,
shutil.Error or specific I/O/restore errors thrown by the restore logic) around
the restore attempt, call self.update_status_message(f"Undo failed: {e}") and
append to self.undo_history only when the backup exists, and re-raise or let
unexpected exceptions propagate (or log them with full trace) so that
programming errors in the undo flow are not silently swallowed; locate the
try/except around the restore logic that references backup_path,
self.update_status_message, self.undo_history, action_type, action_data, and
timestamp to apply this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@faststack/app.py`:
- Around line 1983-1984: The tuple guard allows tuples longer than 2 and then
raises ValueError when unpacking; change the check to require exactly two
elements (e.g., replace "isinstance(result, tuple) and len(result) >= 2" with
"isinstance(result, tuple) and len(result) == 2") or explicitly extract the
first two values (saved_path, backup_path = result[0], result[1]) so you never
attempt a 2-variable unpack on a longer tuple and the malformed-result handling
remains reachable.
- Around line 8255-8259: The AWB diagnostics logging currently indexes
diagnostics keys directly and can raise KeyError; in the logging call that
formats estimate values (used after estimate_auto_white_balance()), replace
direct indexing of "selected_pixels", "stride", and "neutrality_limit" with safe
lookups (e.g., estimate.get("selected_pixels", 0), estimate.get("stride", 0),
estimate.get("neutrality_limit", 0.0)) or compute fallback values before
formatting so the log statement always has valid values; locate the code around
the AWB logging block and the estimate_auto_white_balance() usage to apply these
.get() lookups or precomputed defaults.

---

Nitpick comments:
In `@faststack/app.py`:
- Around line 5520-5524: The current undo restore block is using a broad "except
Exception as e" which can hide programming bugs; change it to catch only
expected recovery errors (e.g., FileNotFoundError, OSError, shutil.Error or
specific I/O/restore errors thrown by the restore logic) around the restore
attempt, call self.update_status_message(f"Undo failed: {e}") and append to
self.undo_history only when the backup exists, and re-raise or let unexpected
exceptions propagate (or log them with full trace) so that programming errors in
the undo flow are not silently swallowed; locate the try/except around the
restore logic that references backup_path, self.update_status_message,
self.undo_history, action_type, action_data, and timestamp to apply this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f5f2635f-4d81-4210-91fb-d0f7a629d752

📥 Commits

Reviewing files that changed from the base of the PR and between f676420 and 7a39935.

📒 Files selected for processing (2)
  • faststack/app.py
  • faststack/tests/test_editor_reopening.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • faststack/tests/test_editor_reopening.py

Comment thread faststack/app.py Outdated
Comment thread faststack/app.py Outdated
@AlanRockefeller AlanRockefeller merged commit 04164a8 into main Apr 12, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant