Finding the most common colors in an image (fast, minimum dependencies)

from collections import defaultdict from PIL import Image most_common = 12 im = Image.open('nature_small.jpg') width, height = im.size impixels = im.load() dd = defaultdict(list) for i in range(width): for j in range(height): dd[impixels[i,j]].append(j * width + i) pixels = sorted(dd, key=lambda k: len(dd[k]), reverse=True)[:most_common] for pixel in pixels: print(pixel) """ (247, 247, 249) (246, 246, 248) (234, 239, 245) (198, 205, 224) (197, 204, 223) (231, 235, 244) (219, 227, 240) (226, 233, 243) (202, 215, 232) (196, 203, 222) (230, 234, 243) (199, 206, 225) """

If we paint each pixel having these RGB values in a separate color, we will notice that the combined regions remain relatively small. This shows that most pixels in homogenous regions tend to have slightly varying RGB values. What we could do then is find the pixels with small relative distances from the current one and paint them in the same color, gradually expanding our regions. We need to ensure that we aren't painting the same pixels repeatedly, so we also keep track of the pixels we have "visited".

def color_dist(color1, color2): """ Computes the Euclidean distance between two colors """ c1r, c1g, c1b = color1 c2r, c2g, c2b = color2 return ((c1r - c2r)**2 + (c1g - c2g)**2 + (c1b - c2b)**2) ** 0.5 from colorsys import hsv_to_rgb def color_in_equal_space(hue, saturation=0.55, value=2.3): """ Produces equally spaced colors as an RGB tuple """ golden_ratio = (1 + 5 ** 0.5) / 2 hue += golden_ratio hue %= 1 return tuple(int(a*100) for a in hsv_to_rgb(hue, saturation, value)) colored = {} dist_threshold = 15 # maximal distance for k, pixel in enumerate(pixels): bgc = color_in_equal_space(k/most_common) for pixel_location in dd[pixel]: j, i = divmod(pixel_location, width) if (i,j) not in colored: impixels[i,j] = bgc colored[i,j] = True # Measure distance from this pixel to all others (costly) distances = {(i,j): color_dist(pixel, impixels[i,j]) for i in range(width) for j in range(height)} # Find pixels whose distance from the current pixel is small coord_distances = sorted(distances.items(), key=lambda x: x[1]) filtered = [coord for coord, distance in coord_distances if distance < dist_threshold] for i, j in filtered: if (i,j) not in colored: impixels[i,j] = bgc colored[i,j] = True im.show()

We obtain the following result:

Fig. 1: Original image
Fig. 2: Image with pixels within a certain distance from the most common pixels

We could do the same using pixels from the sea, the cliffs and the forest, also expanding our dist_threshold from 15 to 35 and painting in red, green and blue:

Fig. 3: Pixels within a certain distance from pixel probes from the sea, the cliffs and the forest

We see that some of the colors of the sky are within a certain distance from the colors of the sea, which gives the image a smooth, mild look, especially at the upper left corner. Once we chose a single pixel from the cliffs, we have some understanding that there might be a path. And once we chose a single pixel from the forest, we can see potentially the bounds to which it extends in height, and where it gets eventually replaced by rocks, even though the tones could be too similar to separate by the eye.