在图像中查找类似颜色的区域

一段时间以来,我一直在研究这个问题,结果很有希望。 我正在尝试将图像分成类似颜色的连接区域。 (基本上将所有像素的列表分成多个组(每组包含属于它的像素的坐标并共享相似的颜色)。

例如: http : //unsplash.com/photos/SoC1ex6sI4w/

在这个图像中,顶部的乌云可能会分成一组。 一些在另一个山上的灰色岩石,另一个在一些橙色的草地上。 雪将是另一个 – 背包的红色 – 等等

我试图devise一个既准确又高效的algorithm(它需要在中等规模的笔记本电脑级硬件上以ms为单位运行)


以下是我所尝试过的:

使用基于连接组件的algorithm,从左上方扫描每个像素,从左到右扫描每一行像素(并将当前像素与顶部像素和左侧像素进行比较)。 使用CIEDE2000色差公式,如果顶部或左侧的像素在一定的范围内,那么它将被认为是“相似的”并且是组的一部分。

这种工作 – 但问题是它依赖于具有锐利边缘的颜色区域 – 如果任何颜色组通过软梯度连接,则它将沿着该梯度行进并继续“连接”像素,因为各个像素之间的差异是比较小到可以被认为是“相似”的。

为了解决这个问题,我select将每个访问像素的颜色设置为最“相似”相邻像素的颜色(顶部或左侧)。 如果没有相似的像素,则保留其原始颜色。 这在一定程度上解决了更模糊的边界或软边缘的问题,因为随着algorithm的进展,新组的第一个颜色将被“携带”,并且最终该颜色和当前比较的颜色之间的差异将超过“相似性”不再是该组的一部分。

希望这是有道理的。 问题是这些选项都没有真正的工作。 在上面的图片返回的不是干净的组,但嘈杂的碎片组是不是我所期待的。

我并没有专门寻找代码 – 但是更多的想法是如何构build一个algorithm来成功地解决这个问题。 有没有人有这个想法?

谢谢!

您可以将RGB转换为HSL ,以便更轻松地计算颜色之间的距离。 我正在设置色差公差在行中:

 if (color_distance(original_pixels[i], group_headers[j]) < 0.3) {...} 

如果你改变0.3 ,你可以得到不同的结果。

看到它工作 。

请让我知道是否有帮助。

 function hsl_to_rgb(h, s, l) { // from http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion var r, g, b; if (s == 0) { r = g = b = l; // achromatic } else { var hue2rgb = function hue2rgb(p, q, t) { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; } var q = l < 0.5 ? l * (1 + s) : l + s - l * s; var p = 2 * l - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3); } return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; } function rgb_to_hsl(r, g, b) { // from http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion r /= 255, g /= 255, b /= 255; var max = Math.max(r, g, b), min = Math.min(r, g, b); var h, s, l = (max + min) / 2; if (max == min) { h = s = 0; // achromatic } else { var d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return [h, s, l]; } function color_distance(v1, v2) { // from http://stackoverflow.com/a/13587077/1204332 var i, d = 0; for (i = 0; i < v1.length; i++) { d += (v1[i] - v2[i]) * (v1[i] - v2[i]); } return Math.sqrt(d); }; function round_to_groups(group_nr, x) { var divisor = 255 / group_nr; return Math.ceil(x / divisor) * divisor; }; function pixel_data_to_key(pixel_data) { return pixel_data[0].toString() + '-' + pixel_data[1].toString() + '-' + pixel_data[2].toString(); } function posterize(context, image_data, palette) { for (var i = 0; i < image_data.data.length; i += 4) { rgb = image_data.data.slice(i, i + 3); hsl = rgb_to_hsl(rgb[0], rgb[1], rgb[2]); key = pixel_data_to_key(hsl); if (key in palette) { new_hsl = palette[key]; new_rgb = hsl_to_rgb(new_hsl[0], new_hsl[1], new_hsl[2]); rgb = hsl_to_rgb(hsl); image_data.data[i] = new_rgb[0]; image_data.data[i + 1] = new_rgb[1]; image_data.data[i + 2] = new_rgb[2]; } } context.putImageData(image_data, 0, 0); } function draw(img) { var canvas = document.getElementById('canvas'); var context = canvas.getContext('2d'); context.drawImage(img, 0, 0, canvas.width, canvas.height); img.style.display = 'none'; var image_data = context.getImageData(0, 0, canvas.width, canvas.height); var data = image_data.data; context.drawImage(target_image, 0, 0, canvas.width, canvas.height); data = context.getImageData(0, 0, canvas.width, canvas.height).data; original_pixels = []; for (i = 0; i < data.length; i += 4) { rgb = data.slice(i, i + 3); hsl = rgb_to_hsl(rgb[0], rgb[1], rgb[2]); original_pixels.push(hsl); } group_headers = []; groups = {}; for (i = 0; i < original_pixels.length; i += 1) { if (group_headers.length == 0) { group_headers.push(original_pixels[i]); } group_found = false; for (j = 0; j < group_headers.length; j += 1) { // if a similar color was already observed if (color_distance(original_pixels[i], group_headers[j]) < 0.3) { group_found = true; if (!(pixel_data_to_key(original_pixels[i]) in groups)) { groups[pixel_data_to_key(original_pixels[i])] = group_headers[j]; } } if (group_found) { break; } } if (!group_found) { if (group_headers.indexOf(original_pixels[i]) == -1) { group_headers.push(original_pixels[i]); } if (!(pixel_data_to_key(original_pixels[i]) in groups)) { groups[pixel_data_to_key(original_pixels[i])] = original_pixels[i]; } } } posterize(context, image_data, groups) } var target_image = new Image(); target_image.crossOrigin = ""; target_image.onload = function() { draw(target_image) }; target_image.src = "http://i.imgur.com/zRzdADA.jpg"; 
 canvas { width: 300px; height: 200px; } 
 <canvas id="canvas"></canvas> 

你可以使用“Mean Shift Filtering”algorithm来做同样的事情。

这是一个例子。 在这里输入图像描述

您将不得不启发式地确定函数参数。

这里是在node.js中的相同的包装

npm用于meanshiftalgorithm的Wrapper

希望这可以帮助!

您正在尝试完成的过程被称为图像分割 ,它是计算机视觉领域的一个深入 研究的 领域 ,有数百种不同的algorithm和实现。

你提到的algorithm应该适用于简单的图像,但是对于真实世界的图像,如链接到的图像,您可能需要更复杂的algorithm,甚至可能需要一个特定于域的图像(是否所有图像都包含一个视图? 。

我在Node.js中没有什么经验,但是从Google上search了一下,我发现了GraphicsMagic库,它作为可能完成这个工作(未validation)的段函数。

在任何情况下,我都会尝试寻找“图像分割”库,如果可能的话,不要仅限于Node.js实现,因为这种语言不是编写视觉应用程序的常用实践,与C ++ / Java / Python 。

我会尝试一个不同的方法。 查看洪水填充algorithm如何工作的描述:

  • 创build一个数组来保存已经着色的坐标信息。
  • 创build一个工作列表数组来保存必须查看的坐标。 把起始位置放在里面。
  • 当工作清单为空时,我们就完成了。
  • 从工作清单中删除一对坐标。
  • 如果这些坐标已经在我们的彩色像素数组中,请返回到步骤3。
  • 在当前坐标处着色像素,并将该坐标添加到彩色像素数组中。
  • 将与颜色与起始像素的原始颜色相同的每个相邻像素的坐标添加到工作列表中。
  • 返回到步骤3。

“search的方法”是优越的,因为它不仅从左到右,而且在全方位search。