1

I have a Python code that identifies dark images:

import os import glob import cv2 import numpy as np def isbright(image, dim=10, thresh=0.16): # Resize image to 10x10 image = cv2.resize(image, (dim, dim)) # Convert color space to LAB format and extract L channel L, A, B = cv2.split(cv2.cvtColor(image, cv2.COLOR_BGR2LAB)) # Normalize L channel by dividing all pixel values with maximum pixel value L = L/np.max(L) # Return True if mean is greater than thresh else False return np.mean(L) > thresh # create output directories if not exists os.makedirs("output/bright", exist_ok=True) os.makedirs("output/dark", exist_ok=True) # iterate through images directory for i, path in enumerate(os.listdir(os.path.abspath(''))): # load image from path image = cv2.imread(path) # find if image is bright or dark path = os.path.basename(path) text = "bright" if isbright(image) else "dark" # save image to disk cv2.imwrite("output/{}/{}".format(text, path), image) print(path, "=>", text) 

I'd like to also identify, for example, mostly red images, mostly yellow images and so on. Basically mostly uniform colors in pictures keeping the same code structure, more or less. How would you guys do it?

Samples:

JPG Sample1

JPG Sample

JPG Sample3

6
  • I'd start with a proper, representative set of sample images in the correct format (JPEG/PNG/TIFF) and a clear specification of which colours I wanted to identify and parameters that tell me how I know I have reached the definition of "mostly red/yellow". Along with what to do when an image doesn't meet any criteria or meets multiple criteria. Commented Jan 4, 2023 at 15:03
  • It's like the code, 84% of dark in images considers that image "dark". I'd like to do the same but with colors. Commented Jan 4, 2023 at 16:51
  • I was suggesting you 1) provide representative images so I know if they are PNG or JPEG which store colours differently and how the content looks 2) a list of the colours you want to identify since I don't know if there are 3 or 42,000 colours or what they are and so on. Else your question can only be answered by guessing, which is a waste of time. Commented Jan 4, 2023 at 17:03
  • 1
    this looks like clustering. cluster the colors of all pixels. analyze the clusters. yes, this is not an easy problem and it doesn't have just one solution. Commented Jan 5, 2023 at 5:16
  • That’s a good approach. I’ll look into it. Commented Jan 5, 2023 at 22:34

2 Answers 2

4

Getting the average color of an image is almost trivial - if you resize it to a single pixel with INTER_AREA, the color of that pixel will be the overall average. Then all you need is a way to tell how closely the image colors map to that average. A good way to do that is to calculate the variance with the mean squared error (MSE). Setting an appropriate threshold will be a matter of trial and error.

def average_color(image): ''' Return the average color (R,G,B) of the image and the variance from that average. ''' bgr = cv2.resize(image, (1,1), interpolation=cv2.INTER_AREA)[0,0] mse = ((image.astype(np.float64) - bgr) ** 2).mean() return (bgr[2], bgr[1], bgr[0]), mse 

This function will work with either a full size image or a resized one as you used in your example, although the variance will be different. I'd advise using interpolation=cv2.INTER_AREA in your resize too.

For your 3 test images, it returned the following:

((231, 64, 72), 387.9535165895062) ((238, 100, 55), 834.753994984568) ((196, 153, 86), 2236.9471739969135) 

You didn't specify how you wanted to classify the color, so I came up with a way to do that too. This code maps an RGB value to the nearest of the CSS color names. You can substitute your own table of color names, as long or as short as you want.

