How to check whether a JPEG image is color or gray scale using only Python stdlib
Asked Answered
M

14

43

I have to write a test case in Python to check whether a jpg image is in color or grayscale. Can anyone please let me know if there is any way to do it with out installing extra libraries like OpenCV?

Mccubbin answered 14/5, 2014 at 17:6 Comment(5)
Questions: a) What libraries are not considered extra libraries? NumPy/Scipy? b) Do you want to simply detect 2 vs 3 channels and use this as your grayscale criteria or will you have 3 channel images that are actually grayscale in appearance?Climber
We have only python 2.6 on our linux work stations. There are strict instructions to not use any external libraries to write any of the test cases. So we don't have permissions to install any libraries. We have some 3 channel images that are actually grayscale in appearance.Mccubbin
Do you have any way of opening an image as pixels? If not this is going to be a hard problem.Onus
@Mark Ransom: you mean you can't just trust the JPEG header, offset 6: number of components (1 = grayscale, 3 = RGB) ?Galvani
@Galvani I guess grayscale JPEGs are so rare that I didn't remember it was possible. There will also be cases where a grayscale image is saved with 3 components.Onus
P
26

You can check every pixel to see if it is grayscale (R == G == B)

from PIL import Image

def is_grey_scale(img_path):
    img = Image.open(img_path).convert('RGB')
    w, h = img.size
    for i in range(w):
        for j in range(h):
            r, g, b = img.getpixel((i,j))
            if r != g != b: 
                return False
    return True
