Commit 686f9546 authored by Matija Obreza's avatar Matija Obreza
Browse files

Color classifier

parent ceb47f99
import * as React from 'react';
import { withStyles } from '@material-ui/core/styles';
import { count } from '@gringlobal/counter/counter';
import { filterThinEdges, filterSum, filterSobel, filterSmooth } from '@gringlobal/counter/filters';
import { logElapsed, filterBlur, filterBW, filterSobel, filterSum } from '@gringlobal/counter/filters';
const styles = {
thumbnail: {
width: '100px',
},
canvases: {
'flex-flow': 'row wrap',
'display': 'flex',
......@@ -23,14 +20,15 @@ const styles = {
original: {
width: '100%',
position: 'relative' as 'relative',
visibility: 'hidden' as 'hidden',
},
resultOverlay: {
width: '100%',
minHeight: '20px',
position: 'absolute' as 'absolute',
border: 'solid 1px red',
top: -1,
left: -1,
top: 20, // -1,
left: 20, // -1,
},
canvas: {
maxHeight: '200px',
......@@ -38,6 +36,7 @@ const styles = {
minHeight: '100px',
},
originalGraphics: {
// width: '100%',
},
};
......@@ -64,13 +63,13 @@ class ProcessImage extends React.Component<any> {
private resizeCanvases = () => {
const source = this.state.video ? this.originalVideo.current : this.original.current;
console.log(`${this.state.video ? 'Video' : 'Image'} size ${this.state.video ? source.videoWidth : source.width}x${this.state.video ? source.videoHeight : source.height}`);
console.log(`Source ${this.state.video ? 'Video' : 'Image'} size ${this.state.video ? source.videoWidth : source.width}x${this.state.video ? source.videoHeight : source.height}`);
// canvas.width = video.videoWidth;
const ratio = this.state.video ? source.videoWidth / source.videoHeight : source.width / source.height;
// original.height = original.width / ratio;
// this.resultOverlay.current.style.width = this.state.video ? source.videoWidth : source.width;
// this.resultOverlay.current.style.height = this.state.video ? source.videoHeight : source.height;
this.canvasSource.current.width = this.resultOverlay.current.width = 640;
this.canvasSource.current.width = this.resultOverlay.current.width = Math.min(640, this.state.video ? source.videoWidth : source.width);
this.canvasSource.current.height = this.resultOverlay.current.height = this.resultOverlay.current.width / ratio;
// for (const canvas2 of canvases) {
// canvas2.width = canvas.width;
......@@ -82,11 +81,31 @@ class ProcessImage extends React.Component<any> {
private captureImage = () => {
const source = this.state.video ? this.originalVideo.current : this.original.current;
const contextSource = this.canvasSource.current.getContext('2d');
contextSource.imageSmoothingEnabled = false;
contextSource.drawImage(source, 0, 0, this.canvasSource.current.width, this.canvasSource.current.height);
const image = contextSource.getImageData(0, 0, this.canvasSource.current.width, this.canvasSource.current.height);
console.log(`Image data ${image.width}x${image.height}`);
const filtered = filterThinEdges(filterSum(filterSobel(filterSmooth(image))));
this.resultOverlay.current.getContext('2d').putImageData(filtered, 0, 0);
let startTime = Date.now();
// const filtered = filterThinEdges(filterSum(filterSobel(filterSmooth(image))));
// let filtered = filterBW(image);
// startTime = logElapsed(startTime, 'filterBw');
let filtered = filterBlur(image);
startTime = logElapsed(startTime, 'filterBlur');
// filtered = colorClassifier(filtered, 16);
// startTime = logElapsed(startTime, 'colorClassifier');
filtered = filterSobel(filtered);
startTime = logElapsed(startTime, 'filterSobel');
filtered = filterSum(filtered);
startTime = logElapsed(startTime, 'filterSum');
filtered = filterBW(filtered);
startTime = logElapsed(startTime, 'filterBW');
// filtered = filterThreshold(filtered, 1);
// startTime = logElapsed(startTime, 'filterThreshold');
// filtered = filterThinEdges(filtered);
// startTime = logElapsed(startTime, 'filterThinEdges');
const contextResult = this.resultOverlay.current.getContext('2d');
contextResult.putImageData(filtered, 0, 0);
contextResult.imageSmoothingEnabled = false;
if (this.state.video) {
setTimeout(() => { this.captureImage(); }, 500);
......
......@@ -6,6 +6,14 @@
* All rights reserved.
*/
import Vector from './vector';
export const logElapsed = (prevTime, message?: string): number => {
const now = Date.now();
console.log(`${message || ''} took ${now - prevTime}ms`);
return now;
};
const elapsed = (prevTime) => (Date.now() - prevTime) + 'ms';
export function repeat(count, f, image, ...args) {
......@@ -61,14 +69,15 @@ export function filterSum(image) {
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] / 255; // R
const G = data[p + 1] / 255; // G
const B = data[p + 2] / 255; // B
const R = data[p + 0];
const G = data[p + 1];
const B = data[p + 2];
const A = data[p + 3];
dataR[p + 0] = 255 * (R + G + B);
dataR[p + 1] = 255 * (R + G + B);
dataR[p + 2] = 255 * (R + G + B);
dataR[p + 3] = 255;
dataR[p + 0] = (R + G + B);
dataR[p + 1] = (R + G + B);
dataR[p + 2] = (R + G + B);
dataR[p + 3] = A;
}
}
return result;
......@@ -331,7 +340,7 @@ export function filterDiffB(image) {
const sum = 2 * Math.abs(RR[0] - RR[4]) + 2 * Math.abs(RR[0] - RR[2]) + Math.abs(RR[1] - RR[2]) + Math.abs(RR[3] - RR[4]) +
2 * Math.abs(GG[0] - GG[4]) + 2 * Math.abs(GG[0] - GG[2]) + Math.abs(GG[1] - GG[2]) + Math.abs(GG[3] - GG[4]) +
2 * Math.abs(BB[0] - BB[4]) + 2 * Math.abs(BB[0] - BB[2]) + Math.abs(BB[1] - BB[2]) + Math.abs(BB[3] - BB[4]);
const total = sum > 200 ? 255 : 0;
const total = sum > 100 ? 255 : 0;
dataR[p + 0] = dataR[p + 1] = dataR[p + 2] = total;
dataR[p + 3] = 255;
}
......@@ -426,7 +435,7 @@ export function filterSobel(image) {
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const p = (y * w + x) * 4;
dataR[p + 3] = 255;
dataR[p + 3] = ((dataR[p + 0] + dataR[p + 1] + dataR[p + 2]) > 0) ? 255 : 200;
}
}
return result;
......@@ -475,7 +484,7 @@ export function fillChannel(image, color, value) {
* @param {Uint8ClampedArray} data the data
* @param {*} factor multiplication factor
*/
export function arrMul(data, factor) {
export function arrMul(data: Uint8ClampedArray, factor) {
for (let i = data.length - 1; i >= 0; i--) {
data[i] = data[i] * factor;
}
......@@ -488,7 +497,7 @@ export function arrMul(data, factor) {
* @param {function} op Optional function to execute on the difference
*/
export function arrDiff(a, b, target, op) {
if (! op) {
if (!op) {
// noop
op = (x) => x;
}
......@@ -546,11 +555,11 @@ export function filterThinEdges(image) {
so = se;
se = dataR[P(x + 1, y + 1)]; // &(pBelow[x+1]);
const A = (no === 0 && ne === 1 ? 1 : 0) + (ne === 0 && ea === 1 ? 1 : 0) +
(ea === 0 && se === 1 ? 1 : 0) + (se === 0 && so === 1 ? 1 : 0) +
(so === 0 && sw === 1 ? 1 : 0) + (sw === 0 && we === 1 ? 1 : 0) +
(we === 0 && nw === 1 ? 1 : 0) + (nw === 0 && no === 1 ? 1 : 0);
const B = no + ne + ea + se + so + sw + we + nw;
const A = (no === 0 && ne === 1 ? 1 : 0) + (ne === 0 && ea === 1 ? 1 : 0) +
(ea === 0 && se === 1 ? 1 : 0) + (se === 0 && so === 1 ? 1 : 0) +
(so === 0 && sw === 1 ? 1 : 0) + (sw === 0 && we === 1 ? 1 : 0) +
(we === 0 && nw === 1 ? 1 : 0) + (nw === 0 && no === 1 ? 1 : 0);
const B = no + ne + ea + se + so + sw + we + nw;
const m1 = iter === 0 ? (no * ea * so) : (no * ea * we);
const m2 = iter === 0 ? (ea * so * we) : (no * so * we);
......@@ -662,14 +671,14 @@ export function filterOutline(image) {
// directions in clockwise order
const DIRECTIONS = [
[ 1, 0 ], // ea
[ 1, 1 ], // se
[ 0, 1 ], // so
[ -1, 1 ], // sw
[ -1, 0 ], // we
[ -1, -1 ], // nw
[ 0, -1 ], // no
[ 1, -1 ], // ne
[1, 0], // ea
[1, 1], // se
[0, 1], // so
[-1, 1], // sw
[-1, 0], // we
[-1, -1], // nw
[0, -1], // no
[1, -1], // ne
];
const findNext = (startX, startY, color) => {
......@@ -724,7 +733,7 @@ export function filterOutline(image) {
}
}
// no next pixel found. End of trail. Go back
if (! go) {
if (!go) {
// console.log('Changing direction', x, y);
dataR[P(x, y, color)] = 0; // unmark
dataR[P(x, y, 3)] = 0; // no alpha
......@@ -814,7 +823,7 @@ export function floodFill(data, w, h, startX, startY, color) {
let point = [];
for (let x = startX; x < w; x++) {
if (data[P(x, startY + 1, color)] === 0) {
point = [ x, startY + 1];
point = [x, startY + 1];
break;
}
}
......@@ -838,7 +847,7 @@ export function floodFill(data, w, h, startX, startY, color) {
while ((point = stack.pop())) {
console.log('Next start at', point);
const x = point[0], y= point[1];
const x = point[0], y = point[1];
if (data[P(x, y, color)] !== 0) {
continue;
}
......@@ -856,5 +865,123 @@ export function floodFill(data, w, h, startX, startY, color) {
}
data[P(x, y, color)] = 255;
}
}
/**
* Find color clusters
*
* @param image Input RGBA image
*/
export function colorClassifier(image, buckets: number = 16): ImageData {
const w = image.width;
const h = image.height;
const data = image.data;
const dataR = new Uint8ClampedArray(data);
// const histogram = new Array(buckets);
// histogram.forEach((val, idx, arr) => {
// console.log(`Making ${idx}`, arr);
// arr[idx] = new Array(buckets);
// arr[idx].forEach((val2, idx2, arr2) => {
// console.log(`Making ${idx},${idx2}`, arr2);
// arr2[idx2] = new Array(buckets);
// });
// });
const histogram = new Array(buckets * buckets * buckets);
histogram.fill(0);
// console.log('Starting histogram', histogram);
const factor = 255.0 / buckets;
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const p = (y * w + x) * 4;
const R = Math.floor(data[p + 0] / factor);
const G = Math.floor(data[p + 1] / factor);
const B = Math.floor(data[p + 2] / factor);
// const A = data[p + 3];
const pH = R * buckets * buckets + G * buckets + B;
histogram[pH]++;
dataR[p + 0] = Math.ceil(R * factor);
dataR[p + 1] = Math.ceil(G * factor);
dataR[p + 2] = Math.ceil(B * factor);
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());
// }
}
}
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.`);
})
// const v1 = new Vector(32, 128, 0);
// const v2 = new Vector(0, 96, 32);
// const v3 = new Vector(30, 128, 192);
// const d = Vector.distance(v1, v2);
// console.log('v1', v1.toString(), v1.length(), v1);
// console.log('v2', v2.toString(), v2.length(), v2);
// console.log('Distance', d);
// console.log('v1-v2', v1.diff(v2).toString(), v1.diff(v2), v1.diff(v2).length());
// console.log('v2-v1', v2.diff(v1).toString(), v2.diff(v1), v2.diff(v1).length());
// console.log('v3-v1', v3.diff(v1).toString(), v3.diff(v1), v3.diff(v1).length());
return new ImageData(dataR, w, h);
}
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