cover logo: supersample the winner, and scale to the target not past it

Five changes to the mask the tracer receives, plus the diagnostic needed to
tell them apart.

The alpha ramp's bottom now scales with the measured ink distance instead of
sitting at a fixed 46. On a faint mark that constant ate the outer half of
every edge gradient -- Wand's "Machine Man / M.E." inks at distance 97, so the
pixel carrying ~20% coverage was forced fully transparent and the boundary
quantised to whole source pixels. The finished alpha is then rescaled so the
glyph body reaches opacity, bounded at 4x; narrowing the ramp onto the ink was
tried instead and let paper grain bite the edges.

A BLIND OCR read (zero lines, not "no match") retries once at 512px. Windows
OCR is scale-sensitive on large display type and reads that cover at 512 and
at no other size; gating on blindness rather than on no-match kept a 21-artist
regeneration at 1m47s where the looser gate took ~6min.

The winning cover is re-cut with the crop resampled up BEFORE the alpha ramp,
so smoothstep evaluates interpolated colour and places the edge between source
pixels. Selection still runs at native scale: extract_wordmark feeds MIN_MARK_H
and the aa/coverage/bleed score, so supersampling during scoring changes which
cover wins.

That scale is fractional. A floored integer factor is 1 for every crop wider
than half the target, so at 2400 a crop of 1201px was supersampled by nothing
at all while 1200px got 2x -- and the covers on the wrong side of that cliff
are the large printed wordmarks, where the staircase was worst. Measured over
eight artists, Hot Toddy's zigzag share fell 0.312 -> 0.048 and Crazy P's
0.220 -> 0.021, with every other artist unchanged. 2400 itself is confirmed by
sweeping 1200/3600/4800: past 2400 the mask overshoots the tracer's raster so
the browser downscales what Rust just built, every shape measure goes flat and
the data URL doubles.

catalog_cover_logo_debug reports the rejection reason and the mask itself, and
its cover cap follows COVER_LOGO_MAX_COVERS instead of undercutting it at 14 --
where it hid exactly the covers production relies on. The artist page also
re-runs the logo pass when ownership changes the covers on offer; that pass
races the identity merge and had been reading the unfiltered list, which is how
Wand wore a wordmark lifted from a co-tenant's single.
This commit is contained in:
KeelrLeek committed 2026-07-26 09:33:11 -05:00
1 parent 4d6e69e324
commit 4c0c4a1ea5
3 files changed
+343 -28

No files matched your search