colors = {'AliceBlue': (240, 248, 255), 'AntiqueWhite': (250, 235, 215), 'Aqua': (0, 255, 255), 'Aquamarine': (127, 255, 212), 'Azure': (240, 255, 255), 'Beige': (245, 245, 220), 'Bisque': (255, 228, 196), 'Black': (0, 0, 0), 'BlanchedAlmond': (255, 235, 205), 'Blue': (0, 0, 255), 'BlueViolet': (138, 43, 226), 'Brown': (165, 42, 42), 'BurlyWood': (222, 184, 135), 'CadetBlue': (95, 158, 160), 'Chartreuse': (127, 255, 0), 'Chocolate': (210, 105, 30), 'Coral': (255, 127, 80), 'CornflowerBlue': (100, 149, 237), 'Cornsilk': (255, 248, 220), 'Crimson': (220, 20, 60), 'Cyan': (0, 255, 255), 'DarkBlue': (0, 0, 139), 'DarkCyan': (0, 139, 139), 'DarkGoldenRod': (184, 134, 11), 'DarkGray': (169, 169, 169), 'DarkGreen': (0, 100, 0), 'DarkGrey': (169, 169, 169), 'DarkKhaki': (189, 183, 107), 'DarkMagenta': (139, 0, 139), 'DarkOliveGreen': (85, 107, 47), 'DarkOrange': (255, 140, 0), 'DarkOrchid': (153, 50, 204), 'DarkRed': (139, 0, 0), 'DarkSalmon': (233, 150, 122), 'DarkSeaGreen': (143, 188, 143), 'DarkSlateBlue': (72, 61, 139), 'DarkSlateGray': (47, 79, 79), 'DarkSlateGrey': (47, 79, 79), 'DarkTurquoise': (0, 206, 209), 'DarkViolet': (148, 0, 211), 'DeepPink': (255, 20, 147), 'DeepSkyBlue': (0, 191, 255), 'DimGray': (105, 105, 105), 'DimGrey': (105, 105, 105), 'DodgerBlue': (30, 144, 255), 'FireBrick': (178, 34, 34), 'FloralWhite': (255, 250, 240), 'ForestGreen': (34, 139, 34), 'Fuchsia': (255, 0, 255), 'Gainsboro': (220, 220, 220), 'GhostWhite': (248, 248, 255), 'Gold': (255, 215, 0), 'GoldenRod': (218, 165, 32), 'Gray': (128, 128, 128), 'Green': (0, 128, 0), 'GreenYellow': (173, 255, 47), 'Grey': (128, 128, 128), 'HoneyDew': (240, 255, 240), 'HotPink': (255, 105, 180), 'IndianRed ': (205, 92, 92), 'Indigo ': (75, 0, 130), 'Ivory': (255, 255, 240), 'Khaki': (240, 230, 140), 'Lavender': (230, 230, 250), 'LavenderBlush': (255, 240, 245), 'LawnGreen': (124, 252, 0), 'LemonChiffon': (255, 250, 205), 'LightBlue': (173, 216, 230), 'LightCoral': (240, 128, 128), 'LightCyan': (224, 255, 255), 'LightGoldenRodYellow': (250, 250, 210), 'LightGray': (211, 211, 211), 'LightGreen': (144, 238, 144), 'LightGrey': (211, 211, 211), 'LightPink': (255, 182, 193), 'LightSalmon': (255, 160, 122), 'LightSeaGreen': (32, 178, 170), 'LightSkyBlue': (135, 206, 250), 'LightSlateGray': (119, 136, 153), 'LightSlateGrey': (119, 136, 153), 'LightSteelBlue': (176, 196, 222), 'LightYellow': (255, 255, 224), 'Lime': (0, 255, 0), 'LimeGreen': (50, 205, 50), 'Linen': (250, 240, 230), 'Magenta': (255, 0, 255), 'Maroon': (128, 0, 0), 'MediumAquaMarine': (102, 205, 170), 'MediumBlue': (0, 0, 205), 'MediumOrchid': (186, 85, 211), 'MediumPurple': (147, 112, 219), 'MediumSeaGreen': (60, 179, 113), 'MediumSlateBlue': (123, 104, 238), 'MediumSpringGreen': (0, 250, 154), 'MediumTurquoise': (72, 209, 204), 'MediumVioletRed': (199, 21, 133), 'MidnightBlue': (25, 25, 112), 'MintCream': (245, 255, 250), 'MistyRose': (255, 228, 225), 'Moccasin': (255, 228, 181), 'NavajoWhite': (255, 222, 173), 'Navy': (0, 0, 128), 'OldLace': (253, 245, 230), 'Olive': (128, 128, 0), 'OliveDrab': (107, 142, 35), 'Orange': (255, 165, 0), 'OrangeRed': (255, 69, 0), 'Orchid': (218, 112, 214), 'PaleGoldenRod': (238, 232, 170), 'PaleGreen': (152, 251, 152), 'PaleTurquoise': (175, 238, 238), 'PaleVioletRed': (219, 112, 147), 'PapayaWhip': (255, 239, 213), 'PeachPuff': (255, 218, 185), 'Peru': (205, 133, 63), 'Pink': (255, 192, 203), 'Plum': (221, 160, 221), 'PowderBlue': (176, 224, 230), 'Purple': (128, 0, 128), 'RebeccaPurple': (102, 51, 153), 'Red': (255, 0, 0), 'RosyBrown': (188, 143, 143), 'RoyalBlue': (65, 105, 225), 'SaddleBrown': (139, 69, 19), 'Salmon': (250, 128, 114), 'SandyBrown': (244, 164, 96), 'SeaGreen': (46, 139, 87), 'SeaShell': (255, 245, 238), 'Sienna': (160, 82, 45), 'Silver': (192, 192, 192), 'SkyBlue': (135, 206, 235), 'SlateBlue': (106, 90, 205), 'SlateGray': (112, 128, 144), 'SlateGrey': (112, 128, 144), 'Snow': (255, 250, 250), 'SpringGreen': (0, 255, 127), 'SteelBlue': (70, 130, 180), 'Tan': (210, 180, 140), 'Teal': (0, 128, 128), 'Thistle': (216, 191, 216), 'Tomato': (255, 99, 71), 'Turquoise': (64, 224, 208), 'Violet': (238, 130, 238), 'Wheat': (245, 222, 179), 'White': (255, 255, 255), 'WhiteSmoke': (245, 245, 245), 'Yellow': (255, 255, 0), 'YellowGreen': (154, 205, 50)} 
def nearest_lab(rgb): def to_lab(color): return cv2.cvtColor(np.array([[[color[i]/255 for i in range(3)]]], dtype=np.float32), cv2.COLOR_RGB2LAB)[0,0] lab = to_lab(rgb) def dist_squared(item): lab2 = to_lab(item[1]) return sum((lab[i] - lab2[i]) ** 2 for i in range(3)) return min(colors.items(), key=dist_squared)[0] 

