Commit 89bfd83b authored by Matija Obreza's avatar Matija Obreza
Browse files

Detect and apply primary colors

parent fc569627
import * as React from 'react';
import { withStyles } from '@material-ui/core/styles';
import { count } from '@gringlobal/counter/counter';
import { logElapsed, filterSobel, filterBlur, filterSum, filterSharpenWithMask, filterOutline } from '@gringlobal/counter/filters';
import { logElapsed, colorMainsClassifier, filterBlur } from '@gringlobal/counter/filters';
const styles = {
canvases: {
......@@ -25,8 +25,9 @@ const styles = {
resultOverlay: {
width: '100%',
minHeight: '20px',
position: 'absolute' as 'absolute',
// position: 'absolute' as 'absolute',
border: 'solid 1px red',
backgroundColor: 'lime',
top: -1,
left: -1,
},
......@@ -40,9 +41,13 @@ const styles = {
},
};
const L = console.log;
class ProcessImage extends React.Component<any> {
// private MAX_PROCESSING_WIDTH: number = 300;
private logger: any;
private loggerDate: number = Date.now();
private original: any;
private originalVideo: any;
private resultOverlay: any;
......@@ -50,6 +55,7 @@ class ProcessImage extends React.Component<any> {
public constructor(props) {
super(props);
this.logger = React.createRef();
this.original = React.createRef();
this.originalVideo = React.createRef();
this.resultOverlay = React.createRef();
......@@ -61,6 +67,25 @@ class ProcessImage extends React.Component<any> {
video: false,
};
public componentDidMount() {
console.log('Component did mount', this.logger);
if (this.logger.current && this.logger.current.appendChild) {
console.log = (text, ...args) => {
const time = Date.now();
L(`+${time - this.loggerDate} ${text}`, ...args);
const msg = document.createElement('pre');
msg.innerHTML = `+${time - this.loggerDate} ${text}`; // \n${args.map((arg) => `${JSON.stringify(arg, null, 2).substring(0, 300)}\n\n`)}`;
this.logger.current.appendChild(msg);
this.loggerDate = time;
}
}
}
public componentWillUnmount() {
console.log = L;
L('Reset console.log');
}
private resizeCanvases = () => {
const source = this.state.video ? this.originalVideo.current : this.original.current;
console.log(`Source ${this.state.video ? 'Video' : 'Image'} size ${this.state.video ? source.videoWidth : source.width}x${this.state.video ? source.videoHeight : source.height}`);
......@@ -79,6 +104,9 @@ class ProcessImage extends React.Component<any> {
}
private captureImage = () => {
if (this.logger.current) {
this.logger.current.innerHTML = '';
}
const source = this.state.video ? this.originalVideo.current : this.original.current;
const contextSource = this.canvasSource.current.getContext('2d');
contextSource.imageSmoothingEnabled = false;
......@@ -91,18 +119,20 @@ class ProcessImage extends React.Component<any> {
// startTime = logElapsed(startTime, 'filterBw');
// let filtered = filterSharpenWithMask(image);
// startTime = logElapsed(startTime, 'filterSharpen');
// filtered = colorClassifier(filtered, 16);
// startTime = logElapsed(startTime, 'colorClassifier');
let filtered = filterBlur(image);
startTime = logElapsed(startTime, 'filterBlur');
filtered = filterSobel(filtered);
startTime = logElapsed(startTime, 'filterSobel');
filtered = filterSum(filtered);
startTime = logElapsed(startTime, 'filterSum');
filtered = filterSharpenWithMask(filtered, 2);
startTime = logElapsed(startTime, 'filterSharpenWithMask');
filtered = filterOutline(filtered);
startTime = logElapsed(startTime, 'filterOutline');
filtered = colorMainsClassifier(filtered, 2);
startTime = logElapsed(startTime, 'colorMainsClassifier');
// filtered = colorClassifier(filtered, 4);
// startTime = logElapsed(startTime, 'colorClassifier');
// filtered = filterSobel(filtered);
// startTime = logElapsed(startTime, 'filterSobel');
// filtered = filterTakeHighest(filtered);
// startTime = logElapsed(startTime, 'filterSum');
// filtered = filterSharpenWithMask(filtered, 2);
// startTime = logElapsed(startTime, 'filterSharpenWithMask');
// filtered = filterOutline(filtered);
// startTime = logElapsed(startTime, 'filterOutline');
// filtered = filterBW(filtered);
// startTime = logElapsed(startTime, 'filterBW');
// filtered = filterThreshold(filtered, 50);
......@@ -181,11 +211,13 @@ class ProcessImage extends React.Component<any> {
</div> }
<h1>Count: { this.state.count }</h1>
<div className={ classes.originalContainer }>
<img ref={ this.original } onLoad={ this.resizeCanvases } className={ classes.original } src="examples/G32 seed.jpg" style={ { display: this.state.video ? 'none' : '' } } />
<img ref={ this.original } onLoad={ this.resizeCanvases } className={ classes.original } src="examples/G25012 seed.jpg" style={ { display: this.state.video ? 'none' : '' } } />
<video ref={ this.originalVideo } className={ classes.original } onPlaying={ this.resizeCanvases } style={ { display: this.state.video ? '' : 'none' } } ></video>
<canvas ref={ this.resultOverlay } className={ classes.resultOverlay }></canvas>
</div>
<button onClick={ this.toggleCamera }>Camera</button>
<h1>Log</h1>
<div ref={ this.logger }></div>
<h1>Debugger</h1>
<div id="canvases" className={ classes.canvases } />
<canvas ref={ this.canvasSource } className={ classes.originalGraphics }></canvas>
......
......@@ -1000,6 +1000,282 @@ export function colorClassifier(image, buckets: number = 16): ImageData {
return new ImageData(dataR, w, h);
}
/**
* Find color clusters
*
* @param image Input RGBA image
*/
export function colorMainsClassifier(image, buckets: number = 1): ImageData {
const w = image.width;
const h = image.height;
console.log(`colorMainsClassifier ${w}x${h} buckets=${buckets}`);
const data = image.data;
const dataR = new Uint8ClampedArray(w * h * 4);
let primaries = new Array();
// for (let r = 0; r <= buckets; r++) {
// for (let g = 0; g <= buckets; g++) {
// for (let b = 0; b <= buckets; b++) {
// const v = new Vector(255 / buckets * r, 255 / buckets * g, 255 / buckets * b);
// primaries.push(v);
// }
// }
// }
primaries.push(new Vector(0, 0, 0));
primaries.push(new Vector(255, 255, 255));
primaries.push(new Vector(128, 128, 128));
// console.log(primaries);
const colors: Map<number, any> = new Map();
let startTime = Date.now();
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const p = (y * w + x) * 4;
const R = data[p + 0];
const G = data[p + 1];
const B = data[p + 2];
// const A = data[p + 3];
const color = R * 256 * 256 + G * 256 + B;
const colorCountAndIndex = colors.get(color);
if (colorCountAndIndex) {
colorCountAndIndex[0]++;
} else {
colors.set(color, [ 1, -1 ]);
}
// const distances: Array<number> = primaries.map((primary) => primary.distance(R, G, B));
// let minIdx = 0, minVal = distances[0];
// for (let i = 1; i < primaries.length; i++) {
// if (distances[i] < minVal) {
// minIdx = i;
// minVal = distances[i];
// }
// }
// dataR[p + 0] = minIdx;
// dataR[p + 3] = 255;
// if (R === 3 && G === 2) {
// const v = new Vector(R, G, B);
// console.log('Vector', v.toString());
// const v2 = new Vector(dataR[p + 0], dataR[p + 1], dataR[p + 2]);
// console.log('Vector', v2.toString());
// }
}
}
startTime = logElapsed(startTime, `Found ${colors.size} colors`);
// let colorList = 0;
for (const color of colors.keys()) {
const colorCountAndIndex = colors.get(color);
// const B = color % 256;
// const G = (color >> 8) % 256;
// const R = (color >> 16) % 256;
// if (colorList++ < 1000) {
// console.log(`${R},${G},${B}`, colorCountAndIndex);
// }
if (colorCountAndIndex[0] < 4) {
colors.delete(color);
}
}
logElapsed(startTime, `Keeping ${colors.size} colors`);
const findPrimary = (x, y, z): number => {
let minIdx = 0, minVal = primaries[0].distance(x, y, z);
for (let i = 1; i < primaries.length; i++) {
const d = primaries[i].distance(x, y, z);
if (d < minVal) {
minIdx = i;
minVal = d;
}
}
return minIdx;
}
const updateAverage = (existingAvg, newAvg) => {
const r = (existingAvg[0][0] * existingAvg[1] + newAvg[0][0] * newAvg[1]) / (existingAvg[1] + newAvg[1]);
const g = (existingAvg[0][1] * existingAvg[1] + newAvg[0][1] * newAvg[1]) / (existingAvg[1] + newAvg[1]);
const b = (existingAvg[0][2] * existingAvg[1] + newAvg[0][2] * newAvg[1]) / (existingAvg[1] + newAvg[1]);
return [[ r, g, b], existingAvg[1] + newAvg[1] ];
}
let safety = 0;
let clusterChanges;
let colorChanges;
const averagesByPrimary: Map<number, Array<any>> = new Map();;
do {
clusterChanges = 0;
colorChanges = 0;
averagesByPrimary.clear();
for (const color of colors.keys()) {
const colorCountAndIndex = colors.get(color);
const B = color % 256;
const G = (color >> 8) % 256;
const R = (color >> 16) % 256;
const primaryIndex = findPrimary(R, G, B);
if (colorCountAndIndex[1] !== primaryIndex) {
colorCountAndIndex[1] = primaryIndex;
// colors.set(color, [ colorCountAndIndex[0], primaryIndex ]);
colorChanges++;
// if (safety >= 40) {
// console.log(`Yaay R${safety} ${R},${G},${B} from ${colorCountAndIndex[1]} -> ${primaryIndex}`);
// }
}
const primaryAvg = averagesByPrimary.get(primaryIndex) || [ [ R, G, B ], 0 ];
// add to average
averagesByPrimary.set(primaryIndex, updateAverage(primaryAvg, [ [ R, G, B ], colorCountAndIndex[0] ]));
}
// update primaries
primaries.forEach((primary, idx) => {
const primaryAvg = averagesByPrimary.get(idx);
if (primaryAvg) {
if (primary.distance(primaryAvg[0][0], primaryAvg[0][1], primaryAvg[0][2]) > 1) {
clusterChanges++;
}
primary.v[0] = Math.round(primaryAvg[0][0]);
primary.v[1] = Math.round(primaryAvg[0][1]);
primary.v[2] = Math.round(primaryAvg[0][2]);
}
});
console.log(`${clusterChanges} cluster changes, ${colorChanges} colors changed cluster.`); // , averagesByPrimary);
// Remove tiny clusters
const clustersWithValues = [];
for (const averageByPrimary of averagesByPrimary.values()) {
if (averageByPrimary[1] > 0) {
clustersWithValues.push(averageByPrimary);
}
}
if (true || clustersWithValues.length < averagesByPrimary.size) {
const clusterPowers = clustersWithValues.reduce((sum, averageByPrimary) => sum + averageByPrimary[1], 0);
const clusterPowerAvg = clusterPowers / clustersWithValues.length;
console.log(`Cluster power avg=${clusterPowerAvg} clustersWithValues ${clustersWithValues.length}`);
const sortedClusters = clustersWithValues.sort((averageByPrimary1, averageByPrimary2) => Vector.fromArray(averageByPrimary1[0]).length() - Vector.fromArray(averageByPrimary2[0]).length());
// console.log(`Distance sorted ${sortedClusters.length} clusters`, sortedClusters);
let toRemove = [];
let prevAbP = sortedClusters[0];
let prevAbPv = Vector.fromArray(prevAbP[0]);
for (let i = 1; i < sortedClusters.length; i++) {
const AbP = sortedClusters[i];
// console.log('Comparing', prevAbP, AbP);
// Distance to prev
const distance = prevAbPv.distance(AbP[0][0], AbP[0][1], AbP[0][2]);
// console.log(`Distance ${prevAbPv.toString()}`, distance);
if (distance < 20) {
if (AbP[1] > clusterPowerAvg && prevAbP[1] > clusterPowerAvg) {
// keep both
} else if (AbP[1] < clusterPowerAvg) {
toRemove.push(i);
} else if (prevAbP[1] < clusterPowerAvg) {
toRemove.push(i - 1);
}
}
prevAbP = AbP;
prevAbPv = Vector.fromArray(AbP[0]);
}
// console.log('We should remove', toRemove);
toRemove = toRemove.filter((v, idx, arr) => arr.indexOf(v) === idx);
if (toRemove.length > 0) {
console.log('We will remove', toRemove);
const keeping = sortedClusters.filter((AbP, idx) => toRemove.indexOf(idx) < 0);
console.log(`Keeping ${keeping.length}`, keeping);
console.log(`averagesByPrimary1 ${averagesByPrimary.size}`); // , averagesByPrimary);
averagesByPrimary.forEach((AbP, key) => {
if (keeping.indexOf(AbP) < 0) {
averagesByPrimary.delete(key);
}
});
console.log(`averagesByPrimary2 ${averagesByPrimary.size}`); // , averagesByPrimary);
}
}
console.log(`Primaries1 ${primaries.length}`);
primaries = primaries.filter((primary, idx) => averagesByPrimary.has(idx));
console.log(`Primaries2 ${primaries.length}`);
// primaries = primaries.filter((primary, idx) => averagesByPrimary.has(idx) && averagesByPrimary.get(idx)[1] > clusterPowerAvg / 2);
} while (clusterChanges > 0 && safety++ < 10);
// primaries = primaries.filter((primary, idx) => averagesByPrimary.has(idx));
// primaries.sort(Vector.compareByCoordinates).forEach((primary, idx) => {
// console.log(`R${safety} P#${idx} = ${primary.v[0]}, ${primary.v[1]}, ${primary.v[2]}`)
// });
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const p = (y * w + x) * 4;
const R = data[p + 0];
const G = data[p + 1];
const B = data[p + 2];
// const A = data[p + 3];
const avgColor = primaries[findPrimary(R, G, B)];
dataR[p + 0] = avgColor.v[0];
dataR[p + 1] = avgColor.v[1];
dataR[p + 2] = avgColor.v[2];
dataR[p + 3] = 255;
}
}
return new ImageData(dataR, w, h);
// let topColors = new Array(buckets * buckets);
// let colorCount = 0;
// for (let r = 0; r < buckets; r++) {
// for (let g = 0; g < buckets; g++) {
// for (let b = 0; b < buckets; b++) {
// const pH = r * buckets * buckets + g * buckets + b;
// // console.log(`Color ${pH} f=${factor}`, r, g, b);
// if (histogram[pH] > 0) {
// const v = new Vector(Math.ceil(r * factor), Math.ceil(g * factor), Math.ceil(b * factor));
// topColors.push([v, histogram[pH]]);
// colorCount++;
// // console.log(`Color ${pH}`, v.toString(), histogram[pH]);
// }
// }
// }
// }
// topColors.sort((a, b) => b[1] - a[1]);
// topColors.slice(0, 50).forEach((a, idx) => {
// console.log(`#${idx + 1} ${a[0].toString()}`, a[1]);
// });
// topColors = topColors.map((a) => a[0]);
// const mainColors = topColors.splice(0, 1); // Math.ceil(colorCount * 0.01));
// console.log(`Starting with ${mainColors.length} main colors of ${topColors.length}`);
// topColors.forEach((color, idx) => {
// if (mainColors.length > 10000) {
// console.log('** BREAK **');
// return;
// }
// for (const color2 of mainColors) {
// if (Vector.distance(color, color2) <= 16) {
// // console.log(`Distance ${color.toString()}:${color2.toString()} is`, Vector.distance(color, color2));
// mainColors.push(color);
// topColors.splice(idx, 1);
// // console.log(`Removed ${removed.toString()}`);
// return;
// }
// }
// });
// console.log(`Clear Alpha for top ${mainColors.length} colors of ${colorCount}`);
// mainColors.forEach((color) => {
// let clearedPixels = 0;
// for (let y = 0; y < h; y++) {
// for (let x = 0; x < w; x++) {
// const p = (y * w + x) * 4;
// if (dataR[p + 0] === color.v[0] && dataR[p + 1] === color.v[1] && dataR[p + 2] === color.v[2]) {
// dataR[p + 3] = 0;
// clearedPixels++;
// }
// }
// }
// console.log(`Top color ${color.toString()} cleared ${clearedPixels} pixels.`);
// });
// return new ImageData(dataR, w, h);
}
/**
* Implementation of filtering by kernel
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment