Commit db599467 authored by Matija Obreza's avatar Matija Obreza
Browse files

Connected components

parent 89bfd83b
import * as React from 'react';
import { withStyles } from '@material-ui/core/styles';
import { count } from '@gringlobal/counter/counter';
import { logElapsed, colorMainsClassifier, filterBlur } from '@gringlobal/counter/filters';
import { logElapsed, colorMainsClassifier, filterBlur, connectedComponentLabels } from '@gringlobal/counter/filters';
const styles = {
canvases: {
......@@ -94,7 +94,7 @@ class ProcessImage extends React.Component<any> {
// 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 = Math.min(640, this.state.video ? source.videoWidth : source.width);
this.canvasSource.current.width = this.resultOverlay.current.width = Math.min(320, 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;
......@@ -109,7 +109,7 @@ class ProcessImage extends React.Component<any> {
}
const source = this.state.video ? this.originalVideo.current : this.original.current;
const contextSource = this.canvasSource.current.getContext('2d');
contextSource.imageSmoothingEnabled = false;
contextSource.imageSmoothingEnabled = true;
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}`);
......@@ -121,8 +121,12 @@ class ProcessImage extends React.Component<any> {
// startTime = logElapsed(startTime, 'filterSharpen');
let filtered = filterBlur(image);
startTime = logElapsed(startTime, 'filterBlur');
filtered = colorMainsClassifier(filtered, 2);
const primaryColors = [];
filtered = colorMainsClassifier(filtered, 3, primaryColors);
startTime = logElapsed(startTime, 'colorMainsClassifier');
console.log(`Primary colors ${primaryColors.length}`, primaryColors);
filtered = connectedComponentLabels(filtered, primaryColors);
startTime = logElapsed(startTime, 'connectedComponentLabels');
// filtered = colorClassifier(filtered, 4);
// startTime = logElapsed(startTime, 'colorClassifier');
// filtered = filterSobel(filtered);
......
......@@ -1005,7 +1005,7 @@ export function colorClassifier(image, buckets: number = 16): ImageData {
*
* @param image Input RGBA image
*/
export function colorMainsClassifier(image, buckets: number = 1): ImageData {
export function colorMainsClassifier(image, buckets: number = 1, primaryColorHolder?: Array<any>): ImageData {
const w = image.width;
const h = image.height;
console.log(`colorMainsClassifier ${w}x${h} buckets=${buckets}`);
......@@ -1013,17 +1013,17 @@ export function colorMainsClassifier(image, buckets: number = 1): ImageData {
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));
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();
......@@ -1191,6 +1191,11 @@ export function colorMainsClassifier(image, buckets: number = 1): ImageData {
}
console.log(`Primaries1 ${primaries.length}`);
primaries.forEach((primary, idx) => {
if (averagesByPrimary.has(idx)) {
primary.count = averagesByPrimary.get(idx)[1];
}
});
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);
......@@ -1201,6 +1206,10 @@ export function colorMainsClassifier(image, buckets: number = 1): ImageData {
// console.log(`R${safety} P#${idx} = ${primary.v[0]}, ${primary.v[1]}, ${primary.v[2]}`)
// });
if (primaryColorHolder) {
primaryColorHolder.push(...primaries);
}
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const p = (y * w + x) * 4;
......@@ -1370,3 +1379,167 @@ export function filterSharpenWithMask(image: ImageData, sharpenForce: number = 0
}
return new ImageData(dataR, w, h);
}
/**
* Label connected components.
* https://en.wikipedia.org/wiki/Connected-component_labeling
*
* @param image The source image
* @param primaries The list of primary colors
* @returns labeled components
*/
export function connectedComponentLabels(image: ImageData, primaries: Array<Vector>): ImageData {
console.log('connectedComponentLabels');
const w = image.width;
const h = image.height;
const data = image.data;
const dataI = new Uint8ClampedArray(w * h);
// const P = (x, y) => (y * w + x) * 4;
const P1 = (x, y) => x < 0 || y < 0 || x >= w || y >= h ? -1 : (y * w + x); // * 4;
const findPrimary = (x, y, z): number => {
for (let i = 0; i < primaries.length; i++) {
if (primaries[i].distance(x, y, z) === 0) {
return i;
}
}
return -1;
}
// Reverse primaries to their indexes
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 primaryIndex = findPrimary(R, G, B);
dataI[P1(x, y)] = primaryIndex;
}
}
console.log('Reverted to indexed colors');
// labels
const dataL = new Uint32Array(w * h);
// First pass
const UF = {
parent: [],
rank: [],
put: (val) => {
UF.parent[val] = val;
UF.rank[val] = 0;
},
size: () => {
return UF.parent.filter((val, idx) => val === idx).length;
},
roots: () => {
return UF.parent.filter((val, idx) => val === idx);
},
find: (k: number): number => {
while (UF.parent[k] !== k ) {
if (UF.parent[k] === undefined) {
throw new Error(`Value ${k} is not a member`);
}
k = UF.parent[k];
}
return k;
},
unionRoot: (x: number, y: number): number => {
if (x === y) {
return -1;
}
if (UF.rank[x] > UF.rank[y]) {
UF.parent[y] = x;
return x;
} else {
UF.parent[x] = y;
if (UF.rank[x] === UF.rank[y]) {
UF.rank[y]++;
}
return y;
}
},
};
const findBackgroundColorIndex = (colors: Array<any>) => {
let idx = 0;
let count = 0;
for (let i = 0; i < colors.length; i++) {
if (count < colors[i].count) {
idx = i;
count = colors[i].count;
}
}
return idx;
}
const backgrounds = new Set<number>();
const background = findBackgroundColorIndex(primaries);
console.log(`Background color idx=${background}`);
UF.put(0); // for background
primaries.forEach((color, idx) => {
if (idx === background) {
backgrounds.add(idx);
return;
}
if (Vector.distance(color, primaries[background]) < 100) {
console.log(`Extra background ${color.toString()} idx=${idx}`);
backgrounds.add(idx);
} else {
console.log(`Keeping ${color.toString()} idx=${idx}`);
}
});
console.log(`${backgrounds.size} background colors`, backgrounds);
let labelCounter = 1;
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const me = dataI[P1(x, y)];
if (backgrounds.has(me)) {
dataL[P1(x, y)] = 0;
continue;
}
const no = dataI[P1(x, y - 1)];
const nol = dataL[P1(x, y - 1)];
const we = dataI[P1(x - 1, y)];
const wel = dataL[P1(x - 1, y)];
if (no !== undefined && ! backgrounds.has(no) && we !== undefined && ! backgrounds.has(we)) { // if (me === no && me === we) {
dataL[P1(x, y)] = Math.min(nol, wel);
if (nol !== wel) {
const noRoot = UF.find(nol);
const weRoot = UF.find(wel);
UF.unionRoot(noRoot, weRoot);
}
} else if (we !== undefined && ! backgrounds.has(we)) { // if (me === no) {
dataL[P1(x, y)] = wel;
} else if (no !== undefined && ! backgrounds.has(no)) { // if (me === we) {
dataL[P1(x, y)] = nol;
} else {
dataL[P1(x, y)] = ++labelCounter;
UF.put(labelCounter);
}
}
}
console.log(`UnionFind ${UF.roots().length}`, UF.roots());
const ufRoots = UF.roots();
const ufColors = [];
for (let i = 0; i<ufRoots.length; i++) {
ufColors[ufRoots[i]] = i * 256 / ufRoots.length;
}
const dataR = new Uint8ClampedArray(w * h * 4);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const p = (y * w + x) * 4;
// dataR[p + 0] = dataR[p + 1] = dataR[p + 2] = dataI[y * w + x] * 256 / primaries.length;
const ufRoot = UF.find(dataL[P1(x, y)]);
dataR[p + 0] = dataR[p + 1] = dataR[p + 2] = ufColors[ufRoot];
dataR[p + 3] = ufRoot === 0 ? 0 : 255;
}
}
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