+113 -15
View File
@@ -1569,7 +1569,17 @@ pub async fn catalog_artist_socials(
// v4: the alpha ramp now scales to the measured ink distance, so every cached
// mask predates the anti-aliasing this pipeline relies on and must be redrawn.
const COVER_LOGO_RECIPE: &str = "cover-logo-v4-ink-scaled-alpha";
// v5: three changes, each of which can turn a previously-empty result into a
// mark (or trim one that over-reached), so every v4 entry is stale by
// construction — the alpha ramp's ceiling can no longer exceed the ink's own
// distance, the dominance guard admits flat multi-tone crops, and a row walk
// that never met a gap falls back to the OCR box instead of running to the crop
// edge. Wand had four covers printing its name and v4 extracted none of them.
// v10: the winner's supersample scale is fractional instead of a floored
// integer. Every mask whose crop was wider than half the target was previously
// supersampled by exactly 1x — i.e. not at all — so the covers this rewrites are
// the LARGE printed wordmarks, which is where the staircase was worst.
const COVER_LOGO_RECIPE: &str = "cover-logo-v10-supersample-2400-fractional";
// A prolific artist's wordmark is often nowhere near the top of this list. The
// rank puts albums first and penalises `remix|mix|edit|live` titles — but for
// Poolside (74 releases) every one of the SEVEN covers that OCR reads as
@@ -1903,11 +1913,29 @@ async fn generate_cover_logo(
const PROMINENCE_W: f32 = 3.0;
const MIN_PROMINENCE: f32 = 0.05;
const MIN_MARK_H: u32 = 74;
// Mask long edge the WINNER is re-cut at. Matching the tracer's own raster
// is the whole rationale: one good Lanczos enlargement in Rust instead of
// Lanczos followed by the browser's bilinear, with the trace then running at
// roughly the mask's native resolution.
//
// Swept against 1200/3600/4800 over eight artists (see
// `research/cover-logo-superres/prototypes/supersample-batch.mjs`), and 2400
// is where the curve flattens. 1200 is below several crops and leaves them
// untouched. Past 2400 the mask overshoots the trace raster, so the browser
// DOWNSCALES what Rust just built — every shape measure is flat from there
// (Poolside, Wand, Camping In Alaska, Turnover all identical at 2400 and
// 3600) while the data URL doubles: Mildlife 137KB -> 267KB, Camping 76KB ->
// 152KB, for nothing. The one artist that still gains above 2400 is Hot
// Toddy, and it gains from the downscale itself rather than from the extra
// detail — the same smoothing the tracer's blur term would give it.
const MASK_SUPERSAMPLE_EDGE: u32 = 2400;
let extract_started = std::time::Instant::now();
let (best, winner) = tokio::task::spawn_blocking(move || {
use base64::Engine as _;
use image::{GenericImageView as _, ImageEncoder as _};
let mut best: Option<(f32, usize, String, image::RgbaImage)> = None;
// The winner's INPUTS, not its mask: the mask is re-cut once below at
// full supersample, so scoring never pays for it and never sees it.
let mut best: Option<(f32, CoverLogoCandidate)> = None;
for candidate in candidates {
let prom_w = (candidate.loc.rect.w / candidate.dims_1000.0 as f32).min(1.0);
if prom_w < MIN_PROMINENCE {
@@ -1923,13 +1951,13 @@ async fn generate_cover_logo(
hw as f32 / candidate.dims_1000.0 as f32,
hh as f32 / candidate.dims_1000.1 as f32,
);
crate::cover_logo::extract_wordmark(&hi, &scaled)
crate::cover_logo::extract_wordmark(&hi, &scaled, 0)
})
.or_else(|| {
image::load_from_memory(candidate.bytes_1000.as_slice())
.ok()
.and_then(|image| {
crate::cover_logo::extract_wordmark(&image, &candidate.loc)
crate::cover_logo::extract_wordmark(&image, &candidate.loc, 0)
})
});
let Some(mark) = mark else { continue };
@@ -1937,14 +1965,46 @@ async fn generate_cover_logo(
continue;
}
let final_score = mark.score + PROMINENCE_W * prom_w;
if best
.as_ref()
.is_none_or(|(score, _, _, _)| final_score > *score)
{
best = Some((final_score, candidate.rank, candidate.title, mark.image));
if best.as_ref().is_none_or(|(score, _)| final_score > *score) {
best = Some((final_score, candidate));
}
}
let Some((_, rank, title, image)) = best else {
let Some((_, winner)) = best else {
return (None, None);
};
let (rank, title) = (winner.rank, winner.title.clone());
// Re-cut the winning mark, this time resampling the crop up before the
// alpha ramp so the ramp reads interpolated colour and places the edge
// between source pixels rather than on them. Selection above ran at
// native scale on purpose — see `try_extract_wordmark`'s `target_edge`.
// Falls back to the selection-scale mask, so the richer pass failing can
// never cost an artist its logo.
let recut = winner
.bytes_hi
.as_ref()
.and_then(|bytes| image::load_from_memory(bytes.as_slice()).ok())
.and_then(|hi| {
let (hw, hh) = hi.dimensions();
let scaled = winner.loc.scaled(
hw as f32 / winner.dims_1000.0 as f32,
hh as f32 / winner.dims_1000.1 as f32,
);
crate::cover_logo::extract_wordmark(&hi, &scaled, MASK_SUPERSAMPLE_EDGE)
.or_else(|| crate::cover_logo::extract_wordmark(&hi, &scaled, 0))
})
.or_else(|| {
image::load_from_memory(winner.bytes_1000.as_slice())
.ok()
.and_then(|img| {
crate::cover_logo::extract_wordmark(
&img,
&winner.loc,
MASK_SUPERSAMPLE_EDGE,
)
.or_else(|| crate::cover_logo::extract_wordmark(&img, &winner.loc, 0))
})
});
let Some(image) = recut.map(|mark| mark.image) else {
return (None, None);
};
let (width, height) = image.dimensions();
@@ -2124,6 +2184,13 @@ pub struct CoverLogoDiag {
pub mark_w: u32,
pub mark_h: u32,
pub extract_score: f32,
pub reject_reason: String,
/// The extracted mask itself, as a `data:image/png` URL, or empty when the
/// cover produced none. The numbers above say a mark was found and how big
/// it is; only the pixels say whether its EDGES are worth tracing, which is
/// the whole question when sweeping `target_edge`. Empty on the OCR-matrix
/// paths that never extract.
pub mark_png: String,
pub winner: bool,
pub ocr: Vec<String>,
}
@@ -2134,12 +2201,21 @@ pub async fn catalog_cover_logo_debug(
artist_id: Option<u64>,
artist: String,
covers: Vec<CoverRef>,
target_edge: Option<u32>,
) -> Result<Vec<CoverLogoDiag>, String> {
use image::GenericImageView as _;
// Mask supersample target to segment at. 0/absent means native, which is
// what candidate SELECTION uses in production; a value here is for sweeping
// "does more supersampling help" without a rebuild per level.
let target_edge = target_edge.unwrap_or(0);
// Download 1000px covers for OCR.
let mut downloaded = Vec::new();
for cover in covers.into_iter().take(14) {
// Must match COVER_LOGO_MAX_COVERS, not undercut it. At 14 this diagnostic
// silently hid exactly the covers the real pipeline relies on — Poolside's
// seven POOLSIDE-readable covers are all remix singles ranked past 14, so
// the tool reported "one match" for an artist production finds several on.
for cover in covers.into_iter().take(COVER_LOGO_MAX_COVERS) {
if let Some(bytes) = state.metadata.cover_bytes(&cover.url).await {
downloaded.push((cover.title, cover.url, bytes));
}
@@ -2202,6 +2278,8 @@ pub async fn catalog_cover_logo_debug(
mark_w: 0,
mark_h: 0,
extract_score: 0.0,
reject_reason: String::new(),
mark_png: String::new(),
winner: false,
};
if let Some((loc, text, mscore)) = found {
@@ -2240,10 +2318,30 @@ pub async fn catalog_cover_logo_debug(
row.hi_w = hw;
row.hi_h = hh;
if let Ok(img_ex) = image::load_from_memory(&bytes_ex) {
if let Some(mark) = crate::cover_logo::extract_wordmark(&img_ex, &loc_ex) {
row.mark_w = mark.image.width();
row.mark_h = mark.image.height();
row.extract_score = mark.score;
match crate::cover_logo::try_extract_wordmark(&img_ex, &loc_ex, target_edge) {
Ok(mark) => {
use base64::Engine as _;
use image::ImageEncoder as _;
row.mark_w = mark.image.width();
row.mark_h = mark.image.height();
row.extract_score = mark.score;
let mut png = Vec::new();
if image::codecs::png::PngEncoder::new(&mut png)
.write_image(
mark.image.as_raw(),
row.mark_w,
row.mark_h,
image::ExtendedColorType::Rgba8,
)
.is_ok()
{
row.mark_png = format!(
"data:image/png;base64,{}",
base64::engine::general_purpose::STANDARD.encode(&png)
);
}
}
Err(reason) => row.reject_reason = reason.to_string(),
}
}
}