@thi.ng/pixel-analysis 0.4.5 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Change Log
2
2
 
3
- - **Last updated**: 2025-07-21T08:21:58Z
3
+ - **Last updated**: 2025-07-24T19:45:06Z
4
4
  - **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
5
5
 
6
6
  All notable changes to this project will be documented in this file.
@@ -11,6 +11,23 @@ See [Conventional Commits](https://conventionalcommits.org/) for commit guidelin
11
11
  **Note:** Unlisted _patch_ versions only involve non-code or otherwise excluded changes
12
12
  and/or version bumps of transitive dependencies.
13
13
 
14
+ # [1.0.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/pixel-analysis@1.0.0) (2025-07-24)
15
+
16
+ #### 🛑 Breaking changes
17
+
18
+ - major update/rewrite hue range analysis fns ([5730fea](https://github.com/thi-ng/umbrella/commit/5730fea))
19
+ - BREAKING CHANGE: replace/update hue range analysis fns
20
+ - major update/rewrite hue range analysis fns ([9b12898](https://github.com/thi-ng/umbrella/commit/9b12898))
21
+ - BREAKING CHANGE: update AnalyzedImage result, update color analysis fns
22
+ - update structure & details of `AnalyzedImage` result type
23
+ - update `derivedColorsResults()`
24
+ - update/split `analyzeColors()`
25
+ - replace temperature calculation ([2a23cfa](https://github.com/thi-ng/umbrella/commit/2a23cfa))
26
+ - BREAKING CHANGE: replace temperature calculation with whole new approach
27
+ - replace existing `temperature()` with new method/approach
28
+ - add `TemperatureResult`
29
+ - update `analyzeColors()`, `derivedColorsResult()`
30
+
14
31
  ### [0.4.1](https://github.com/thi-ng/umbrella/tree/@thi.ng/pixel-analysis@0.4.1) (2025-07-15)
15
32
 
16
33
  #### ♻️ Refactoring
package/README.md CHANGED
@@ -30,23 +30,24 @@ Image color & feature analysis utilities. This is a support package for [@thi.ng
30
30
  ### Color analysis
31
31
 
32
32
  - Dominant colors in different color modes/formats:
33
- - CSS
33
+ - CSS (hex)
34
34
  - sRGB
35
35
  - HSV
36
36
  - Oklch (perceptual)
37
37
  - Normalized areas of dominant color clusters
38
- - Min/max HSV hue range of dominant colors
38
+ - Min/max HSV hue range of dominant colors (considering angular wrap-around)
39
39
  - Min/max HSV saturation range of dominant colors
40
- - Min/max Oklch chroma range of dominant colors
41
40
  - Min/max luminance range of dominant colors (obtained from SRGB)
42
41
  - Min/max luminance range of entire grayscale image (obtained from SRGB)
43
- - Normalized warmth, i.e. the area-weighted intensity of "warm" colors in the image
42
+ - Average values for hue (circular mean), saturation, luminance
43
+ - Area-weighted average saturation of dominant colors
44
+ - Area-weighted average luminance of dominant colors
45
+ - Standard deviation for hue, saturation, luminance
46
+ - Normalized color temperature, incl. area-weighted indicator of "cold" vs
47
+ "warm" hues present
44
48
  - Luminance contrast of dominant colors
45
49
  - Luminance contrast of entire grayscale image
46
50
  - Max. normalized WCAG color contrast of dominant colors
47
- - Average luminance of dominant colors, weighted by area
48
- - Average HSV saturation of dominant colors, weighted by area
49
- - Average Oklch chroma of dominant colors, weighted by area
50
51
 
51
52
  ## Status
52
53
 
@@ -80,11 +81,12 @@ For Node.js REPL:
80
81
  const pa = await import("@thi.ng/pixel-analysis");
81
82
  ```
82
83
 
83
- Package sizes (brotli'd, pre-treeshake): ESM: 1.36 KB
84
+ Package sizes (brotli'd, pre-treeshake): ESM: 1.70 KB
84
85
 
85
86
  ## Dependencies
86
87
 
87
88
  - [@thi.ng/api](https://github.com/thi-ng/umbrella/tree/develop/packages/api)
89
+ - [@thi.ng/arrays](https://github.com/thi-ng/umbrella/tree/develop/packages/arrays)
88
90
  - [@thi.ng/color](https://github.com/thi-ng/umbrella/tree/develop/packages/color)
89
91
  - [@thi.ng/compare](https://github.com/thi-ng/umbrella/tree/develop/packages/compare)
90
92
  - [@thi.ng/math](https://github.com/thi-ng/umbrella/tree/develop/packages/math)
@@ -14,5 +14,5 @@ export declare const analyzeColors: (img: FloatBuffer | IntBuffer, opts?: Partia
14
14
  *
15
15
  * @param colors
16
16
  */
17
- export declare const derivedColorsResults: (colors: number[][]) => Pick<AnalyzedImage, "css" | "srgb" | "hsv" | "oklch" | "hueRange" | "satRange" | "chromaRange" | "lumaRange" | "contrast" | "colorContrast">;
17
+ export declare const derivedColorsResult: (colors: number[][], minSat?: number) => Pick<AnalyzedImage, "css" | "srgb" | "hsv" | "oklch" | "mean" | "sd" | "range" | "contrast" | "colorContrast" | "temperature">;
18
18
  //# sourceMappingURL=analyze-colors.d.ts.map
package/analyze-colors.js CHANGED
@@ -6,6 +6,7 @@ import { oklch } from "@thi.ng/color/oklch/oklch";
6
6
  import { srgb } from "@thi.ng/color/srgb/srgb";
7
7
  import { compareByKey } from "@thi.ng/compare/keys";
8
8
  import { compareNumDesc } from "@thi.ng/compare/numeric";
9
+ import { TAU } from "@thi.ng/math/api";
9
10
  import { fit } from "@thi.ng/math/fit";
10
11
  import { dominantColorsKmeans } from "@thi.ng/pixel-dominant-colors/kmeans";
11
12
  import { FloatBuffer } from "@thi.ng/pixel/float";
@@ -20,30 +21,22 @@ import { permutations } from "@thi.ng/transducers/permutations";
20
21
  import { pluck } from "@thi.ng/transducers/pluck";
21
22
  import { reduce } from "@thi.ng/transducers/reduce";
22
23
  import { transduce } from "@thi.ng/transducers/transduce";
24
+ import { circularMean, circularSD } from "@thi.ng/vectors/circular";
23
25
  import { dot } from "@thi.ng/vectors/dot";
26
+ import { vmean } from "@thi.ng/vectors/mean";
24
27
  import { roundN } from "@thi.ng/vectors/roundn";
25
- import { warmIntensityHsv } from "./hues.js";
28
+ import { sd } from "@thi.ng/vectors/variance";
29
+ import { temperature } from "./hues.js";
26
30
  const analyzeColors = (img, opts) => {
27
31
  let $img = img.format !== FLOAT_RGBA ? img.as(FLOAT_RGBA) : img;
28
- if (opts?.size) {
29
- const size = ~~opts.size;
30
- let w = $img.width;
31
- let h = $img.height;
32
- [w, h] = w > h ? [size, ~~Math.max(1, h / w * size)] : [~~Math.max(1, w / h * size), size];
33
- $img = $img.resize(w, h);
34
- }
32
+ if (opts?.size) $img = __resize($img, opts.size);
35
33
  const imgGray = $img.as(FLOAT_GRAY);
36
34
  const imgHsv = $img.as(FLOAT_HSVA);
37
- const dominantColors = opts?.dominantFn ?? dominantColorsKmeans;
38
- const prec = opts?.prec ?? 1e-3;
39
- const colors = dominantColors($img, opts?.numColors ?? 4).sort(compareByKey("area", compareNumDesc)).map((x) => {
40
- roundN(null, x.color, prec);
41
- return x;
42
- });
35
+ const colors = __dominantColors($img, opts);
43
36
  const colorAreas = colors.map((x) => x.area);
44
- const derived = derivedColorsResults(colors.map((x) => x.color));
37
+ const derived = derivedColorsResult(colors.map((x) => x.color));
45
38
  const lumaRangeImg = reduce(minMax(), imgGray.data);
46
- const weightedLuma = dot(derived.lumaRange, colorAreas);
39
+ const weightedLuma = dot(derived.range.luma, colorAreas);
47
40
  const weightedChroma = dot(
48
41
  derived.oklch.map((x) => x[1]),
49
42
  colorAreas
@@ -58,7 +51,7 @@ const analyzeColors = (img, opts) => {
58
51
  imgHsv,
59
52
  ...derived,
60
53
  area: colorAreas,
61
- warmth: warmIntensityHsv(imgHsv, opts?.minSat),
54
+ temperature: temperature(imgHsv, opts?.minSat),
62
55
  contrastImg: lumaRangeImg[1] - lumaRangeImg[0],
63
56
  lumaRangeImg,
64
57
  weightedSat,
@@ -66,42 +59,66 @@ const analyzeColors = (img, opts) => {
66
59
  weightedChroma
67
60
  };
68
61
  };
69
- const derivedColorsResults = (colors) => {
62
+ const derivedColorsResult = (colors, minSat) => {
70
63
  const dominantLuma = colors.map((x) => luminanceSrgb(x));
71
64
  const dominantSrgb = colors.map((x) => srgb(x));
72
65
  const dominantHsv = dominantSrgb.map((x) => hsv(x));
73
66
  const dominantOklch = dominantSrgb.map((x) => oklch(x));
74
67
  const dominantCss = dominantSrgb.map((x) => css(x));
75
- const hueRange = transduce(pluck(0), minMax(), dominantHsv);
76
- const satRange = transduce(pluck(1), minMax(), dominantHsv);
77
- const chromaRange = transduce(pluck(1), minMax(), dominantOklch);
68
+ const $hueRange = transduce(pluck(0), minMax(), dominantHsv);
69
+ const hues = dominantHsv.map((x) => x[0] * TAU);
70
+ const sats = dominantHsv.map((x) => x[1]);
71
+ const meanHue = circularMean(hues) / TAU;
72
+ const hueRange = meanHue > $hueRange[1] || meanHue < $hueRange[0] ? [$hueRange[1], $hueRange[0]] : $hueRange;
78
73
  const lumaRange = reduce(minMax(), dominantLuma);
79
- const contrast = lumaRange[1] - lumaRange[0];
80
- const colorContrast = fit(
81
- transduce(
82
- map((pair) => contrastWCAG(...pair)),
83
- max(),
84
- permutations(dominantSrgb, dominantSrgb)
85
- ),
86
- 1,
87
- 21,
88
- 0,
89
- 1
90
- );
91
74
  return {
92
75
  css: dominantCss,
93
76
  srgb: dominantSrgb,
94
77
  hsv: dominantHsv,
95
78
  oklch: dominantOklch,
96
- hueRange,
97
- satRange,
98
- chromaRange,
99
- lumaRange,
100
- contrast,
101
- colorContrast
79
+ mean: {
80
+ hue: meanHue,
81
+ sat: vmean(sats),
82
+ luma: vmean(dominantLuma)
83
+ },
84
+ sd: {
85
+ hue: circularSD(hues),
86
+ sat: sd(sats),
87
+ luma: sd(dominantLuma)
88
+ },
89
+ range: {
90
+ hue: hueRange,
91
+ sat: reduce(minMax(), sats),
92
+ luma: lumaRange
93
+ },
94
+ contrast: lumaRange[1] - lumaRange[0],
95
+ colorContrast: fit(
96
+ transduce(
97
+ map((pair) => contrastWCAG(...pair)),
98
+ max(),
99
+ permutations(dominantSrgb, dominantSrgb)
100
+ ),
101
+ 1,
102
+ 21,
103
+ 0,
104
+ 1
105
+ ),
106
+ temperature: temperature(dominantHsv, minSat)
102
107
  };
103
108
  };
109
+ const __dominantColors = (img, {
110
+ dominantFn = dominantColorsKmeans,
111
+ numColors = 4,
112
+ prec = 1e-3
113
+ } = {}) => dominantFn(img, numColors).sort(compareByKey("area", compareNumDesc)).map((x) => (roundN(null, x.color, prec), x));
114
+ const __resize = ($img, size) => {
115
+ size = ~~size;
116
+ let w = $img.width;
117
+ let h = $img.height;
118
+ [w, h] = w > h ? [size, ~~Math.max(1, h / w * size)] : [~~Math.max(1, w / h * size), size];
119
+ return $img.resize(w, h);
120
+ };
104
121
  export {
105
122
  analyzeColors,
106
- derivedColorsResults
123
+ derivedColorsResult
107
124
  };
package/api.d.ts CHANGED
@@ -17,7 +17,7 @@ export interface AnalysisOpts {
17
17
  */
18
18
  size: number;
19
19
  /**
20
- * Min. saturation to consider for computing {@link warmIntensity}.
20
+ * Min. saturation to consider for computing {@link temperature}.
21
21
  */
22
22
  minSat: number;
23
23
  /**
@@ -60,31 +60,60 @@ export interface AnalyzedImage {
60
60
  * Normalized areas of dominant color clusters
61
61
  */
62
62
  area: number[];
63
- /**
64
- * Min/max HSV hue range of dominant colors
65
- */
66
- hueRange: [number, number];
67
- /**
68
- * Min/max HSV saturation range of dominant colors
69
- */
70
- satRange: [number, number];
71
- /**
72
- * Min/max Oklch chroma range of dominant colors
73
- */
74
- chromaRange: [number, number];
75
- /**
76
- * Min/max luminance range of dominant colors (obtained from SRGB)
77
- */
78
- lumaRange: [number, number];
63
+ mean: {
64
+ /**
65
+ * Normalized mean hue (using circular mean).
66
+ */
67
+ hue: number;
68
+ /**
69
+ * Mean saturation
70
+ */
71
+ sat: number;
72
+ /**
73
+ * Mean luminance
74
+ */
75
+ luma: number;
76
+ };
77
+ sd: {
78
+ /**
79
+ * Circular standard deviation of normalized hues.
80
+ */
81
+ hue: number;
82
+ /**
83
+ * Standard deviation of normalized saturation.
84
+ */
85
+ sat: number;
86
+ /**
87
+ * Standard deviation of normalized luminance.
88
+ */
89
+ luma: number;
90
+ };
91
+ range: {
92
+ /**
93
+ * Min/max HSV hue range of dominant colors. IMPORTANT: In case of
94
+ * circular overflow (360 => 0 degrees), the min hue WILL be greater
95
+ * than the max hue (e.g. a hue range of `[0.8, 0.2]` indicates the hue
96
+ * range from magenta -> orange). Also see {@link AnalyzedImage.mean.hue}.
97
+ */
98
+ hue: [number, number];
99
+ /**
100
+ * Min/max HSV saturation range of dominant colors
101
+ */
102
+ sat: [number, number];
103
+ /**
104
+ * Min/max luminance range of dominant colors (obtained from SRGB)
105
+ */
106
+ luma: [number, number];
107
+ };
79
108
  /**
80
109
  * Min/max luminance range of entire grayscale image (obtained from SRGB)
81
110
  */
82
111
  lumaRangeImg: [number, number];
83
112
  /**
84
- * Normalized warmth, i.e. the area-weighted intensity of "warm" colors in
85
- * the image (see {@link warmIntensity} and {@link AnalysisOpts.minSat}).
113
+ * Comprehensive {@link TemperatureResult} as produced by
114
+ * {@link temperature}. Also see {@link AnalysisOpts.minSat}.
86
115
  */
87
- warmth: number;
116
+ temperature: TemperatureResult;
88
117
  /**
89
118
  * Luminance contrast of dominant colors (i.e. delta of
90
119
  * {@link AnalyzedImage.lumaRange}).
@@ -112,4 +141,34 @@ export interface AnalyzedImage {
112
141
  */
113
142
  weightedChroma: number;
114
143
  }
144
+ /**
145
+ * Result type for {@link temparature}.
146
+ */
147
+ export interface TemperatureResult {
148
+ /**
149
+ * Hue-based histogram (up to 12 bins), each item: `[hue, count]` (all
150
+ * normalized).
151
+ */
152
+ hues: [number, number][];
153
+ /**
154
+ * Normalized weighted mean hue, based on histogram distribution.
155
+ */
156
+ meanHue: number;
157
+ /**
158
+ * Normalized abstract color temperature (see {@link hueTemperature}) based
159
+ * on weighted {@link TemperatureResult.meanHue}. Red/orange/yellow hues
160
+ * produce results close to 1.0, cyan/blue/purple hues produce results
161
+ * closer to -1.0.
162
+ */
163
+ temp: number;
164
+ /**
165
+ * {@link TemperatureResult.temp} weighted by {@link TemperatureResult.area}
166
+ */
167
+ areaTemp: number;
168
+ /**
169
+ * Normalized area (percentage) of the filtered (sufficiently) saturated
170
+ * colors which were used to compute the histogram & temperature.
171
+ */
172
+ area: number;
173
+ }
115
174
  //# sourceMappingURL=api.d.ts.map
package/hues.d.ts CHANGED
@@ -1,44 +1,127 @@
1
- import type { FloatBuffer } from "@thi.ng/pixel/float";
2
- import type { IntBuffer } from "@thi.ng/pixel/int";
1
+ import type { ReadonlyVec } from "@thi.ng/vectors";
2
+ import type { TemperatureResult } from "./api.js";
3
3
  /**
4
- * Iterator yielding HSV pixel values/colors matching given hue range and
5
- * minimum saturation.
4
+ * Iterator consuming HSV colors and only yielding those matching given hue
5
+ * range and minimum saturation (all normalized in [0,1] range).
6
6
  *
7
- * @param img
7
+ * @remarks
8
+ * If given a pixel buffer as input, it MUST be in `FLOAT_HSVA` format.
9
+ *
10
+ * If `minHue` is greater than `maxHue`, the range will be interpreted to wrap
11
+ * around at 1. E.g. the range `[0.8, 0.2]` selects hues >= 0.8 and hues
12
+ * < 0.2.
13
+ *
14
+ * @param colors
15
+ * @param minHue
16
+ * @param maxHue
17
+ * @param minSat
18
+ */
19
+ export declare function selectHueRange(colors: Iterable<ReadonlyVec>, minHue: number, maxHue: number, minSat: number): Generator<ReadonlyVec, void, unknown>;
20
+ /**
21
+ * Similar to {@link selectHueRange}, but only yields indices/IDs of matching
22
+ * colors.
23
+ *
24
+ * @remarks
25
+ * See {@link selectHueRange} for details about hue ranges.
26
+ *
27
+ * @param colors
8
28
  * @param minHue
9
29
  * @param maxHue
10
30
  * @param minSat
11
31
  */
12
- export declare function selectHueRangeHsv(img: IntBuffer | FloatBuffer, minHue: number, maxHue: number, minSat: number): Generator<Uint8Array<ArrayBufferLike> | Float32Array<ArrayBufferLike> | Uint8ClampedArray<ArrayBufferLike> | Uint16Array<ArrayBufferLike> | Uint32Array<ArrayBufferLike>, void, unknown>;
32
+ export declare function selectHueRangeIDs(colors: Iterable<ReadonlyVec>, minHue: number, maxHue: number, minSat: number): Generator<number, void, unknown>;
13
33
  /**
14
- * Similar to {@link selectHueRangeHsv}, but only returns a count of HSV pixel
15
- * values/colors matching given hue range and minimum saturation (all
16
- * normalized).
34
+ * Similar to {@link selectHueRange}, but only returns a count of inputs
35
+ * matching given hue range and minimum saturation (all normalized in [0,1]
36
+ * range).
17
37
  *
18
- * @param img
38
+ * @remarks
39
+ * See {@link selectHueRange} for details about hue ranges.
40
+ *
41
+ * @param colors
19
42
  * @param minHue
20
43
  * @param maxHue
21
44
  * @param minSat
22
45
  */
23
- export declare const countHueRangeHsv: (img: IntBuffer | FloatBuffer, minHue: number, maxHue: number, minSat: number) => number;
46
+ export declare const countHueRange: (colors: Iterable<ReadonlyVec>, minHue: number, maxHue: number, minSat: number) => number;
47
+ /**
48
+ * Takes a list of HSV colors, a list of min/max hue ranges and a min saturation
49
+ * (all normalized in [0,1] range). Computes the normalized area of all matching
50
+ * colors.
51
+ *
52
+ * @remarks
53
+ * If given a pixel buffer as input, it MUST be in `FLOAT_HSVA` format. Also see
54
+ * {@link temperature}.
55
+ *
56
+ * See {@link selectHueRange} for details about hue ranges.
57
+ *
58
+ * @param colors
59
+ * @param hueRanges
60
+ * @param minSat
61
+ */
62
+ export declare const hueRangeArea: (colors: Iterable<ReadonlyVec>, hueRanges: [number, number][], minSat?: number) => number;
24
63
  /**
25
64
  * Takes a list of hue ranges and computes the area-weighted mean intensity of
26
- * matching pixels in the given image. Also see {@link warmIntensityHsv}.
65
+ * matching pixels in the given image. Also see {@link temperatureIntensity}.
27
66
  *
28
67
  * @remarks
68
+ * If given a pixel buffer as input, it MUST be in `FLOAT_HSVA` format.
69
+ *
29
70
  * Intensity here means the product of HSV `saturation * brightness`.
30
71
  *
31
- * @param img
32
- * @param hues
72
+ * See {@link selectHueRange} for details about hue ranges.
73
+ *
74
+ * @param colors
75
+ * @param hueRanges
33
76
  * @param minSat
34
77
  */
35
- export declare const hueRangeIntensityHsv: (img: IntBuffer | FloatBuffer, hues: [number, number][], minSat?: number) => number;
78
+ export declare const hueRangeAreaIntensity: (colors: Iterable<ReadonlyVec>, hueRanges: [number, number][], minSat?: number) => number;
79
+ /**
80
+ * Computes the average intensity of given HSV colors. Intensity here means the
81
+ * product of `saturation * value` (ignoring hues).
82
+ *
83
+ * @remarks
84
+ * If given a pixel buffer as input, it MUST be in `FLOAT_HSVA` format.
85
+ *
86
+ * @param colors
87
+ */
88
+ export declare const meanIntensity: (colors: Iterable<ReadonlyVec>) => number;
36
89
  /**
37
- * Syntax sugar for {@link hueRangeIntensityHsv} to compute the area-weighted mean
38
- * intensity of pixels in the yellow/orange/red hue ranges.
90
+ * Computes an abstract measure of a normalized "color temperature" of the given
91
+ * HSV `colors` (also normalized in [0,1] range). Results closer to 1.0 indicate
92
+ * a prevalence of warmer colors, results closer to -1.0 mean more
93
+ * colder/blue-ish colors present.
39
94
  *
40
- * @param img
95
+ * @remarks
96
+ * Computation is as follows:
97
+ * - Discard colors with lower saturation than given `minSat`
98
+ * - Compute normalized area (percentage) of qualifying saturated colors
99
+ * - Compute the histogram for 12 evenly spread hues
100
+ * - Produce a weighted list of hue angles, based on histogram
101
+ * - Compute circular mean of hue angles
102
+ * - Use {@link hueTemperature} to compute normalized temperature in [-1,1] range
103
+ * - Scale temparature by normlized area
104
+ * - Return object of the various result metrics
105
+ *
106
+ * If given a pixel buffer as input, it MUST be in `FLOAT_HSVA` format.
107
+ *
108
+ * Also see {@link temperatureIntensity}.
109
+ *
110
+ * @param colors
41
111
  * @param minSat
42
112
  */
43
- export declare const warmIntensityHsv: (img: IntBuffer | FloatBuffer, minSat?: number) => number;
113
+ export declare const temperature: (colors: Iterable<ReadonlyVec>, minSat?: number) => TemperatureResult;
114
+ /**
115
+ * Computes an abstract measure of a normalized "color temperature" ([-1,1]
116
+ * range) for the given `hue` (in [0,1] range). Red/orange/yellow hues produce
117
+ * results close to 1.0, cyan/blue/purple-ish hues produce results closer to
118
+ * -1.0.
119
+ *
120
+ * @remarks
121
+ * Uses one of two smoothstep curves for non-linear interpolation, depending on
122
+ * input hue.
123
+ *
124
+ * @param hue
125
+ */
126
+ export declare const hueTemperature: (hue: number) => number;
44
127
  //# sourceMappingURL=hues.d.ts.map
package/hues.js CHANGED
@@ -1,56 +1,95 @@
1
- import { FLOAT_HSVA } from "@thi.ng/pixel/format/float-hsva";
1
+ import { ensureArray } from "@thi.ng/arrays/ensure-array";
2
+ import { compareByKey } from "@thi.ng/compare";
3
+ import { roundTo, smoothStep, TAU } from "@thi.ng/math";
4
+ import { normFrequenciesAuto, repeat, transduce } from "@thi.ng/transducers";
2
5
  import { map } from "@thi.ng/transducers/map";
3
6
  import { mapcat } from "@thi.ng/transducers/mapcat";
4
7
  import { mean } from "@thi.ng/transducers/mean";
5
- import { transduce } from "@thi.ng/transducers/transduce";
6
- function* selectHueRangeHsv(img, minHue, maxHue, minSat) {
7
- if (img.format !== FLOAT_HSVA) img = img.as(FLOAT_HSVA);
8
- const {
9
- data,
10
- stride: [stride]
11
- } = img;
12
- const pred = minHue <= maxHue ? (i) => data[i] >= minHue && data[i] < maxHue : (i) => data[i] >= minHue || data[i] < maxHue;
13
- for (let i = 0, n = data.length; i < n; i += stride) {
14
- if (data[i + 1] >= minSat && pred(i)) yield data.subarray(i, i + 4);
8
+ import { circularMean } from "@thi.ng/vectors/circular";
9
+ function* selectHueRange(colors, minHue, maxHue, minSat) {
10
+ const pred = __hueSelector(minHue, maxHue);
11
+ for (let col of colors) {
12
+ if (col[1] >= minSat && pred(col[0])) yield col;
15
13
  }
16
14
  }
17
- const countHueRangeHsv = (img, minHue, maxHue, minSat) => {
18
- if (img.format !== FLOAT_HSVA) img = img.as(FLOAT_HSVA);
19
- const {
20
- data,
21
- stride: [stride]
22
- } = img;
23
- const pred = minHue <= maxHue ? (i) => data[i] >= minHue && data[i] < maxHue : (i) => data[i] >= minHue || data[i] < maxHue;
15
+ function* selectHueRangeIDs(colors, minHue, maxHue, minSat) {
16
+ const pred = __hueSelector(minHue, maxHue);
17
+ let id = 0;
18
+ for (let col of colors) {
19
+ if (col[1] >= minSat && pred(col[0])) yield id;
20
+ id++;
21
+ }
22
+ }
23
+ const countHueRange = (colors, minHue, maxHue, minSat) => {
24
+ const pred = __hueSelector(minHue, maxHue);
24
25
  let count = 0;
25
- for (let i = 0, n = data.length; i < n; i += stride) {
26
- if (data[i + 1] >= minSat && pred(i)) count++;
26
+ for (let col of colors) {
27
+ if (col[1] >= minSat && pred(col[0])) count++;
27
28
  }
28
29
  return count;
29
30
  };
30
- const hueRangeIntensityHsv = (img, hues, minSat = 0.2) => {
31
- if (img.format !== FLOAT_HSVA) img = img.as(FLOAT_HSVA);
32
- const selected = [
33
- ...mapcat((range) => selectHueRangeHsv(img, ...range, minSat), hues)
34
- ];
35
- const area = selected.length / (img.data.length / 4);
31
+ const __hueSelector = (min, max) => min <= max ? (h) => h >= min && h < max : (h) => h >= min || h < max;
32
+ const hueRangeArea = (colors, hueRanges, minSat = 0.2) => {
33
+ const $img = ensureArray(colors);
34
+ const selected = new Set(
35
+ mapcat((range) => selectHueRangeIDs($img, ...range, minSat), hueRanges)
36
+ );
37
+ return selected.size / $img.length;
38
+ };
39
+ const hueRangeAreaIntensity = (colors, hueRanges, minSat = 0.2) => {
40
+ const $colors = ensureArray(colors);
41
+ const selected = new Set(
42
+ mapcat(
43
+ (range) => selectHueRangeIDs($colors, ...range, minSat),
44
+ hueRanges
45
+ )
46
+ );
47
+ const area = selected.size / $colors.length;
36
48
  const intensity = transduce(
37
- map((x) => x[1] * x[2]),
49
+ map((id) => {
50
+ const color = $colors[id];
51
+ return color[1] * color[2];
52
+ }),
38
53
  mean(),
39
54
  selected
40
55
  );
41
56
  return intensity * area;
42
57
  };
43
- const warmIntensityHsv = (img, minSat) => hueRangeIntensityHsv(
44
- img,
45
- [
46
- [345 / 360, 55 / 360]
47
- // red, orange, yellow
48
- ],
49
- minSat
58
+ const meanIntensity = (colors) => transduce(
59
+ map((x) => x[1] * x[2]),
60
+ mean(),
61
+ colors
50
62
  );
63
+ const temperature = (colors, minSat = 0.2) => {
64
+ const $colors = ensureArray(colors);
65
+ const filtered = $colors.filter((x) => x[1] >= minSat);
66
+ const area = filtered.length / $colors.length;
67
+ const hues = [
68
+ ...transduce(
69
+ map((x) => roundTo(x[0], 1 / 12) % 1),
70
+ normFrequenciesAuto(),
71
+ filtered
72
+ )
73
+ ].sort(compareByKey(0));
74
+ const angles = [
75
+ ...mapcat(([hue, num]) => {
76
+ num *= 50;
77
+ return num >= 1 ? repeat(hue * TAU, num) : null;
78
+ }, hues)
79
+ ];
80
+ const meanHue = circularMean(angles) / TAU;
81
+ const temp = hueTemperature(meanHue);
82
+ const areaTemp = temp * area;
83
+ return { hues, meanHue, temp, areaTemp, area };
84
+ };
85
+ const hueTemperature = (hue) => 2 * (hue < 2 / 3 ? smoothStep(0.6, 0.1, hue) : smoothStep(0.72, 0.92, hue)) - 1;
51
86
  export {
52
- countHueRangeHsv,
53
- hueRangeIntensityHsv,
54
- selectHueRangeHsv,
55
- warmIntensityHsv
87
+ countHueRange,
88
+ hueRangeArea,
89
+ hueRangeAreaIntensity,
90
+ hueTemperature,
91
+ meanIntensity,
92
+ selectHueRange,
93
+ selectHueRangeIDs,
94
+ temperature
56
95
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thi.ng/pixel-analysis",
3
- "version": "0.4.5",
3
+ "version": "1.0.0",
4
4
  "description": "Image color & feature analysis utilities",
5
5
  "type": "module",
6
6
  "module": "./index.js",
@@ -40,14 +40,15 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@thi.ng/api": "^8.11.32",
43
- "@thi.ng/color": "^5.7.50",
43
+ "@thi.ng/arrays": "^2.13.5",
44
+ "@thi.ng/color": "^5.7.51",
44
45
  "@thi.ng/compare": "^2.4.24",
45
46
  "@thi.ng/math": "^5.11.32",
46
47
  "@thi.ng/pixel": "^7.5.4",
47
48
  "@thi.ng/pixel-convolve": "^1.1.4",
48
- "@thi.ng/pixel-dominant-colors": "^2.0.5",
49
+ "@thi.ng/pixel-dominant-colors": "^2.0.6",
49
50
  "@thi.ng/transducers": "^9.6.3",
50
- "@thi.ng/vectors": "^8.4.1"
51
+ "@thi.ng/vectors": "^8.5.0"
51
52
  },
52
53
  "devDependencies": {
53
54
  "esbuild": "^0.25.8",
@@ -64,10 +65,14 @@
64
65
  "gradient",
65
66
  "grayscale",
66
67
  "hsv",
68
+ "hue",
67
69
  "luminance",
70
+ "mean",
68
71
  "oklch",
69
72
  "rgb",
70
73
  "saturation",
74
+ "standard-deviation",
75
+ "statistics",
71
76
  "temperature",
72
77
  "typescript"
73
78
  ],
@@ -107,5 +112,5 @@
107
112
  "status": "beta",
108
113
  "year": 2024
109
114
  },
110
- "gitHead": "11747c482773d3da03c8b7848b25a56251ccd759\n"
115
+ "gitHead": "c5c6b895169788c7f8b3d6d61ff76c0c6e5d2c2a\n"
111
116
  }