Phycomycete answered 14/5, 2014 at 17:33 Comment(10)
Just a performance-enhance for fast results: since many images have black or white border, you'd expect faster termination by sampling random i,j-points from im and test them? Or use modulo arithmetic to traverse the image. If sampling(-without-replacement) say 100 random i,j-points isn't conclusive, then just scan it linearly. Or maybe vary the row order with modulo arithmetic. You could wrap all this in a custom iterator iter_pixels(im).Galvani
Sorry. The code is failing when I tried to run the script and it is giving the error @ r,g,b = im.getpixel((i,j)) TypeError: 'int' object is not iterableMccubbin
Need to add rgb_im = im.convert('RGB')Mccubbin
@Mccubbin if it opens and isn't RGB then you already have your answer - I believe the only other possibility is grayscale. At least for a JPEG.Onus
@MarkRansom, JPEGs are stored in either CMYK, Greyscale, or YUV (with YUV almost always converted to RGB by the parser).Overmuch
@Overmuch by the time the image is opened by most libraries the distinction is lost.Onus
You can even increase the speed by skipping some pixels such as for i in range(0, w, 2) and for j in range(0, h, 2)Variolite
This is grossly inefficient and a poor idea.Marrilee
Besides the fact that the code is the slowest implementation possible, it also has a bug: say r=20, g=20, b=30 (color image): r != g != b: will give False, instead of True.Tadtada
Use Pillow or Numpy (or equivalent) to process the content of an image, it is much more performant (because it can be parallelized efficiently) and less error prone (because you don't have to deal with loops). See this answer for example.Ancylostomiasis
I
30

Can be done as follow:

from scipy.misc import imread, imsave, imresize
image = imread(f_name)
if(len(image.shape)<3):
      print 'gray'
elif len(image.shape)==3:
      print 'Color(RGB)'
else:
      print 'others'
Impost answered 21/6, 2015 at 21:29 Comment(5)
Effective answer. Whether an image is RGB or gray can be determined by its size.Turf
Please note that the 3-channel image can also be a grayscale image. This answer is applied to a 2-channel grayscale image only.Stabler
@FonyLew what do you mean by that? RGB image has 3 channels, grayscale image has 1 channel.Markus
@Markus It's in an ideal case. However, there can be a greyscale image with the 3-channel image (RGB) where R == G == B as mentioned in other comments. The output is visually the same as a grey image, but it is RGB in metadata where this answer doesn't apply to that case. I meant to say that the answer is only counted as greyscale when the number of channels is less than three.Stabler
This is incorrect.Marrilee
P
26

You can check every pixel to see if it is grayscale (R == G == B)

from PIL import Image

def is_grey_scale(img_path):
    img = Image.open(img_path).convert('RGB')
    w, h = img.size
    for i in range(w):
        for j in range(h):
            r, g, b = img.getpixel((i,j))
            if r != g != b: 
                return False
    return True
Phycomycete answered 14/5, 2014 at 17:33 Comment(10)
Just a performance-enhance for fast results: since many images have black or white border, you'd expect faster termination by sampling random i,j-points from im and test them? Or use modulo arithmetic to traverse the image. If sampling(-without-replacement) say 100 random i,j-points isn't conclusive, then just scan it linearly. Or maybe vary the row order with modulo arithmetic. You could wrap all this in a custom iterator iter_pixels(im).Galvani
Sorry. The code is failing when I tried to run the script and it is giving the error @ r,g,b = im.getpixel((i,j)) TypeError: 'int' object is not iterableMccubbin
Need to add rgb_im = im.convert('RGB')Mccubbin
@Mccubbin if it opens and isn't RGB then you already have your answer - I believe the only other possibility is grayscale. At least for a JPEG.Onus
@MarkRansom, JPEGs are stored in either CMYK, Greyscale, or YUV (with YUV almost always converted to RGB by the parser).Overmuch
@Overmuch by the time the image is opened by most libraries the distinction is lost.Onus
You can even increase the speed by skipping some pixels such as for i in range(0, w, 2) and for j in range(0, h, 2)Variolite
This is grossly inefficient and a poor idea.Marrilee
Besides the fact that the code is the slowest implementation possible, it also has a bug: say r=20, g=20, b=30 (color image): r != g != b: will give False, instead of True.Tadtada
Use Pillow or Numpy (or equivalent) to process the content of an image, it is much more performant (because it can be parallelized efficiently) and less error prone (because you don't have to deal with loops). See this answer for example.Ancylostomiasis
P
22

There is more pythonic way using numpy functionality and opencv:

import cv2
def isgray(imgpath):
    img = cv2.imread(imgpath)
    if len(img.shape) < 3: return True
    if img.shape[2]  == 1: return True
    b,g,r = img[:,:,0], img[:,:,1], img[:,:,2]
    if (b==g).all() and (b==r).all(): return True
    return False
Pullover answered 10/11, 2019 at 17:21 Comment(1)
I like this approach. However, note that your code doesn't actually use OpenCV functions in its logic. OpenCV is only used to load the image from file.Adorl
N
21

For faster processing, it is better to avoid loops on every pixel, using ImageChops, (but also to be sure that the image is truly grayscale, we need to compare colors on every pixel and cannot just use the sum):

from PIL import Image,ImageChops

def is_greyscale(im):
    """
    Check if image is monochrome (1 channel or 3 identical channels)
    """
    if im.mode not in ("L", "RGB"):
        raise ValueError("Unsuported image mode")

    if im.mode == "RGB":
        rgb = im.split()
        if ImageChops.difference(rgb[0],rgb[1]).getextrema()[1]!=0: 
            return False
        if ImageChops.difference(rgb[0],rgb[2]).getextrema()[1]!=0: 
            return False
    return True
Nowadays answered 9/12, 2015 at 9:51 Comment(1)
Best answer here.Marrilee
G
7

In case of a grayscale image, all channels in a certain pixel are equal (if you only have one channel, then you don't have a problem). So basically, you can list all the pixels with their three channel values to check if each pixel has all three channels equal.

Image.getcolors() returns an unsorted list of (count, pixel) values.

im = Image.open('path_to_image.whatever')
color_count = im.getcolors()

If len(color_count) exceeds 256 (default max value), this function returns None, meaning you had more than 256 color options in your pixel list, hence it is a colored image (grayscale can only have 256 colors, (0,0,0) to (255,255,255)).

So after that you only need :

if color_count: 
    # your image is grayscale
else:
    # your images is colored

Note this will work only when using the default parameter value of getcolors().

Documentation: https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.getcolors

Giraud answered 1/12, 2020 at 10:53 Comment(2)
I tested this on a grayscale image (single-channel), an image with color (with and without transparency), and an image without color (converted to black/white mode in Photoshop, with and without transparency). All tests produced the expected result. I don't understand why this isn't the accepted answer. This is the simplest and cleanest approach of all the answers, and it works reliably!Bombacaceous
Huh? An image which is half red and half blue will have 2 colours, so you will report it as greyscale because that is less than 256?Marrilee
G
5

A performance-enhance for fast results: since many images have black or white border, you'd expect faster termination by sampling a few random i,j-points from im and test them? Or use modulo arithmetic to traverse the image rows. First we sample(-without-replacement) say 100 random i,j-points; in the unlikely event that isn't conclusive, then we scan it linearly.

Using a custom iterator iterpixels(im). I don't have PIL installed so I can't test this, here's the outline:

import Image

def isColor(r,g,b): # use tuple-unpacking to unpack pixel -> r,g,b
    return (r != g != b)

class Image_(Image):
    def __init__(pathname):
        self.im = Image.open(pathname)
        self.w, self.h = self.im.size
    def iterpixels(nrand=100, randseed=None):
        if randseed:
            random.seed(randseed) # For deterministic behavior in test
        # First, generate a few random pixels from entire image
        for randpix in random.choice(im, n_rand)
            yield randpix
        # Now traverse entire image (yes we will unwantedly revisit the nrand points once)
        #for pixel in im.getpixel(...): # you could traverse rows linearly, or modulo (say) (im.height * 2./3) -1
        #    yield pixel

    def is_grey_scale(img_path="lena.jpg"):
        im = Image_.(img_path)
        return (any(isColor(*pixel)) for pixel in im.iterpixels())

(Also my original remark stands, first you check the JPEG header, offset 6: number of components (1 = grayscale, 3 = RGB). If it's 1=grayscale, you know the answer already without needing to inspect individual pixels.)

Galvani answered 14/5, 2014 at 18:12 Comment(0)
U
5

I faced a similar situation, where I tried the following approaches:

  1. Reading using IMREAD_UNCHANGEDand checking for image.shape
  2. Splitting B,G,R channels and checking if they are equal

Both of these approaches got me only like 53% accuracy in my dataset. I had to relax the condition for checking pixels in different channels and create a ratio to classify it as grey or color. With this approach, I was able to get 87.3% accuracy on my dataset.

Here is the logic which worked for me:

import cv2
import numpy as np

###test image
img=cv2.imread('test.jpg')

### splitting b,g,r channels
b,g,r=cv2.split(img)

### getting differences between (b,g), (r,g), (b,r) channel pixels
r_g=np.count_nonzero(abs(r-g))
r_b=np.count_nonzero(abs(r-b))
g_b=np.count_nonzero(abs(g-b))

### sum of differences
diff_sum=float(r_g+r_b+g_b)

### finding ratio of diff_sum with respect to size of image
ratio=diff_sum/img.size

if ratio>0.005:
    print("image is color")
else:
    print("image is greyscale")
Unorthodox answered 15/7, 2020 at 12:37 Comment(3)
This error is showing after using this code not enough values to unpack (expected 3, got 0)Snakebird
check if the image path was given correctly. After img=cv2.imread('path/to/image.jpg') do a print(img.shape) and what is the output.Unorthodox
abs not needed, only two differences neededHammertoe
B
3

Old question but I needed a different solution. Sometimes 3 channel images (eg RGB) might be almost grayscale without every pixel being identical in 3 channels. This checks every pixel but you can also subsample the image if needed. I used slope here but you can use checks on most of these parmaters from the regression. Linear regressions are usually very fast due to internal matrix multiply solution.

import glob
import scipy
import cv2
THRESH = 0.01
BASEDIR = 'folder/*.jpg'

files = glob.glob(BASEDIR)
for file in files:
    img = cv2.imread(file)
    slope1, intercept1, r1, p1, se1 = scipy.stats.linregress(img[:,:,0].flatten(),img[:,:,1].flatten())
    slope2, intercept2, r2, p2, se2 = scipy.stats.linregress(img[:,:,0].flatten(),img[:,:,2].flatten())
    if abs(slope1 - 1) > THRESH or abs(slope2 - 1) > THRESH:
        print(f'{file} is colour')
    else:
        print(f'{file} is close to grey scale')
Burrow answered 17/12, 2022 at 12:42 Comment(1)
This approach is interesting and it can be improved considering https://mcmap.net/q/390290/-fitting-a-line-in-3d, but I think it has a weakness due to the fact that, for some color images (very far from grayscale), colors might fit very close to a grayscale image. Just think of an image made of the 3 pixels (0,255,255), (255,0,255) and (255,255,0); if I understand your code, it would be detected as perfect grayscale, but it's not. Anyway a vote up for considering the almost grayscale topic.Seve
B
2

Why wouldn't we use ImageStat module?

from PIL import Image, ImageStat

def is_grayscale(path="image.jpg")

    im = Image.open(path).convert("RGB")
    stat = ImageStat.Stat(im)

    if sum(stat.sum)/3 == stat.sum[0]:
        return True
    else:
        return False

stat.sum gives us a sum of all pixels in list view = [R, G, B] for example [568283302.0, 565746890.0, 559724236.0]. For grayscale image all elements of list are equal.

Boutonniere answered 24/9, 2014 at 17:47 Comment(1)
an image composed of an equal number of pure red, pure green and pure blue pixels would wrongly identify as greyscaleArgillite
M
1

The best way of checking if a JPEG is greyscale with Pillow, is to open and see if it is in L mode which Pillow uses for single-channel, greyscale images. If it is L mode there is nothing else needs doing and you have your answer.

Then you should be aware it could be in CMYK mode, so you should convert it to RGB mode for the following step.

Next, you should convert to HSV mode and get the maximum saturation of any pixel. If all pixels are black/white or grey, that will be zero. As the colours in the image become more saturated towards pure reds, greens, blues, cyans, magentas and yellows, so the maximum saturation will tend towards 255.

#!/usr/bin/env python3

from PIL import Image,ImageChops
import sys

def isGreyscale(im):
    """Check if image is greyscale, i.e. unsaturated."""

    # If mode is 'L', image is necessarily greyscale, nothing else needs doing
    if im.mode == 'L':
       return True

    # Check if image is not RGB, it may be CMYK and could be Palette if PNG image
    if im.mode != 'RGB':
        im = im.convert('RGB')

    # Convert to HSV and extract saturation
    # https://en.wikipedia.org/wiki/HSL_and_HSV
    HSV = im.convert('HSV')
    H, S, V = HSV.split()

    # Get minimum and maximum saturation
    minSat, maxSat = S.getextrema()
    print(f'{maxSat=}')

    # maxSat will approach zero for black, white, and grey images and reach 255 for staturated, colourful images
    if maxSat < 1:
        return True
    return False

if __name__ == "__main__":
    im = Image.open(sys.argv[1]);
    result = isGreyscale(im)
    print(result)

Note that if you are processing images in Python with for loops, you have almost certainly gone wrong - that is inefficient and error-prone. Try to use Pillow's built-in functions which are coded in C, or Numpy or OpenCV which is SIMD-optimised.

Marrilee answered 13/6 at 14:19 Comment(0)
O
0

Here is a version of Alexey Antonenko answer using PIL.image instead of cv2. In case you have float images I think it is safer to use the np.allclose function.

from PIL import Image
import numpy as np
def isgray(imgpath):
    img_pil = Image.open(imgpath)
    img = np.asarray(img_pil)
    if len(img.shape) < 3: return True
    if img.shape[2]  == 1: return True
    r,g,b = img[:,:,0], img[:,:,1], img[:,:,2]
    if np.allclose(r,g) and np.allclose(r,b): return True
    return False
Ophiolatry answered 26/12, 2022 at 11:51 Comment(1)
OP specifically states he has JPEGs, so the data cannot be float.Marrilee
C
-1

Can be done as follow:

import numpy as np
import cv2
image = cv2.imread('f_name',cv2.IMREAD_UNCHANGED)
if(len(image.shape)<3):
  if(len(np.unique(image)) == 2):
    print('Binary Image')
  else:
    print('gray')
elif(len(image.shape))==3:
  print('Color(RGB)')
else:
  print('others')
Criminate answered 14/2 at 11:59 Comment(0)
T
-1

Just adding my 2 cents from the performance perspective. I tested 4 implementations on my set of 368 images, each of size 1920x1080, 227 were colour images, 141 were in grayscale. Here are implementations and corresponding time results in seconds (note that time comparison exclude jpeg decoding time):

def is_gray_scale1(img: Image) -> bool:
    img = img.convert('RGB')
    w, h = img.size
    for i in range(w):
        for j in range(h):
            r, g, b = img.getpixel((i, j))
            if r != g or g != b:
                return False
    return True

Elapsed time: 104.14 s

def is_gray_scale2(img: Image) -> bool:
    img = img.convert('RGB')
    np_img = np.array(img)
    r, g, b = np_img[:, :, 0], np_img[:, :, 1], np_img[:, :, 2]
    return np.array_equal(r, g) and np.array_equal(g, b)

Elapsed time: 2.94 s

def is_gray_scale3(img: Image) -> bool:
    if img.mode not in ("L", "RGB"):
        raise ValueError("Unsuported image mode")

    if img.mode == "RGB":
        rgb = img.split()
        if ImageChops.difference(rgb[0], rgb[1]).getextrema()[1] != 0:
            return False
        if ImageChops.difference(rgb[0], rgb[2]).getextrema()[1] != 0:
            return False
    return True

Elapsed time: 1.47 s

def is_gray_scale4(img: Image) -> bool:
    color_count = img.getcolors()
    if color_count:
        return True
    else:
        return False

Elapsed time: 0.34 s (note this is buggy implementation, think of entire image being e.g. red)

Tadtada answered 13/6 at 9:53 Comment(1)
Iterating over pixels is hopelessly inefficient and should be avoided. Converting an image, that is potentially already in L mode (i.e. greyscale) into RGB is pointless and inefficient. Counting the colours and assuming less than 256 makes it greyscale is clearly wrong - what about an image that is half red and half blue - it will have two colours.Marrilee
S
-3

As you are probably correct, OpenCV may be an overkill for this task but it should be okay to use Python Image Library (PIL) for this. The following should work for you:

import Image
im = Image.open("lena.jpg")

EDIT As pointed out by Mark and JRicardo000, you may iterate over each pixel. You could also make use of the im.split() function here.

Schuler answered 14/5, 2014 at 17:15 Comment(1)
The mode is always going to be RGB from a JPEG. You need to actually examine the pixels.Onus

© 2022 - 2024 — McMap. All rights reserved.