Skip to content

fix(metrics): mean-center before Hamming window in FRC/FSC preprocessing#42

Merged
alxndrkalinin merged 2 commits into
mainfrom
fix/frc-mean-center-before-hamming
May 28, 2026
Merged

fix(metrics): mean-center before Hamming window in FRC/FSC preprocessing#42
alxndrkalinin merged 2 commits into
mainfrom
fix/frc-mean-center-before-hamming

Conversation

@alxndrkalinin

@alxndrkalinin alxndrkalinin commented May 28, 2026

Copy link
Copy Markdown
Owner

Summary

preprocess_images (cubic/metrics/spectral/frc.py) applied the Hamming taper directly to the raw image. The Hamming window has mean ~0.54 and goes to zero at edges; multiplying an image with mean μ by this taper produces a μ·(w(x) − mean(w)) low-frequency artifact that survives the later image − image.mean() DC removal in the FFT layer — the artifact has zero spatial mean by construction but non-zero power across all low-frequency bins.

On a membrane-fluorescence-like input (mean ≈ 2500, range 800–14600) the artifact carries 761× more low-frequency power than the corrected pipeline, shifting the FSC=0.143 crossing.

Fix

Mean-subtract each (already-split) image before windowing in preprocess_images. Mirrors what dcr.py:_preprocess_dcr_image already does correctly (center, then Tukey). The post-window image − image.mean() stays as defensive DC removal — a windowed zero-mean signal still has a tiny residual mean from the non-uniform weighting.

After the fix, low-frequency power on the same synthetic input drops from 2.7e162.1e12 (a ~4-order-of-magnitude reduction).

Impact

Touches every FRC/FSC path routed through preprocess_images:

  • FRC (2D)
  • DirectionalFSC (3D, mask backend)
  • _calculate_frc_core hist backend
  • _calculate_fsc_sectioned_hist
  • Reverse-checkerboard averaging

DCR is unaffected — it already centered before windowing.

Resolution estimates on high-DC-offset data will shift (should improve, not regress). No change expected on already-centered or low-offset inputs.

Test plan

  • pytest tests/metrics/spectral/ — 27 passed, 4 GPU-skipped
  • ruff check + ruff format --check clean
  • mypy --ignore-missing-imports cubic/ clean (via pre-commit hook)
  • Numerical verification: low-frequency power drops ~761× on a membrane-fluorescence-like synthetic with mean ≈ 2500

🤖 Generated with Claude Code

Summary by Sourcery

Bug Fixes:

  • Prevent low-frequency artifacts in FRC/FSC computations by subtracting the per-image mean prior to Hamming windowing across all code paths using preprocess_images.

preprocess_images was applying the Hamming taper directly to the raw
image. With a large DC offset μ the taper produces a μ·(w(x)−mean(w))
low-frequency artifact that survives the later image−image.mean() DC
removal (zero spatial mean, non-zero power across low-frequency bins),
shifting the FSC=0.143 crossing on high-offset data. Mirrors what DCR's
_preprocess_dcr_image already does (mean-subtract, then window).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Adjusts FRC/FSC preprocessing so that images are mean-centered before applying the Hamming window, eliminating a large low-frequency artifact for high-DC-offset inputs while keeping the existing defensive post-window DC subtraction.

Flow diagram for updated FRC/FSC image preprocessing

flowchart TD
    A[preprocess_images start] --> B[image1,image2 input]
    B --> C{zero_padding enabled?}
    C -->|yes| D[pad_image_to_cube image1]
    D --> E[pad_image_to_cube image2]
    C -->|no| E
    E --> F{disable_hamming?}
    F -->|yes| G[skip Hamming window]
    F -->|no| H["image1_minus_mean = image1 - image1.mean()"]
    H --> I["image2_minus_mean = image2 - image2.mean()"]
    I --> J[image1 = hamming_window image1_minus_mean]
    J --> K[image2 = hamming_window image2_minus_mean]
    G --> L[return preprocessed images]
    K --> L
