Detecta si la imagen es de color, escala de grises o blanco y negro con Python / PIL

Extraigo las imágenes de las páginas de un archivo PDF en formato jpeg y necesito determinar si cada imagen es mucho más en escala de grises, color o blanco y negro (con un factor de tolerancia).

He encontrado algunas formas de trabajar con la detección de color con PIL ( aquí y aquí ) pero no puedo averiguar cómo responder a esta simple pregunta (visual): ¿es mucho más blanco y negro, color o imagen en escala de grises?

Prefiero trabajar con Python y PIL para esta parte, pero podría usar también OpenCV si alguien tiene una pista (o solución).

Probé la solución de Gepeto y tiene muchos falsos positivos ya que las grandes variaciones de color pueden ser similares solo por casualidad. La forma correcta de hacer esto es calcular la varianza por píxel. Reduzca la imagen primero para no tener que procesar millones de píxeles.

De forma predeterminada, esta función también utiliza un ajuste de polarización de color medio, que a mi juicio mejora la predicción. Un efecto colateral de esto es que también detectará imágenes monocromáticas pero no en escala de grises (generalmente en tonos sepia, el modelo parece descomponerse un poco al detectar desviaciones más grandes de la escala de grises). Puede separarlos de la escala de grises real mediante el umbral en la banda de color.

Corrí esto en un conjunto de pruebas de 13,000 imágenes fotográficas y obtuve una clasificación con 99.1% de precisión y 92.5% de recordación. La precisión probablemente podría mejorarse aún más mediante el uso de un ajuste de polarización no lineal (los valores de color deben estar entre 0 y 255, por ejemplo). Tal vez mirar el error de la mediana al cuadrado en lugar del MSE permitiría, por ejemplo, imágenes en escala de grises con pequeños sellos de color.

from PIL import Image, ImageStat def detect_color_image(file, thumb_size=40, MSE_cutoff=22, adjust_color_bias=True): pil_img = Image.open(file) bands = pil_img.getbands() if bands == ('R','G','B') or bands== ('R','G','B','A'): thumb = pil_img.resize((thumb_size,thumb_size)) SSE, bias = 0, [0,0,0] if adjust_color_bias: bias = ImageStat.Stat(thumb).mean[:3] bias = [b - sum(bias)/3 for b in bias ] for pixel in thumb.getdata(): mu = sum(pixel)/3 SSE += sum((pixel[i] - mu - bias[i])*(pixel[i] - mu - bias[i]) for i in [0,1,2]) MSE = float(SSE)/(thumb_size*thumb_size) if MSE <= MSE_cutoff: print "grayscale\t", else: print "Color\t\t\t", print "( MSE=",MSE,")" elif len(bands)==1: print "Black and white", bands else: print "Don't know...", bands 

Usamos esta función simple para determinar el factor de color de una imagen.

 # Iterate over all Pixels in the image (width * height times) and do this for every pixel: { int rg = Math.abs(r - g); int rb = Math.abs(r - b); int gb = Math.abs(g - b); diff += rg + rb + gb; } return diff / (height * width) / (255f * 3f); 

Como los valores de gris tienen rg = 0 y rb = 0 y gb = 0 diff estarán cerca de 0 para imágenes en escala de grises y> 0 para imágenes en color.

He encontrado una manera de adivinar esto con el módulo PIL ImageStat. Gracias a este post por la determinación monocromática de una imagen.

 from PIL import Image, ImageStat MONOCHROMATIC_MAX_VARIANCE = 0.005 COLOR = 1000 MAYBE_COLOR = 100 def detect_color_image(file): v = ImageStat.Stat(Image.open(file)).var is_monochromatic = reduce(lambda x, y: x and y < MONOCHROMATIC_MAX_VARIANCE, v, True) print file, '-->\t', if is_monochromatic: print "Monochromatic image", else: if len(v)==3: maxmin = abs(max(v) - min(v)) if maxmin > COLOR: print "Color\t\t\t", elif maxmin > MAYBE_COLOR: print "Maybe color\t", else: print "grayscale\t\t", print "(",maxmin,")" elif len(v)==1: print "Black and white" else: print "Don't know..." 

Las constantes COLOR y MAYBE_COLOR son cambios rápidos para encontrar las diferencias entre las imágenes en color y en escala de grises, pero no son seguras. Como ejemplo, tengo varias imágenes JPEG que se ven como color, pero en realidad son en escala de grises con algunos artefactos de color debido a un proceso de escaneo. Es por eso que tengo otro nivel para notar realmente la imagen en color de los demás.

Si alguien tiene un mejor enfoque, hágamelo saber.

Personalmente prefiero la respuesta de TomB. Esta no es una nueva respuesta, solo quiero publicar la versión de Java:

 private Mat calculateChannelDifference(Mat mat) { // Create channel list: List channels = new ArrayList<>(); for (int i = 0; i < 3; i++) { channels.add(new Mat()); } // Split the channels of the input matrix: Core.split(mat, channels); Mat temp = new Mat(); Mat result = Mat.zeros(mat.size(), CvType.CV_8UC1); for (int i = 0; i < channels.size(); i++) { // Calculate difference between 2 successive channels: Core.absdiff(channels.get(i), channels.get((i + 1) % channels.size()), temp); // Add the difference to the result: Core.add(temp, result, result); } return result; } 

El resultado es la diferencia como una matriz, de esta manera podría aplicar algún umbral e incluso detectar formas. Si desea el resultado como un solo número, solo tendrá que calcular el valor promedio. Esto se puede hacer usando Core.mean()

Puede usar el operador cv :: Mat :: channels () y eso puede decirle si se trata de una imagen de “escala de grises” (es decir, 2 canales) o “color” (es decir, de 3 canales). Para blanco y negro, necesitará establecer pruebas más profundas basadas en escala de grises, ya que la definición varía.