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 { promises as fsp } from "fs";
|
||||||
|
import { instantiateEmscriptenWasm } from "./emscripten-utils.js";
|
||||||
|
|
||||||
// MozJPEG
|
// MozJPEG
|
||||||
import mozEnc from "../../codecs/mozjpeg/enc/mozjpeg_enc.js";
|
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 avifDec from "../../codecs/avif/dec/avif_dec.js";
|
||||||
import avifDecWasm from "asset-url:../../codecs/avif/dec/avif_dec.wasm";
|
import avifDecWasm from "asset-url:../../codecs/avif/dec/avif_dec.wasm";
|
||||||
|
|
||||||
function instantiateEmscriptenWasm(factory, path) {
|
// Our decoders currently rely on a `ImageData` global.
|
||||||
if (path.startsWith("file://")) {
|
import ImageData from "./image_data.js";
|
||||||
path = path.slice("file://".length);
|
globalThis.ImageData = ImageData;
|
||||||
}
|
|
||||||
return factory({
|
|
||||||
locateFile() {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mozjpeg: {
|
mozjpeg: {
|
||||||
@@ -53,6 +47,11 @@ export default {
|
|||||||
chroma_subsample: 2,
|
chroma_subsample: 2,
|
||||||
separate_chroma_quality: false,
|
separate_chroma_quality: false,
|
||||||
chroma_quality: 75
|
chroma_quality: 75
|
||||||
|
},
|
||||||
|
autoOptimize: {
|
||||||
|
option: "quality",
|
||||||
|
min: 0,
|
||||||
|
max: 100
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
webp: {
|
webp: {
|
||||||
@@ -89,6 +88,11 @@ export default {
|
|||||||
near_lossless: 100,
|
near_lossless: 100,
|
||||||
use_delta_palette: 0,
|
use_delta_palette: 0,
|
||||||
use_sharp_yuv: 0
|
use_sharp_yuv: 0
|
||||||
|
},
|
||||||
|
autoOptimize: {
|
||||||
|
option: "quality",
|
||||||
|
min: 0,
|
||||||
|
max: 100
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
avif: {
|
avif: {
|
||||||
@@ -104,6 +108,11 @@ export default {
|
|||||||
tileRowsLog2: 0,
|
tileRowsLog2: 0,
|
||||||
speed: 10,
|
speed: 10,
|
||||||
subsample: 0
|
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 supportedFormats from "./codecs.js";
|
||||||
import WorkerPool from "./worker_pool.js";
|
import WorkerPool from "./worker_pool.js";
|
||||||
|
import { autoOptimize } from "./auto-optimizer.js";
|
||||||
// Our decoders currently rely on a `ImageData` global.
|
|
||||||
import ImageData from "./image_data.js";
|
|
||||||
globalThis.ImageData = ImageData;
|
|
||||||
|
|
||||||
function clamp(v, min, max) {
|
function clamp(v, min, max) {
|
||||||
if (v < min) return min;
|
if (v < min) return min;
|
||||||
@@ -51,18 +48,49 @@ async function decodeFile(file) {
|
|||||||
async function encodeFile({
|
async function encodeFile({
|
||||||
file,
|
file,
|
||||||
size,
|
size,
|
||||||
bitmap,
|
bitmap: bitmapIn,
|
||||||
outputFile,
|
outputFile,
|
||||||
encName,
|
encName,
|
||||||
encConfig
|
encConfig
|
||||||
}) {
|
}) {
|
||||||
|
let out;
|
||||||
const encoder = await supportedFormats[encName].enc();
|
const encoder = await supportedFormats[encName].enc();
|
||||||
const out = encoder.encode(
|
if (encConfig === "auto") {
|
||||||
bitmap.data.buffer,
|
const optionToOptimize = supportedFormats[encName].autoOptimize.option;
|
||||||
bitmap.width,
|
const decoder = await supportedFormats[encName].dec();
|
||||||
bitmap.height,
|
const encode = (bitmapIn, quality) =>
|
||||||
encConfig
|
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);
|
await fsp.writeFile(outputFile, out);
|
||||||
return {
|
return {
|
||||||
inputSize: size,
|
inputSize: size,
|
||||||
@@ -89,11 +117,14 @@ async function processFiles(files) {
|
|||||||
if (!program[encName]) {
|
if (!program[encName]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const encConfig = Object.assign(
|
const encConfig =
|
||||||
{},
|
program[encName].toLowerCase() === "auto"
|
||||||
value.defaultEncoderOptions,
|
? "auto"
|
||||||
JSON5.parse(program[encName])
|
: Object.assign(
|
||||||
);
|
{},
|
||||||
|
value.defaultEncoderOptions,
|
||||||
|
JSON5.parse(program[encName])
|
||||||
|
);
|
||||||
const outputFile = join(program.outputDir, `${base}.${value.extension}`);
|
const outputFile = join(program.outputDir, `${base}.${value.extension}`);
|
||||||
jobsStarted++;
|
jobsStarted++;
|
||||||
workerPool
|
workerPool
|
||||||
@@ -147,54 +178,3 @@ if (isMainThread) {
|
|||||||
} else {
|
} else {
|
||||||
WorkerPool.useThisThreadAsWorker(encodeFile);
|
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