Here are the results of that lookup for your 3 test images:

enter image description here

enter image description here

enter image description here


I had second thoughts about using the CSS colors. If you know anything about their history, you know that most of the colors were adapted from X11 which was a product of MIT. The colors and their names in X11 were chosen by a small number of people, and I don't know how much emphasis was placed on their practicality. Who, when seeing that light brown, thinks "Peru"?

I decided to try an alternate color table. Randall Munroe of xkcd crowd-sourced a list of color names and released it to the public at https://xkcd.com/color/rgb/. It's a much larger list than CSS, and since it required lots of people agreeing on the name of a color there's a chance it would be more useful. The code for the new color table is too long to put in this answer, but if you're interested leave a comment and I'll find a way to get it to you.

Here are the updated results again for the 3 test images. I don't think the first 2 really improved at all, but I was impressed by the third - it's almost dead on.

enter image description here

enter image description here

enter image description here

Sign up to request clarification or add additional context in comments.

2 Comments

Nice approach. It's great when everyone comes at a problem from different angles.
@MarkSetchell yes it's great to see totally different solutions. It makes the site richer.
3

I had a try at this by converting to HSV colourspace and looking for the 6 vertices of the HSV Hue wheel, namely red, yellow, green, cyan, blue and magenta as identified in the lower part of this diagram from the previously linked page.

I convert the input image to HSV colourspace and use cv2.inRange() to find each of the 6 colours. There are two things to note:

  • OpenCV uses a range of 0..180 for Hue so that it fits into a np.uint8 so you need to halve all the values in the conventional 0..360 Hue wheel. I mean green shows as 120 in the conventional wheel, but I use 60 in the code.

  • the red hues straddle the boundary around zero degrees, i.e. 170..179..0..10 and this makes for code where you have to coalesce two ranges 170..180 and 0..10. Rather than doing that, when looking for red, I invert the image and look for cyan.

I then count the pixels selected by that hue and show as an absolute number and as a percentage.

#!/usr/bin/env python3 import numpy as np import glob import cv2 colours = { 'reds': (90, True), # Hue angle and whether to invert 'yellows': (30, False), 'greens': (60, False), 'cyans': (90, False), 'blues': (120, False), 'magentas': (150, False) } def process(filename): print(f'Image: {filename}') im = cv2.imread(filename) total = im.shape[0] * im.shape[1] # total pixels in image hueTolerance = 20 for name, params in colours.items(): hueAngle, invert = params if invert: hsv = cv2.cvtColor(255-im, cv2.COLOR_BGR2HSV) else: hsv = cv2.cvtColor(im, cv2.COLOR_BGR2HSV) # Set low and high limit for this colour lo = np.uint8([hueAngle-hueTolerance,10,0]) hi = np.uint8([hueAngle+hueTolerance,255,255]) # Get in range pixels and count them inRange = cv2.inRange(hsv,lo,hi) N = cv2.countNonZero(inRange) percent = (N * 100)/ total print(f' {name}: {N}/{total} ({percent:.1f})') def main(): for image in glob.glob('mostly-*'): process(image) if __name__ == "__main__": main() 

For your three images, which I saved as mostly-red.jpg, mostly-orange.jpg and mostly-yellow.jpg, I get these results:

Image: mostly-yellow.jpg reds: 152901/172800 (88.5) yellows: 147459/172800 (85.3) greens: 0/172800 (0.0) cyans: 0/172800 (0.0) blues: 16476/172800 (9.5) magentas: 9335/172800 (5.4) Image: mostly-red.jpg reds: 172800/172800 (100.0) yellows: 0/172800 (0.0) greens: 0/172800 (0.0) cyans: 0/172800 (0.0) blues: 0/172800 (0.0) magentas: 0/172800 (0.0) Image: mostly-orange.jpg reds: 172800/172800 (100.0) yellows: 18749/172800 (10.9) greens: 0/172800 (0.0) cyans: 0/172800 (0.0) blues: 0/172800 (0.0) magentas: 0/172800 (0.0) 

Note that you didn't answer when I asked what colours you are looking for so I didn't add "orange" to the list.

Note that there is a tolerance on each colour, so the percentages will not sum to 100, because some shades say of orange could get counted as looking like both red and yellow. You can define the tolerance more closely and increase the number of different colours you are looking for in the list of colours at the start.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.