FDNavigate back to the homepage

Spatial Clustering of Colors in Star Maps and Cell Images

Rick
August 24th, 2024 · 1 min read

So this is more like a show case rather than a indepth analysis. It was just something I found interesting and wantd to explore with JS canvas.

I created a node graph in blender which mimiced a star map, heat map or cell imaging result.

Here is the node graph.

Node Graph of cell like image from blender

It creates a nice image which resembles a star map or the result of a cell imaging processes.

Result of blender node graph

So the aim of the experiment was to cluster sections of an image based on a color and threshold color value.

What does this mean?

It means can we define an edge on a complex image filled with noisy data?

And the result is yes you can to a degree.

Below are the 3 images of the original map, the dark spots highlighted and then the light area highlighted.

Noisy image

Dark spots highlighted on image

Light spots highlighted on image

As you can see by manipulating the color and the concave modulator we can highlight different areas of a very noisy image.

The principle is we split the image into sections and then use a concave hull around specific coloured pixels to create a border and then fill this hull with a color via JS canvas methods.

Below is the example code:

1import { useEffect } from 'react';
2import hull from 'hull.js'
3
4function loadImageToCanvas(imagePath, canvasId) {
5 const canvas = document.getElementById(canvasId);
6 const ctx = canvas.getContext('2d');
7
8 const img = new Image();
9
10 img.src = imagePath;
11
12 img.onload = function() {
13 ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
14 splitImageIntoSquares(75, ctx, canvas);
15 };
16}
17
18function splitImageIntoSquares(size, ctx, canvas) {
19 const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
20 const { width, height } = imageData;
21 const squares = [];
22
23 for (let y = 0; y < height; y += size) {
24 for (let x = 0; x < width; x += size) {
25 const squareWidth = Math.min(size, width - x);
26 const squareHeight = Math.min(size, height - y);
27
28 const squareImageData = ctx.createImageData(squareWidth, squareHeight);
29
30 for (let row = 0; row < squareHeight; row++) {
31 for (let col = 0; col < squareWidth; col++) {
32 const srcIndex = ((y + row) * width + (x + col)) * 4;
33 const dstIndex = (row * squareWidth + col) * 4;
34
35 squareImageData.data[dstIndex] = imageData.data[srcIndex];
36 squareImageData.data[dstIndex + 1] = imageData.data[srcIndex + 1];
37 squareImageData.data[dstIndex + 2] = imageData.data[srcIndex + 2];
38 squareImageData.data[dstIndex + 3] = imageData.data[srcIndex + 3];
39 }
40 }
41
42 squares.push({
43 x,
44 y,
45 width: squareWidth,
46 height: squareHeight,
47 data: squareImageData
48 });
49 }
50 }
51
52 processSquares(squares, ctx)
53
54 return squares;
55}
56
57function processSquares(squares, ctx) {
58 const thresholdColor = { r: 49, g: 49, b: 49 };
59 // const thresholdColor = { r: 49, g: 49, b: 213 };
60 const threshold = 80;
61 squares.forEach(square => {
62 const { x, y, width, height, data } = square;
63 const points = [];
64
65 for (let row = 0; row < height; row++) {
66 for (let col = 0; col < width; col++) {
67 const index = (row * width + col) * 4;
68 const r = data.data[index];
69 const g = data.data[index + 1];
70 const b = data.data[index + 2];
71
72 if (isColorInThreshold(r, g, b, thresholdColor, threshold)) {
73 points.push({ x: x + col, y: y + row });
74 }
75 }
76 }
77
78 if (points.length > 0) {
79 // 4 for black
80 // 7 for blue
81 const h = hull(points.map((item) => ([item.x, item.y])), 4);
82
83 drawConcaveHull(h, ctx);
84 }
85 });
86}
87
88function isColorInThreshold(r, g, b, targetColor, threshold) {
89 return Math.abs(r - targetColor.r) < threshold &&
90 Math.abs(g - targetColor.g) < threshold &&
91 Math.abs(b - targetColor.b) < threshold;
92}
93
94function drawConcaveHull(hull, ctx) {
95 ctx.strokeStyle = 'red'; // Color of the hull border
96 ctx.lineWidth = 0;
97 ctx.beginPath();
98
99 hull.forEach((point, index) => {
100 if (index === 0) {
101 ctx.moveTo(point[0], point[1]);
102 } else {
103 ctx.lineTo(point[0], point[1]);
104 }
105 });
106
107 ctx.fillStyle = "green";
108 ctx.fill()
109 ctx.closePath();
110 // ctx.stroke();
111}
112
113
114const constructCanvas = () => {
115 loadImageToCanvas('/cells.png', 'canvas1');
116}
117
118const ClusteredConcave = () => {
119
120 useEffect(() => {
121 constructCanvas()
122 }, [])
123
124 return (
125 <canvas id="canvas1" width="500" height="300"/>
126 )
127}
128
129export default ClusteredConcave;

If you have any questions, reach out to me [email protected], Id be more than happy to discuss.

More articles from theFrontDev

Decal BuffergGeometry Merged with BufferGeometry (Part 1)

A simple and straight forward way to merge a decal Geometry with a buffer geometry and visualise the vertices.

July 26th, 2024 · 1 min read

Edge Detection and Concave Hulls

A novel approach for edge detection in Three. Using shapes, shape geometreis and concave hulls to define edges and then pass then to a shader material, where you can create a color gradient.

January 22nd, 2024 · 2 min read
© 2021–2024 theFrontDev
Link to $https://twitter.com/TheFrontDevLink to $https://github.com/Richard-ThompsonLink to $https://www.linkedin.com/in/richard-thompson-248ba3111/