mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-24 01:31:44 +00:00
99 lines
3.3 KiB
TypeScript
99 lines
3.3 KiB
TypeScript
|
|
export type HSV = [number, number, number]
|
||
|
|
export type RGBA = [number, number, number, number] | Uint8ClampedArray
|
||
|
|
|
||
|
|
export class ColorPicker {
|
||
|
|
public hsvData: HSV;
|
||
|
|
|
||
|
|
constructor(pixelData: RGBA) {
|
||
|
|
this.hsvData = this.RGBtoHSV(pixelData);
|
||
|
|
}
|
||
|
|
|
||
|
|
public selectedColorRange() {
|
||
|
|
return this.widenRange([[...this.hsvData], [...this.hsvData]]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public expandColorRange(currentRange: [HSV, HSV]) {
|
||
|
|
const widenedHSV = this.widenRange([[...this.hsvData], [...this.hsvData]]);
|
||
|
|
return this.createRange(currentRange.concat(widenedHSV));
|
||
|
|
}
|
||
|
|
|
||
|
|
public shrinkColorRange(currentRange: [HSV, HSV]) {
|
||
|
|
const widenedHSV = this.widenRange([[...this.hsvData], [...this.hsvData]]);
|
||
|
|
|
||
|
|
//Tries to shrink the lower part of to widened HSV
|
||
|
|
if(!this.shrinkRange(currentRange, widenedHSV[0])) {
|
||
|
|
//If the prev attempt failed, try to shrink the higher part of to widened HSV
|
||
|
|
this.shrinkRange(currentRange, widenedHSV[1]);
|
||
|
|
}
|
||
|
|
|
||
|
|
return currentRange;
|
||
|
|
}
|
||
|
|
|
||
|
|
private createRange(range: HSV[]): [HSV, HSV] {
|
||
|
|
const newRange: [HSV, HSV] = [[0, 0, 0], [0, 0, 0]];
|
||
|
|
for (let i = 0; i < 3; i++) {
|
||
|
|
newRange[0][i] = range[0][i];
|
||
|
|
newRange[1][i] = range[0][i];
|
||
|
|
for (let j = range.length - 1; j >= 0; j--) {
|
||
|
|
newRange[0][i] = Math.min(range[j][i], newRange[0][i]);
|
||
|
|
newRange[1][i] = Math.max(range[j][i], newRange[1][i]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return newRange;
|
||
|
|
}
|
||
|
|
|
||
|
|
private widenRange(range: [HSV, HSV]): [HSV, HSV] {
|
||
|
|
const expanded: [HSV, HSV] = [[0, 0, 0], [0, 0, 0]];
|
||
|
|
for (let i = 0; i < 3; i++) {
|
||
|
|
//Expanding the range by 10
|
||
|
|
expanded[0][i] = Math.max(0, range[0][i] - 10);
|
||
|
|
expanded[1][i] = Math.min(255, range[1][i] + 10);
|
||
|
|
}
|
||
|
|
expanded[1][0] = Math.min(180, expanded[1][0]); //h is up to 180
|
||
|
|
return expanded;
|
||
|
|
}
|
||
|
|
|
||
|
|
private shrinkRange(range: [HSV, HSV], color: HSV): boolean {
|
||
|
|
for (let i = 0; i < color.length; i++) {
|
||
|
|
if (!(range[0][i] <= color[i] && color[i] <= range[1][i])) return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
for (let i = 0; i < color.length; i++) {
|
||
|
|
if (color[i] - range[0][i] < range[1][i] - color[i]) {
|
||
|
|
//shrink from min side
|
||
|
|
range[0][i] = Math.min(range[0][i] + 10, range[1][i]);
|
||
|
|
} else {
|
||
|
|
//shrink from max side
|
||
|
|
range[1][i] = Math.max(range[1][i] - 10, range[0][i]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
private RGBtoHSV(rgba: RGBA ): HSV {
|
||
|
|
// Normalize RGB ranges
|
||
|
|
let r = rgba[0],
|
||
|
|
g = rgba[1],
|
||
|
|
b = rgba[2];
|
||
|
|
r = r / 255;
|
||
|
|
g = g / 255;
|
||
|
|
b = b / 255;
|
||
|
|
|
||
|
|
const minRGB = Math.min(r, Math.min(g, b));
|
||
|
|
const maxRGB = Math.max(r, Math.max(g, b));
|
||
|
|
const d = (r === minRGB) ? g - b : ((b === minRGB) ? r - g : b - r);
|
||
|
|
const h = (r === minRGB) ? 3 : ((b === minRGB) ? 1 : 5);
|
||
|
|
let H = 30 * (h - d / (maxRGB - minRGB));
|
||
|
|
let S = 255 * (maxRGB - minRGB) / maxRGB;
|
||
|
|
let V = 255 * maxRGB;
|
||
|
|
if (isNaN(H))
|
||
|
|
H = 0;
|
||
|
|
if (isNaN(S))
|
||
|
|
S = 0;
|
||
|
|
if (isNaN(V))
|
||
|
|
V = 0;
|
||
|
|
return [Math.round(H), Math.round(S), Math.round(V)];
|
||
|
|
}
|
||
|
|
}
|