Loading

File-Level Changes

Change Details Files
Mean-center images before applying the Hamming window in FRC/FSC preprocessing to avoid low-frequency artifacts from high DC offsets.
  • Change preprocessing flow so Hamming windowing is applied to (image - image.mean()) instead of the raw image for both image1 and image2 when windowing is enabled.
  • Preserve validation that image2 is set after splitting and before preprocessing, raising RuntimeError if missing.
  • Update inline comments to document the rationale for mean-centering prior to windowing and its effect on low-frequency artifacts.
cubic/metrics/spectral/frc.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="cubic/metrics/spectral/frc.py" line_range="153-154" />
<code_context>
         if image2 is None:
             raise RuntimeError("image2 must be set after splitting")
-        image2 = hamming_window(image2)
+        image1 = hamming_window(image1 - image1.mean())
+        image2 = hamming_window(image2 - image2.mean())

     if image2 is None:
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Mean subtraction will propagate NaNs across the entire image, which may be undesirable compared to the previous behavior.

Because the mean is now computed over the full array, any NaNs in `image1`/`image2` will make `image*.mean()` NaN, and thus `image* - image*.mean()` will turn the entire array into NaNs instead of keeping them localized. If NaNs are expected (e.g., masked pixels), consider `np.nanmean` or masking so missing data doesn’t corrupt the whole windowed image.

Suggested implementation:

```python
        # Use NaN-aware means so missing data does not corrupt the whole image
        image1 = hamming_window(image1 - np.nanmean(image1))
        image2 = hamming_window(image2 - np.nanmean(image2))

```

1. Ensure this file imports numpy as `np` at the top if it does not already:
   - `import numpy as np`
2. If your codebase prefers explicit masking instead of `nanmean`, you could instead:
   - Compute a mask for valid pixels (e.g. `valid1 = np.isfinite(image1)`) and compute the mean only over valid pixels.
   - Apply the mean subtraction only where the mask is true, leaving invalid pixels unchanged.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread cubic/metrics/spectral/frc.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR updates FRC/FSC preprocessing so split images are mean-centered before Hamming windowing, reducing low-frequency artifacts caused by tapering high-DC-offset inputs.

Changes:

  • Mean-subtracts each preprocessed split image before applying hamming_window.
  • Adds an explanatory comment documenting why centering must happen before tapering.
  • Keeps downstream defensive DC removal unchanged.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cubic/metrics/spectral/frc.py Outdated
…on tests

Moves mean-subtraction ahead of pad_image_to_cube so the zero-padded ring
stays at zero instead of carrying a -diluted_mean offset that the Hamming
taper would leak back into low-frequency bins. On a non-cubic shape with
DC offset, low-frequency artifact power drops a further ~600x beyond the
initial fix and now matches a centered-input reference. Binomial counts
splitting still needs raw photon counts, so its halves are centered after
splitting instead.

Adds two regression tests pinning both ordering invariants
(center-before-window, center-before-pad) by comparing low-frequency FFT
power against a DC-only-shifted reference (Copilot review on PR #42).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alxndrkalinin alxndrkalinin merged commit 6cec44b into main May 28, 2026
9 checks passed
@alxndrkalinin alxndrkalinin deleted the fix/frc-mean-center-before-hamming branch May 28, 2026 22:49
alxndrkalinin added a commit that referenced this pull request Jun 1, 2026
Resets the version after v0.7.0a8/a9/a10 tags were deleted before
publish. Covers PRs #42, #43, #44, #45, and #46 on top of v0.7.0a7.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
alxndrkalinin added a commit that referenced this pull request Jun 1, 2026
PyPI already holds cubic-0.7.0a8 from an earlier release and rejects
file-name reuse, so skip ahead to a9. Covers PRs #42-#46 on top of
v0.7.0a7.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

2 participants