Add auto optimizer

This commit is contained in:
Surma
2020-09-15 17:53:49 +01:00
parent 1d7b6ab13e
commit c8dc88f8a1
4 changed files with 144 additions and 77 deletions

67
cli/src/auto-optimizer.js Normal file
View 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
};
}

View File

@@ -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
}
}
};

View File

@@ -0,0 +1,11 @@
export function instantiateEmscriptenWasm(factory, path) {
if (path.startsWith("file://")) {
path = path.slice("file://".length);
}
return factory({
locateFile() {
return path;
}
});
}

View File

@@ -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
};
}*/