UNPKG

@thi.ng/pixel-analysis

Version:

Image color & feature analysis utilities

137 lines (136 loc) 4.86 kB
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 };