web: filter the exports the model was only smearing
The upscaler ran for every enlargement, including the ones a plain resample wins: measured on a 2048px source cropped and blown back up it loses to lanczos on PSNR and SSIM at 2x and 3x, and its smoothness reads as plastic skin and lost texture next to it. From 4x — the model's own factor — it stops losing, so the threshold moves to 4 and the crop no longer drags a 2x export through it. The resamples it now carries never set imageSmoothingQuality, and the default 'low' point-samples: a 1px stripe comes out at full amplitude instead of the average of what it crossed. Both callers ask for 'high'. Probe on a 2400x1800 source exported at 4K: 299.9s -> 7.8s, correlation with the source's 1px/2px bands 0.89/0.95 -> 0.98/0.98, grain sd 25.4 -> 47.1 (a plain HQ resize of the same source keeps 16.1).
This commit is contained in:
@@ -76,7 +76,14 @@ const CROP_PREVIEW_MIN_FRAC = 0.05;
|
||||
// Below it a plain filter and the model are the same picture and the filter
|
||||
// finishes in a second; above it (a small crop of a big photo) the model earns
|
||||
// the minutes it costs. Never a cap on the exported size itself.
|
||||
const MODEL_GAIN = 1.5;
|
||||
//
|
||||
// Measured, 2048px source cropped and blown back up, model vs plain filter:
|
||||
// at 2x PSNR/SSIM 18.27/0.3036 against the filter's 18.71/0.3792, at 3x
|
||||
// 17.96/0.2570 against 18.14/0.2782 — the model's smoothness reads as plastic
|
||||
// skin and lost texture next to a plain resample. Only from 4x, where the
|
||||
// pixels truly do not exist, does it stop losing (17.60/0.2305 vs
|
||||
// 17.84/0.2258). So the model is held back to 4x, its own factor.
|
||||
const MODEL_GAIN = 4;
|
||||
// Zoom asks for a bigger copy; quantised so a smooth gesture does not re-cut it
|
||||
// on every notch.
|
||||
const PREVIEW_QUANTUM = 512;
|
||||
@@ -1483,7 +1490,7 @@ export function Workspace() {
|
||||
if (tier.maxExportDim && sourceLongest > tier.maxExportDim) {
|
||||
src = await resizedJpeg(source.bytes, tier.maxExportDim, 0.95);
|
||||
}
|
||||
} else if (wanted > sourceLongest || needLongest > sourceLongest * MODEL_GAIN) {
|
||||
} else if (needLongest > sourceLongest * MODEL_GAIN) {
|
||||
// The photo is short of the pixels being asked for — more so once the
|
||||
// crop is counted — so the model invents them here, on the device; the
|
||||
// picture never leaves it. The whole photo is enlarged by the factor the
|
||||
@@ -1492,10 +1499,11 @@ export function Workspace() {
|
||||
setBusy(t('act.upscaling', { done, total }))
|
||||
);
|
||||
} else if (needLongest !== sourceLongest) {
|
||||
// The photo already holds every pixel the export asks for, so there is
|
||||
// nothing to invent: resample the whole photo (down, or a hair up to
|
||||
// make up for the crop) and let the render cut the crop out of it. This
|
||||
// is what keeps a 2400px photo exportable at 2K in a second.
|
||||
// Within the model's gain the plain filter is the better picture (see
|
||||
// MODEL_GAIN), so this covers every other case: down, and a hair or a
|
||||
// few times up. The whole photo is resampled and the render cuts the
|
||||
// crop out of it. This is what keeps a 2400px photo exportable at 2K in
|
||||
// a second.
|
||||
src = await scaledJpeg(source.bytes, needLongest);
|
||||
}
|
||||
const out = await renderPhoto({
|
||||
|
||||
Reference in New Issue
Block a user