@thi.ng/pixel-analysis
Version:
Image color & feature analysis utilities
137 lines (136 loc) • 4.86 kB
JavaScript
import { contrast as contrastWCAG } from "@thi.ng/color/contrast";
import { css } from "@thi.ng/color/css/css";
import { hsv } from "@thi.ng/color/hsv/hsv";
import { luminanceSrgb } from "@thi.ng/color/luminance-rgb";
import { oklch } from "@thi.ng/color/oklch/oklch";
import { srgb } from "@thi.ng/color/srgb/srgb";
import { compareByKey } from "@thi.ng/compare/keys";
import { compareNumDesc } from "@thi.ng/compare/numeric";
import { fit } from "@thi.ng/math/fit";
import {
aggregateCircularMetrics,
aggregateMetrics,
aggregateWeightedMetrics,
defCircularMetric,
defMetric,
defWeightedMetric
} from "@thi.ng/metrics/metrics";
import { dominantColorsMeanCut } from "@thi.ng/pixel-dominant-colors";
import { dominantColorsKmeans } from "@thi.ng/pixel-dominant-colors/kmeans";
import { FloatBuffer } from "@thi.ng/pixel/float";
import { FLOAT_GRAY } from "@thi.ng/pixel/format/float-gray";
import { FLOAT_HSVA } from "@thi.ng/pixel/format/float-hsva";
import { FLOAT_RGBA } from "@thi.ng/pixel/format/float-rgba";
import { IntBuffer } from "@thi.ng/pixel/int";
import { map } from "@thi.ng/transducers/map";
import { max } from "@thi.ng/transducers/max";
import { permutations } from "@thi.ng/transducers/permutations";
import { transduce } from "@thi.ng/transducers/transduce";
import { roundN } from "@thi.ng/vectors/roundn";
import { ones } from "@thi.ng/vectors/setn";
import { temperature } from "./hues.js";
const analyzeColors = (img, opts) => {
let $img = img.format !== FLOAT_RGBA ? img.as(FLOAT_RGBA) : img;
if (opts?.size) $img = __resize($img, opts.size);
const imgGray = $img.as(FLOAT_GRAY);
const imgHsv = $img.as(FLOAT_HSVA);
const colors = __dominantColors($img, opts);
const colorAreas = colors.map((x) => x.area);
const derived = deriveColorResults(
colors.map((x) => x.color),
colorAreas,
opts?.minSat,
opts?.tempCoeffs
);
const lumImg = defMetric(imgGray.data);
return {
...derived,
img: $img,
imgGray,
imgHsv,
lumImg,
temperature: temperature(imgHsv, opts?.minSat, opts?.tempCoeffs),
contrastImg: lumImg.max - lumImg.min
};
};
const deriveColorResults = (colors, areas = ones(colors.length), minSat, tempCoeffs) => {
const dominantLuma = colors.map((x) => luminanceSrgb(x));
const dominantSrgb = colors.map((x) => srgb(x));
const dominantHsv = dominantSrgb.map((x) => hsv(x));
const dominantOklch = dominantSrgb.map((x) => oklch(x));
const dominantCss = dominantSrgb.map((x) => css(x));
const hues = dominantHsv.map((x) => x[0]);
const sats = dominantHsv.map((x) => x[1]);
const lum = defWeightedMetric(dominantLuma, areas);
return {
css: dominantCss,
srgb: dominantSrgb,
hsv: dominantHsv,
oklch: dominantOklch,
hue: defCircularMetric(hues),
sat: defWeightedMetric(sats, areas),
chroma: defWeightedMetric(
dominantOklch.map((x) => x[1]),
areas
),
lum,
areas,
contrast: lum.max - lum.min,
colorContrast: fit(
transduce(
map((pair) => contrastWCAG(...pair)),
max(),
permutations(dominantSrgb, dominantSrgb)
),
1,
21,
0,
1
),
temperature: temperature(dominantHsv, minSat, tempCoeffs)
};
};
const aggregateColorResults = (results, numColors = 4) => {
const dominant = dominantColorsMeanCut(
results.flatMap((res) => res.srgb),
numColors
).map((x) => srgb(x.color));
return {
srgb: dominant,
css: dominant.map((x) => css(x)),
hsv: dominant.map((x) => hsv(x)),
oklch: dominant.map((x) => oklch(x)),
hue: aggregateCircularMetrics(results.map((x) => x.hue)),
sat: aggregateWeightedMetrics(results.map((x) => x.sat)),
chroma: aggregateWeightedMetrics(results.map((x) => x.chroma)),
lum: aggregateWeightedMetrics(results.map((x) => x.lum)),
lumImg: aggregateMetrics(results.map((x) => x.lumImg)),
contrast: defMetric(results.map((x) => x.contrast)),
contrastImg: defMetric(results.map((x) => x.contrastImg)),
colorContrast: defMetric(results.map((x) => x.colorContrast)),
temperature: {
meanHue: defCircularMetric(
results.map((x) => x.temperature.meanHue)
),
temp: defMetric(results.map((x) => x.temperature.temp)),
areaTemp: defMetric(results.map((x) => x.temperature.areaTemp))
}
};
};
const __dominantColors = (img, {
dominantFn = dominantColorsKmeans,
numColors = 4,
prec = 1e-3
} = {}) => dominantFn(img, numColors).sort(compareByKey("area", compareNumDesc)).map((x) => (roundN(null, x.color, prec), x));
const __resize = ($img, size) => {
size = ~~size;
let w = $img.width;
let h = $img.height;
[w, h] = w > h ? [size, ~~Math.max(1, h / w * size)] : [~~Math.max(1, w / h * size), size];
return $img.resize(w, h);
};
export {
aggregateColorResults,
analyzeColors,
deriveColorResults
};