@thi.ng/pixel-analysis 0.4.5 → 1.0.1
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 +27 -1
- package/README.md +10 -8
- package/analyze-colors.d.ts +1 -1
- package/analyze-colors.js +57 -40
- package/api.d.ts +79 -20
- package/hues.d.ts +102 -19
- package/hues.js +80 -38
- package/package.json +10 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
- **Last updated**: 2025-07-
|
|
3
|
+
- **Last updated**: 2025-07-24T21:23:41Z
|
|
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,32 @@ 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.1](https://github.com/thi-ng/umbrella/tree/@thi.ng/pixel-analysis@1.0.1) (2025-07-24)
|
|
15
|
+
|
|
16
|
+
#### 🩹 Bug fixes
|
|
17
|
+
|
|
18
|
+
- bail out if insufficient samples ([41ba4c3](https://github.com/thi-ng/umbrella/commit/41ba4c3))
|
|
19
|
+
- update `temperature()` to return empty result if insufficient color samples
|
|
20
|
+
- update hue standard deviation ([9b0ea02](https://github.com/thi-ng/umbrella/commit/9b0ea02))
|
|
21
|
+
- return normalized hue SD (not in radians)
|
|
22
|
+
|
|
23
|
+
# [1.0.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/pixel-analysis@1.0.0) (2025-07-24)
|
|
24
|
+
|
|
25
|
+
#### 🛑 Breaking changes
|
|
26
|
+
|
|
27
|
+
- major update/rewrite hue range analysis fns ([5730fea](https://github.com/thi-ng/umbrella/commit/5730fea))
|
|
28
|
+
- BREAKING CHANGE: replace/update hue range analysis fns
|
|
29
|
+
- major update/rewrite hue range analysis fns ([9b12898](https://github.com/thi-ng/umbrella/commit/9b12898))
|
|
30
|
+
- BREAKING CHANGE: update AnalyzedImage result, update color analysis fns
|
|
31
|
+
- update structure & details of `AnalyzedImage` result type
|
|
32
|
+
- update `derivedColorsResults()`
|
|
33
|
+
- update/split `analyzeColors()`
|
|
34
|
+
- replace temperature calculation ([2a23cfa](https://github.com/thi-ng/umbrella/commit/2a23cfa))
|
|
35
|
+
- BREAKING CHANGE: replace temperature calculation with whole new approach
|
|
36
|
+
- replace existing `temperature()` with new method/approach
|
|
37
|
+
- add `TemperatureResult`
|
|
38
|
+
- update `analyzeColors()`, `derivedColorsResult()`
|
|
39
|
+
|
|
14
40
|
### [0.4.1](https://github.com/thi-ng/umbrella/tree/@thi.ng/pixel-analysis@0.4.1) (2025-07-15)
|
|
15
41
|
|
|
16
42
|
#### ♻️ 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
|
-
-
|
|
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.
|
|
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)
|
package/analyze-colors.d.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
|
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 =
|
|
37
|
+
const derived = derivedColorsResult(colors.map((x) => x.color));
|
|
45
38
|
const lumaRangeImg = reduce(minMax(), imgGray.data);
|
|
46
|
-
const weightedLuma = dot(derived.
|
|
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
|
-
|
|
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
|
|
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
|
|
77
|
-
const
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
79
|
+
mean: {
|
|
80
|
+
hue: meanHue,
|
|
81
|
+
sat: vmean(sats),
|
|
82
|
+
luma: vmean(dominantLuma)
|
|
83
|
+
},
|
|
84
|
+
sd: {
|
|
85
|
+
hue: circularSD(hues) / TAU,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
*
|
|
85
|
-
*
|
|
113
|
+
* Comprehensive {@link TemperatureResult} as produced by
|
|
114
|
+
* {@link temperature}. Also see {@link AnalysisOpts.minSat}.
|
|
86
115
|
*/
|
|
87
|
-
|
|
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 {
|
|
2
|
-
import type {
|
|
1
|
+
import type { ReadonlyVec } from "@thi.ng/vectors";
|
|
2
|
+
import type { TemperatureResult } from "./api.js";
|
|
3
3
|
/**
|
|
4
|
-
* Iterator
|
|
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
|
-
* @
|
|
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
|
|
32
|
+
export declare function selectHueRangeIDs(colors: Iterable<ReadonlyVec>, minHue: number, maxHue: number, minSat: number): Generator<number, void, unknown>;
|
|
13
33
|
/**
|
|
14
|
-
* Similar to {@link
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
* @
|
|
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
|
|
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
|
|
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
|
-
* @
|
|
32
|
-
*
|
|
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
|
|
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
|
-
*
|
|
38
|
-
*
|
|
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
|
-
* @
|
|
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
|
|
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,98 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
6
|
-
function*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
26
|
-
if (
|
|
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
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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((
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
if (!angles.length) {
|
|
81
|
+
return { hues, meanHue: 0, temp: 0, areaTemp: 0, area: 0 };
|
|
82
|
+
}
|
|
83
|
+
const meanHue = circularMean(angles) / TAU;
|
|
84
|
+
const temp = hueTemperature(meanHue);
|
|
85
|
+
const areaTemp = temp * area;
|
|
86
|
+
return { hues, meanHue, temp, areaTemp, area };
|
|
87
|
+
};
|
|
88
|
+
const hueTemperature = (hue) => 2 * (hue < 2 / 3 ? smoothStep(0.6, 0.1, hue) : smoothStep(0.72, 0.92, hue)) - 1;
|
|
51
89
|
export {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
90
|
+
countHueRange,
|
|
91
|
+
hueRangeArea,
|
|
92
|
+
hueRangeAreaIntensity,
|
|
93
|
+
hueTemperature,
|
|
94
|
+
meanIntensity,
|
|
95
|
+
selectHueRange,
|
|
96
|
+
selectHueRangeIDs,
|
|
97
|
+
temperature
|
|
56
98
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thi.ng/pixel-analysis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
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/
|
|
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.
|
|
49
|
+
"@thi.ng/pixel-dominant-colors": "^2.0.6",
|
|
49
50
|
"@thi.ng/transducers": "^9.6.3",
|
|
50
|
-
"@thi.ng/vectors": "^8.
|
|
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": "
|
|
115
|
+
"gitHead": "98bec7f990466d4771d8580362d4bdd58ffeea63\n"
|
|
111
116
|
}
|