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:
@@ -23,6 +23,10 @@ export async function resizedJpeg(bytes: Uint8Array, maxDim: number, quality = 0
|
||||
const canvas = new OffscreenCanvas(w, h);
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return bytes;
|
||||
// The default is 'low', which point-samples: a one-pixel stripe in the
|
||||
// source comes out as full-amplitude ripple (255 against the 36 an exact
|
||||
// area average gives) instead of being averaged away.
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.drawImage(bitmap, 0, 0, w, h);
|
||||
const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality });
|
||||
return new Uint8Array(await blob.arrayBuffer());
|
||||
@@ -34,10 +38,9 @@ export async function resizedJpeg(bytes: Uint8Array, maxDim: number, quality = 0
|
||||
// The same resample, but it enlarges as well as shrinks and always re-encodes.
|
||||
// `resizedJpeg` refuses to grow a photo back, which is right for the preview
|
||||
// copy; the export needs the growth because a committed crop keeps only part of
|
||||
// the photo, so the whole photo has to be stretched a little for the crop to
|
||||
// land on the requested size — and a plain filter is the honest answer when the
|
||||
// photo already holds those pixels. Only ever called with a target the photo
|
||||
// already covers (see the export's gain guard).
|
||||
// the photo, so the whole photo has to be stretched for the crop to land on the
|
||||
// requested size — and a plain filter is the honest answer while it still holds
|
||||
// the detail (see the export's gain guard).
|
||||
export async function scaledJpeg(bytes: Uint8Array, targetLongest: number, quality = 0.95): Promise<Uint8Array> {
|
||||
const bitmap = await createImageBitmap(new Blob([bytes as BlobPart]));
|
||||
try {
|
||||
@@ -49,6 +52,9 @@ export async function scaledJpeg(bytes: Uint8Array, targetLongest: number, quali
|
||||
const canvas = new OffscreenCanvas(w, h);
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return bytes;
|
||||
// 'high' for the same reason as `resizedJpeg`: the default aliases, and
|
||||
// this path is now the export's upscale, where a ripple shows.
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.drawImage(bitmap, 0, 0, w, h);
|
||||
const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality });
|
||||
return new Uint8Array(await blob.arrayBuffer());
|
||||
|
||||
Reference in New Issue
Block a user