mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-11 16:26:20 +00:00
Add auto optimizer
This commit is contained in:
67
cli/src/auto-optimizer.js
Normal file
67
cli/src/auto-optimizer.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { instantiateEmscriptenWasm } from "./emscripten-utils.js";
|
||||
|
||||
import visdif from "../../codecs/visdif/visdif.js";
|
||||
import visdifWasm from "asset-url:../../codecs/visdif/visdif.wasm";
|
||||
|
||||
// `measure` is a (async) function that takes exactly one numeric parameter and
|
||||
// returns a value. The function is assumed to be monotonic (an increase in `parameter`
|
||||
// will result in an increase in the return value. The function uses binary search
|
||||
// to find `parameter` such that `measure` returns `measureGoal`, within an error
|
||||
// of `epsilon`. It will use at most `maxRounds` attempts.
|
||||
export async function binarySearch(
|
||||
measureGoal,
|
||||
measure,
|
||||
{ min = 0, max = 100, epsilon = 0.1, maxRounds = 8 } = {}
|
||||
) {
|
||||
let parameter = (max - min) / 2 + min;
|
||||
let delta = (max - min) / 4;
|
||||
let value;
|
||||
let round = 0;
|
||||
do {
|
||||
value = await measure(parameter);
|
||||
if (value > measureGoal) {
|
||||
parameter -= delta;
|
||||
} else if (value < measureGoal) {
|
||||
parameter += delta;
|
||||
}
|
||||
delta /= 2;
|
||||
round++;
|
||||
} while (Math.abs(value - measureGoal) > epsilon && round < maxRounds);
|
||||
return { parameter, round, value };
|
||||
}
|
||||
export async function autoOptimize(
|
||||
bitmapIn,
|
||||
encode,
|
||||
decode,
|
||||
{ butteraugliDistanceGoal = 1.4, ...otherOpts } = {}
|
||||
) {
|
||||
const { VisDiff } = await instantiateEmscriptenWasm(visdif, visdifWasm);
|
||||
|
||||
const comparator = new VisDiff(
|
||||
bitmapIn.data,
|
||||
bitmapIn.width,
|
||||
bitmapIn.height
|
||||
);
|
||||
|
||||
let bitmapOut;
|
||||
let binaryOut;
|
||||
// Increasing quality means _decrease_ in Butteraugli distance.
|
||||
// `binarySearch` assumes that increasing `parameter` will
|
||||
// increase the metric value. So multipliy Butteraugli values by -1.
|
||||
const { parameter } = await binarySearch(
|
||||
-1 * butteraugliDistanceGoal,
|
||||
async quality => {
|
||||
binaryOut = await encode(bitmapIn, quality);
|
||||
bitmapOut = await decode(binaryOut);
|
||||
return -1 * comparator.distance(bitmapOut.data);
|
||||
},
|
||||
otherOpts
|
||||
);
|
||||
comparator.delete();
|
||||
|
||||
return {
|
||||
bitmap: bitmapOut,
|
||||
binary: binaryOut,
|
||||
quality: parameter
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { promises as fsp } from "fs";
|
||||
import { instantiateEmscriptenWasm } from "./emscripten-utils.js";
|
||||
|
||||
// MozJPEG
|
||||
import mozEnc from "../../codecs/mozjpeg/enc/mozjpeg_enc.js";
|
||||
@@ -18,16 +19,9 @@ import avifEncWasm from "asset-url:../../codecs/avif/enc/avif_enc.wasm";
|
||||
import avifDec from "../../codecs/avif/dec/avif_dec.js";
|
||||
import avifDecWasm from "asset-url:../../codecs/avif/dec/avif_dec.wasm";
|
||||
|
||||
function instantiateEmscriptenWasm(factory, path) {
|
||||
if (path.startsWith("file://")) {
|
||||
path = path.slice("file://".length);
|
||||
}
|
||||
return factory({
|
||||
locateFile() {
|
||||
return path;
|
||||
}
|
||||
});
|
||||
}
|
||||
// Our decoders currently rely on a `ImageData` global.
|
||||
import ImageData from "./image_data.js";
|
||||
globalThis.ImageData = ImageData;
|
||||
|
||||
export default {
|
||||
mozjpeg: {
|
||||
@@ -53,6 +47,11 @@ export default {
|
||||
chroma_subsample: 2,
|
||||
separate_chroma_quality: false,
|
||||
chroma_quality: 75
|
||||
},
|
||||
autoOptimize: {
|
||||
option: "quality",
|
||||
min: 0,
|
||||
max: 100
|
||||
}
|
||||
},
|
||||
webp: {
|
||||
@@ -89,6 +88,11 @@ export default {
|
||||
near_lossless: 100,
|
||||
use_delta_palette: 0,
|
||||
use_sharp_yuv: 0
|
||||
},
|
||||
autoOptimize: {
|
||||
option: "quality",
|
||||
min: 0,
|
||||
max: 100
|
||||
}
|
||||
},
|
||||
avif: {
|
||||
@@ -104,6 +108,11 @@ export default {
|
||||
tileRowsLog2: 0,
|
||||
speed: 10,
|
||||
subsample: 0
|
||||
},
|
||||
autoOptimize: {
|
||||
option: "maxQuantizer",
|
||||
min: 0,
|
||||
max: 62
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
11
cli/src/emscripten-utils.js
Normal file
11
cli/src/emscripten-utils.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export function instantiateEmscriptenWasm(factory, path) {
|
||||
if (path.startsWith("file://")) {
|
||||
path = path.slice("file://".length);
|
||||
}
|
||||
return factory({
|
||||
locateFile() {
|
||||
return path;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
114
cli/src/index.js
114
cli/src/index.js
@@ -8,10 +8,7 @@ import { version } from "json:../package.json";
|
||||
|
||||
import supportedFormats from "./codecs.js";
|
||||
import WorkerPool from "./worker_pool.js";
|
||||
|
||||
// Our decoders currently rely on a `ImageData` global.
|
||||
import ImageData from "./image_data.js";
|
||||
globalThis.ImageData = ImageData;
|
||||
import { autoOptimize } from "./auto-optimizer.js";
|
||||
|
||||
function clamp(v, min, max) {
|
||||
if (v < min) return min;
|
||||
@@ -51,18 +48,49 @@ async function decodeFile(file) {
|
||||
async function encodeFile({
|
||||
file,
|
||||
size,
|
||||
bitmap,
|
||||
bitmap: bitmapIn,
|
||||
outputFile,
|
||||
encName,
|
||||
encConfig
|
||||
}) {
|
||||
let out;
|
||||
const encoder = await supportedFormats[encName].enc();
|
||||
const out = encoder.encode(
|
||||
bitmap.data.buffer,
|
||||
bitmap.width,
|
||||
bitmap.height,
|
||||
encConfig
|
||||
);
|
||||
if (encConfig === "auto") {
|
||||
const optionToOptimize = supportedFormats[encName].autoOptimize.option;
|
||||
const decoder = await supportedFormats[encName].dec();
|
||||
const encode = (bitmapIn, quality) =>
|
||||
encoder.encode(
|
||||
bitmapIn.data,
|
||||
bitmapIn.width,
|
||||
bitmapIn.height,
|
||||
Object.assign({}, supportedFormats[encName].defaultEncoderOptions, {
|
||||
[optionToOptimize]: quality
|
||||
})
|
||||
);
|
||||
const decode = binary => decoder.decode(binary);
|
||||
const { bitmap, binary, quality } = await autoOptimize(
|
||||
bitmapIn,
|
||||
encode,
|
||||
decode,
|
||||
{
|
||||
min: supportedFormats[encName].autoOptimize.min,
|
||||
max: supportedFormats[encName].autoOptimize.max
|
||||
}
|
||||
);
|
||||
out = binary;
|
||||
console.log(
|
||||
`Used ${JSON.stringify({
|
||||
[optionToOptimize]: quality
|
||||
})} for ${outputFile}`
|
||||
);
|
||||
} else {
|
||||
out = encoder.encode(
|
||||
bitmapIn.data.buffer,
|
||||
bitmapIn.width,
|
||||
bitmapIn.height,
|
||||
encConfig
|
||||
);
|
||||
}
|
||||
await fsp.writeFile(outputFile, out);
|
||||
return {
|
||||
inputSize: size,
|
||||
@@ -89,11 +117,14 @@ async function processFiles(files) {
|
||||
if (!program[encName]) {
|
||||
continue;
|
||||
}
|
||||
const encConfig = Object.assign(
|
||||
{},
|
||||
value.defaultEncoderOptions,
|
||||
JSON5.parse(program[encName])
|
||||
);
|
||||
const encConfig =
|
||||
program[encName].toLowerCase() === "auto"
|
||||
? "auto"
|
||||
: Object.assign(
|
||||
{},
|
||||
value.defaultEncoderOptions,
|
||||
JSON5.parse(program[encName])
|
||||
);
|
||||
const outputFile = join(program.outputDir, `${base}.${value.extension}`);
|
||||
jobsStarted++;
|
||||
workerPool
|
||||
@@ -147,54 +178,3 @@ if (isMainThread) {
|
||||
} else {
|
||||
WorkerPool.useThisThreadAsWorker(encodeFile);
|
||||
}
|
||||
|
||||
/*
|
||||
const butteraugliGoal = 1.4;
|
||||
const maxRounds = 8;
|
||||
async function optimize(bitmapIn, encode, decode) {
|
||||
const visdifModule = require("../codecs/visdif/visdif.js");
|
||||
let quality = 50;
|
||||
let inc = 25;
|
||||
let butteraugliDistance = 2;
|
||||
let attempts = 0;
|
||||
let bitmapOut;
|
||||
let binaryOut;
|
||||
|
||||
const { VisDiff } = await visdifModule();
|
||||
const comparator = new VisDiff(
|
||||
bitmapIn.data,
|
||||
bitmapIn.width,
|
||||
bitmapIn.height
|
||||
);
|
||||
do {
|
||||
binaryOut = await encode(bitmapIn, quality);
|
||||
bitmapOut = await decode(binaryOut);
|
||||
butteraugliDistance = comparator.distance(bitmapOut.data);
|
||||
console.log({
|
||||
butteraugliDistance,
|
||||
quality,
|
||||
attempts,
|
||||
binaryOut,
|
||||
bitmapOut
|
||||
});
|
||||
if (butteraugliDistance > butteraugliGoal) {
|
||||
quality += inc;
|
||||
} else {
|
||||
quality -= inc;
|
||||
}
|
||||
inc /= 2;
|
||||
attempts++;
|
||||
} while (
|
||||
Math.abs(butteraugliDistance - butteraugliGoal) > 0.1 &&
|
||||
attempts < maxRounds
|
||||
);
|
||||
|
||||
comparator.delete();
|
||||
|
||||
return {
|
||||
bitmap: bitmapOut,
|
||||
binary: binaryOut,
|
||||
quality,
|
||||
attempts
|
||||
};
|
||||
}*/
|
||||
|
||||
Reference in New Issue
Block a user