Skip to content

havel: flat-field harmonize the satellite mosaic quilt#106

Merged
AdaWorldAPI merged 1 commit into
mainfrom
claude/havel-mosaic-harmonize
Jul 13, 2026
Merged

havel: flat-field harmonize the satellite mosaic quilt#106
AdaWorldAPI merged 1 commit into
mainfrom
claude/havel-mosaic-harmonize

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

What

The Havel ver-9 satellite skin is a quilt — a mosaic of unrelated ESRI acquisitions differing in season, sun angle, and white balance, with visible tile seams. A big hazy grey acquisition tile dominated the mid-lower region and two pale strips ran down the right side. Used raw as a terrain texture, the quilt reads as arbitrary bright/dark/desaturated patches rather than as terrain.

This PR replaces the earlier global white-balance step with a flat-field harmonization and ships the rebaked asset.

Why the previous approach made it worse

The earlier "Weißabgleich" was a global per-channel percentile stretch. A global stretch has no notion of "this brightness is a seam, not the scene", so it pushed the hazy tiles toward white — near-white pixels went 0.32% → 5.17% (16×), amplifying the exact quilt it was meant to hide.

The fix — flat-field harmonization

scripts/harmonize_demgrid.py, run offline on the .demgrid before the bake (the global WB is dropped):

  1. A radius-80 Gaussian estimates the quilt's slowly-varying exposure/white-balance field.
  2. Subtract (low − global_mean) · 0.80 to flatten that field toward one global mean. 0.80 removes the seams while a genuinely dark forest block vs a bright agricultural block survives (1.0 erases them into a muddy uniform green — tried and rejected).
  3. Water protection — dark (low-luma) pixels flatten less, so the Müritz and the Havel keep their true dark tone instead of being lifted toward the land mean.
  4. Chroma compression toward a common vegetation magnitude (target 28) so an over-saturated tile and a washed tile land on one green, killing the "some tiles grey, some vivid" tell.

Only the rgb block is rewritten; header / lats / elev are copied byte-for-byte, so geometry and elevation are untouched.

Result

  • Near-white 5.17% → 0.10%.
  • The grey acquisition tile and pale strips pull into a coherent forest green.
  • Lakes and river stay dark.
  • Approved on the 2D after ("Looks perfect"); the 3D proxy (1.21M verts) reads as one place, not a patchwork.

Files

  • cockpit/public/havel.v8grid.soa.gz — the 19.4M-vert rebake of the harmonized skin (67.8 MB gz; on-demand release-hosting is the separate follow-up).
  • scripts/harmonize_demgrid.py — the reproducible transform. Verified it reproduces the shipped .demgrid byte-for-byte with the Havel-approved --strength 0.80 --radius 80 --chroma 28.
  • claude-notes/plans/2026-07-10-garmin-dem-satellite-skin.md — round write-up.

Data-asset + standalone Python script + doc only — no Rust/TS code changed, so the workspace build/tests are unaffected.

🤖 Generated with Claude Code


Generated by Claude Code

The Havel ver-9 skin is a quilt of unrelated ESRI acquisitions (season,
sun-angle, white-balance differences) with visible tile seams — a big hazy
grey tile dominated the mid-lower region and two pale strips ran down the
right. Raw, it reads as arbitrary bright/dark patches, not terrain.

An earlier global per-channel "Weißabgleich" made it worse: a global stretch
pushes hazy tiles toward white (near-white 0.32% -> 5.17%). White balance has
no notion of "this brightness is a seam."

Replace it with a flat-field pass (scripts/harmonize_demgrid.py, run offline
on the .demgrid before the bake):
  - radius-80 Gaussian estimates the quilt's low-frequency exposure field;
  - subtract (low - global_mean) * 0.80 to flatten it toward one mean while a
    genuinely dark forest vs bright field block survives (1.0 erases them);
  - water protection: dark pixels flatten less, so lakes/river stay dark;
  - chroma compression toward a common vegetation magnitude (target 28).

Only the rgb block is rewritten; header/lats/elev are copied byte-for-byte,
so geometry and elevation are untouched. Near-white 5.17% -> 0.10%; the grey
tile and pale strips pull into a coherent forest green. Ships the 19.4M-vert
rebake as cockpit/public/havel.v8grid.soa.gz; the script reproduces the
shipped .demgrid byte-for-byte.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jEwwaT5JZ5x8qWvcnaMYC
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AdaWorldAPI, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ca40379-a681-44b2-a8aa-87c9933e6131

📥 Commits

Reviewing files that changed from the base of the PR and between ccbfd06 and 4657a8d.

⛔ Files ignored due to path filters (1)
  • cockpit/public/havel.v8grid.soa.gz is excluded by !**/*.gz
📒 Files selected for processing (2)
  • claude-notes/plans/2026-07-10-garmin-dem-satellite-skin.md
  • scripts/harmonize_demgrid.py

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.

@cursor

cursor Bot commented Jul 13, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_d3acdef1-4b27-4a73-b809-1f38134f15ff)

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4657a8d7d3

ℹ️ 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 on lines +132 to +133
harm = harmonize(dem["rgb"], args.strength, args.radius, args.chroma, args.water_knee, args.water_soft)
write_demgrid(args.output, dem, harm)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep classification separate from the harmonized skin

When this harmonized .demgrid is fed to the standard iceland_dem --grid --skin bake, these rewritten RGB bytes are also the input to classify_kind (geo/src/bin/iceland_dem.rs:327-328), not just the photoreal skin. That makes the flat-field/chroma grade change the BSO2 kind block that Topo mode renders via palette[kind] and that wet masks read; in the shipped Havel asset I compared against the parent, 8,151,475 of 19,399,908 kind entries changed while geometry stayed byte-identical. For a skin-only harmonization, preserve the original imagery for kind classification or carry a separate harmonized skin so topo/material semantics do not shift with color grading.

Useful? React with 👍 / 👎.

@AdaWorldAPI AdaWorldAPI merged commit c1a8cb8 into main Jul 13, 2026
5 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.

2 participants