tratamiento reconocimiento procesamiento objetos libro imagenes deteccion curso con python opencv image-processing computer-vision image-thresholding

procesamiento - reconocimiento de imagenes opencv python



Ajuste automático de contraste y brillo de una foto en color de una hoja de papel con OpenCV (5)

Al fotografiar una hoja de papel (por ejemplo, con la cámara del teléfono), obtengo el siguiente resultado (imagen de la izquierda) (descarga de jpg here ). El resultado deseado (procesado manualmente con un software de edición de imágenes) está a la derecha:

Me gustaría procesar la imagen original con openCV para obtener un mejor brillo / contraste automáticamente (para que el fondo sea más blanco) .

Supuesto: la imagen tiene un formato de retrato A4 (no es necesario deformarla en este tema aquí), y la hoja de papel es blanca con posiblemente texto / imágenes en negro o en colores.

Lo que he probado hasta ahora:

  1. Varios métodos de umbrales adaptativos , como Gaussian, OTSU (consulte OpenCV doc Image Thresholding ). Por lo general, funciona bien con OTSU:

    ret, gray = cv2.threshold(img, 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY)

    pero solo funciona para imágenes en escala de grises y no directamente para imágenes en color. Además, la salida es binaria (blanca o negra), que no quiero : prefiero mantener una imagen de color no binario como salida

  2. Ecualización de histograma

    • aplicado en Y (después de RGB => YUV transform)
    • o aplicado en V (después de RGB => transformada HSV),

    como lo sugiere esta answer (la ecualización de histograma no funciona en la imagen en color - OpenCV ) o esta ( imagen de color OpenCV Python equalizeHist ):

    img3 = cv2.imread(f) img_transf = cv2.cvtColor(img3, cv2.COLOR_BGR2YUV) img_transf[:,:,0] = cv2.equalizeHist(img_transf[:,:,0]) img4 = cv2.cvtColor(img_transf, cv2.COLOR_YUV2BGR) cv2.imwrite(''test.jpg'', img4)

    o con HSV:

    img_transf = cv2.cvtColor(img3, cv2.COLOR_BGR2HSV) img_transf[:,:,2] = cv2.equalizeHist(img_transf[:,:,2]) img4 = cv2.cvtColor(img_transf, cv2.COLOR_HSV2BGR)

    Desafortunadamente, el resultado es bastante malo ya que crea terriblemente micro contrastes a nivel local (?):

    También probé YCbCr en su lugar, y fue similar.

  3. También probé CLAHE (Ecualización de histograma adaptativa limitada de contraste) con varios tileGridSize de 1 a 1000 :

    img3 = cv2.imread(f) img_transf = cv2.cvtColor(img3, cv2.COLOR_BGR2HSV) clahe = cv2.createCLAHE(tileGridSize=(100,100)) img_transf[:,:,2] = clahe.apply(img_transf[:,:,2]) img4 = cv2.cvtColor(img_transf, cv2.COLOR_HSV2BGR) cv2.imwrite(''test.jpg'', img4)

    Pero el resultado fue igualmente horrible.

  4. Realización de este método CLAHE con espacio de color LAB, como se sugiere en la pregunta Cómo aplicar CLAHE en imágenes de color RGB :

    import cv2, numpy as np bgr = cv2.imread(''_example.jpg'') lab = cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB) lab_planes = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=2.0,tileGridSize=(100,100)) lab_planes[0] = clahe.apply(lab_planes[0]) lab = cv2.merge(lab_planes) bgr = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR) cv2.imwrite(''_example111.jpg'', bgr)

    dio mal resultado también. Imagen de salida:

  5. Hacer un umbral adaptativo o una ecualización de histograma por separado en cada canal (R, G, B) no es una opción, ya que podría interferir con el balance de color, como se explica here .

  6. Método de " scikit-image contraste" del scikit-image de scikit-image sobre la ecualización de histogramas :

    la imagen se vuelve a escalar para incluir todas las intensidades que caen dentro de los percentiles 2 y 98

    es un poco mejor, pero aún está lejos del resultado deseado (vea la imagen en la parte superior de esta pregunta).

TL; DR: ¿cómo obtener una optimización automática de brillo / contraste de una foto en color de una hoja de papel con OpenCV / Python? ¿Qué tipo de umbrales / ecualización de histograma / otra técnica podría ser utilizada?


Binarización suave robusta adaptada localmente! Así es como lo llamo.

He hecho cosas similares antes, para un propósito un poco diferente, por lo que puede que no se ajuste perfectamente a tus necesidades, pero espero que ayude (también escribí este código por la noche para uso personal, por lo que es feo). En cierto sentido, este código estaba destinado a resolver un caso más general en comparación con el suyo, en el que podemos tener una gran cantidad de ruido estructurado en el fondo (consulte la demostración a continuación).

¿Qué hace este código? Dada una foto de una hoja de papel, la blanqueará para que pueda imprimirse perfectamente. Vea las imágenes de ejemplo a continuación.

Teaser: así se verán sus páginas después de este algoritmo (antes y después). Tenga en cuenta que incluso las anotaciones del marcador de color han desaparecido, por lo que no sé si esto se ajustará a su caso de uso, pero el código podría ser útil:

Para obtener resultados perfectamente limpios , es posible que tenga que jugar un poco con los parámetros de filtrado, pero como puede ver, incluso con los parámetros predeterminados funciona bastante bien.

Paso 0: Corta las imágenes para que se ajusten a la página.

Supongamos que de alguna manera hiciste este paso (parece que en los ejemplos que proporcionaste). Si necesita una herramienta manual de anotación y reinversión, solo pm! ^^ Los resultados de este paso están a continuación (los ejemplos que uso aquí son posiblemente más difíciles que los que proporcionaste, aunque es posible que no coincidan exactamente con tu caso):

De esto podemos ver inmediatamente los siguientes problemas:

  • La condición de aligeramiento no es uniforme. Esto significa que todos los métodos de binarización simples no funcionarán. Probé muchas soluciones disponibles en OpenCV , así como sus combinaciones, ¡ninguna de ellas funcionó!
  • Mucho ruido de fondo. En mi caso, necesitaba quitar la cuadrícula del papel y también la tinta del otro lado del papel que se ve a través de la hoja delgada.

Paso 1: Corrección gamma

El razonamiento de este paso es equilibrar el contraste de toda la imagen (ya que su imagen puede estar ligeramente sobreexpuesta o subexpuesta según las condiciones de iluminación).

Esto puede parecer al principio como un paso innecesario, pero su importancia no puede ser subestimada: en cierto sentido, normaliza las imágenes a distribuciones similares de exposiciones, de modo que puede elegir hiper-parámetros significativos más adelante (por ejemplo, el parámetro DELTA en En la siguiente sección, los parámetros de filtrado de ruido, los parámetros para materiales morfológicos, etc.)

from skimage.filters import threshold_yen from skimage.exposure import rescale_intensity from skimage.io import imread, imsave img = imread(''mY7ep.jpg'') yen_threshold = threshold_yen(img) bright = rescale_intensity(img, (0, yen_threshold), (0, 255)) imsave(''out.jpg'', bright)

Aquí están los resultados del ajuste de gamma:

Puedes ver que es un poco más ... "equilibrado" ahora. ¡Sin este paso, todos los parámetros que seleccionará a mano en pasos posteriores serán menos robustos!

Paso 2: Binarización adaptativa para detectar las manchas de texto

En este paso, binarizaremos de forma adaptable los blobs de texto. Agregaré más comentarios más adelante, pero la idea básicamente es la siguiente:

  • Dividimos la imagen en bloques de tamaño BLOCK_SIZE . El truco consiste en elegir su tamaño lo suficientemente grande para que aún consigas una gran porción de texto y fondo (es decir, más grande que cualquier símbolo que tengas), pero lo suficientemente pequeño como para no sufrir ninguna variación en las condiciones de iluminación (es decir, "grande, pero aún así local").
  • Dentro de cada bloque, hacemos binarización adaptativa localmente: observamos el valor de la mediana y suponemos que es el fondo (porque elegimos BLOCK_SIZE suficientemente grande para tener la mayoría como fondo). Luego, definimos con más detalle DELTA , básicamente solo un umbral de "¿a qué distancia de la mediana lo seguiremos considerando como fondo?".

Por lo tanto, la función process_image hace el trabajo. Además, puede modificar las funciones de preprocess y postprocess para adaptarlas a sus necesidades (sin embargo, como puede ver en el ejemplo anterior, el algoritmo es bastante robusto , es decir, funciona bastante bien sin necesidad de modificar los parámetros). ).

El código de esta parte asume que el primer plano es más oscuro que el fondo (es decir, tinta sobre papel). Pero puede cambiarlo fácilmente ajustando la función de preprocess : en lugar de 255 - image , devuelva solo image .

magick image.jpg -colorspace HCL -channel 1 -separate +channel tmp1.png

Los resultados son agradables manchas como esta, siguiendo de cerca el rastro de tinta:

Paso 3: La parte "blanda" de la binarización

Teniendo las manchas que cubren los símbolos y un poco más, finalmente podemos hacer el procedimiento de blanqueamiento.

Si observamos más de cerca las fotos de las hojas de papel con texto (especialmente las que tienen escritos a mano), la transformación de "fondo" (papel blanco) a "primer plano" (la tinta de color oscuro) no es nítida, sino muy gradual. . Otras respuestas basadas en la binarización en esta sección proponen un umbral sencillo (incluso si son adaptativos a nivel local, sigue siendo un umbral), que funciona bien para el texto impreso, pero producirá resultados no tan bonitos con los escritos a mano.

Entonces, la motivación de esta sección es que queremos preservar ese efecto de transmisión gradual del negro al blanco, al igual que las fotos naturales de hojas de papel con tinta natural. El propósito final para eso es hacer que sea imprimible.

La idea principal es simple: cuanto más difiere el valor de píxel (después del umbral anterior) con el valor mínimo local, más probable es que pertenezca al fondo. Podemos expresar esto utilizando una familia de funciones sigmoideas , reescaladas al rango del bloque local (de modo que esta función se adapte de forma adaptativa a la imagen).

magick tmp1.png -auto-threshold otsu tmp2.png

Algunas cosas están comentadas ya que son opcionales. La función combine_process toma la máscara del paso anterior y ejecuta todo el proceso de composición. Puede intentar jugar con ellos para sus datos específicos (imágenes). Los resultados son claros:

Probablemente agregaré más comentarios y explicaciones al código en esta respuesta. Cargará todo (junto con el código de recorte y deformación) en Github.


Creo que la forma de hacerlo es 1) Extraer el canal de croma (saturación) del espacio de color HCL. (HCL funciona mejor que HSL o HSV). Solo los colores deben tener una saturación distinta de cero, por lo que los tonos brillantes y grises serán oscuros. 2) Umbral que resulta utilizando un umbral de otsu para usar como máscara. 3) Convierta su entrada a escala de grises y aplique un umbral de área local (es decir, adaptable). 4) coloque la máscara en el canal alfa del original y luego componga el resultado del umbral del área local con el original, de modo que mantenga el área coloreada del original y en todas partes use el resultado del umbral local.

Lo siento, no conozco bien el OpeCV, pero aquí están los pasos para usar ImageMagick.

Tenga en cuenta que los canales se numeran empezando por 0. (H = 0 o rojo, C = 1 o verde, L = 2 o azul)

Entrada:

β = -minimum_gray * α


import cv2 import numpy as np from matplotlib import pyplot as plt # Automatic brightness and contrast optimization with optional histogram clipping def automatic_brightness_and_contrast(image, clip_hist_percent=1): gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # Calculate grayscale histogram hist = cv2.calcHist([gray],[0],None,[256],[0,256]) hist_size = len(hist) # Calculate cumulative distribution from the histogram accumulator = [] accumulator.append(float(hist[0])) for index in range(1, hist_size): accumulator.append(accumulator[index -1] + float(hist[index])) # Locate points to clip maximum = accumulator[-1] clip_hist_percent *= (maximum/100.0) clip_hist_percent /= 2.0 # Locate left cut minimum_gray = 0 while accumulator[minimum_gray] < clip_hist_percent: minimum_gray += 1 # Locate right cut maximum_gray = hist_size -1 while accumulator[maximum_gray] >= (maximum - clip_hist_percent): maximum_gray -= 1 # Calculate alpha and beta values alpha = 255 / (maximum_gray - minimum_gray) beta = -minimum_gray * alpha '''''' # Calculate new histogram with desired range and show histogram new_hist = cv2.calcHist([gray],[0],None,[256],[minimum_gray,maximum_gray]) plt.plot(hist) plt.plot(new_hist) plt.xlim([0,256]) plt.show() '''''' auto_result = cv2.convertScaleAbs(image, alpha=alpha, beta=beta) return (auto_result, alpha, beta) image = cv2.imread(''1.jpg'') auto_result, alpha, beta = automatic_brightness_and_contrast(image) print(''alpha'', alpha) print(''beta'', beta) cv2.imshow(''auto_result'', auto_result) cv2.waitKey()


# Somehow I found the value of `gamma=1.2` to be the best in my case def adjust_gamma(image, gamma=1.2): # build a lookup table mapping the pixel values [0, 255] to # their adjusted gamma values invGamma = 1.0 / gamma table = np.array([((i / 255.0) ** invGamma) * 255 for i in np.arange(0, 256)]).astype("uint8") # apply gamma correction using the lookup table return cv2.LUT(image, table)


BLOCK_SIZE = 40 DELTA = 25 def preprocess(image): image = cv2.medianBlur(image, 3) return 255 - image def postprocess(image): kernel = np.ones((3,3), np.uint8) image = cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel) return image def get_block_index(image_shape, yx, block_size): y = np.arange(max(0, yx[0]-block_size), min(image_shape[0], yx[0]+block_size)) x = np.arange(max(0, yx[1]-block_size), min(image_shape[1], yx[1]+block_size)) return np.meshgrid(y, x) def adaptive_median_threshold(img_in): med = np.median(img_in) img_out = np.zeros_like(img_in) img_out[img_in - med < DELTA] = 255 kernel = np.ones((3,3),np.uint8) img_out = 255 - cv2.dilate(255 - img_out,kernel,iterations = 2) return img_out def block_image_process(image, block_size): out_image = np.zeros_like(image) for row in range(0, image.shape[0], block_size): for col in range(0, image.shape[1], block_size): idx = (row, col) block_idx = get_block_index(image.shape, idx, block_size) out_image[block_idx] = adaptive_median_threshold(image[block_idx]) return out_image def process_image(img): image_in = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) image_in = preprocess(image_in) image_out = block_image_process(image_in, BLOCK_SIZE) image_out = postprocess(image_out) return image_out


ADICIÓN:

Aquí está el código de Python Wand, que produce el mismo resultado de salida. Necesita Imagemagick 7 y Wand 0.5.5.

def sigmoid(x, orig, rad): k = np.exp((x - orig) * 5 / rad) return k / (k + 1.) def combine_block(img_in, mask): img_out = np.zeros_like(img_in) img_out[mask == 255] = 255 fimg_in = img_in.astype(np.float32) idx = np.where(mask == 0) if idx[0].shape[0] == 0: img_out[idx] = img_in[idx] return img_out lo = fimg_in[idx].min() hi = fimg_in[idx].max() v = fimg_in[idx] - lo r = hi - lo img_in_idx = img_in[idx] ret3,th3 = cv2.threshold(img_in[idx],0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU) bound_value = np.min(img_in_idx[th3[:, 0] == 255]) bound_value = (bound_value - lo) / (r + 1e-5) f = (v / (r + 1e-5)) f = sigmoid(f, bound_value + 0.05, 0.2) img_out[idx] = (255. * f).astype(np.uint8) return img_out def combine_block_image_process(image, mask, block_size): out_image = np.zeros_like(image) for row in range(0, image.shape[0], block_size): for col in range(0, image.shape[1], block_size): idx = (row, col) block_idx = get_block_index(image.shape, idx, block_size) out_image[block_idx] = combine_block(image[block_idx], mask[block_idx]) return out_image def combine_postprocess(image): #image = cv2.medianBlur(image, 3) #image = cv2.medianBlur(image, 5) #kernel = np.ones((3,3), np.uint8) #image = cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel) return image def combine_process(img, mask): image_in = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) image_out = combine_block_image_process(image_in, mask, 20) image_out = combine_postprocess(image_out) return image_out


Primero separamos el texto y las marcas de color. Esto se puede hacer en un espacio de color con un canal de saturación de color. En su lugar, utilicé un método muy simple inspirado en este artículo : la ración de mín (R, G, B) / max (R, G, B) estará cerca de 1 para las áreas grises (claras) y << 1 para las áreas coloreadas. Para las áreas de color gris oscuro obtenemos cualquier cosa entre 0 y 1, pero esto no importa: estas áreas van a la máscara de color y luego se agregan tal como están o no se incluyen en la máscara y se contribuyen a la salida del binarizado texto. Para el negro, usamos el hecho de que 0/0 se convierte en 0 cuando se convierte a uint8.

El texto de la imagen en escala de grises obtiene un umbral local para producir una imagen en blanco y negro. Puedes elegir tu técnica favorita de esta comparación o de esa encuesta . Elegí la técnica NICK que hace frente al bajo contraste y es bastante robusta, es decir, la elección del parámetro k entre aproximadamente -0.3 y -0.1 funciona bien para una amplia gama de condiciones, lo que es bueno para el procesamiento automático. Para el documento de muestra proporcionado, la técnica elegida no juega un papel importante, ya que se ilumina de manera relativamente uniforme, pero para hacer frente a las imágenes con iluminación no uniforme, debe ser una técnica de umbral local .

En el paso final, las áreas de color se agregan de nuevo a la imagen de texto binarizada.

Por lo tanto, esta solución es muy similar a la solución de @fmw42 (todo lo que se le atribuye a él) con la excepción de los diferentes métodos de detección de color y binarización.

image = cv2.imread(''mY7ep.jpg'') # make mask and inverted mask for colored areas b,g,r = cv2.split(cv2.blur(image,(5,5))) np.seterr(divide=''ignore'', invalid=''ignore'') # 0/0 --> 0 m = (np.fmin(np.fmin(b, g), r) / np.fmax(np.fmax(b, g), r)) * 255 _,mask_inv = cv2.threshold(np.uint8(m), 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU) mask = cv2.bitwise_not(mask_inv) # local thresholding of grayscale image gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) text = cv2.ximgproc.niBlackThreshold(gray, 255, cv2.THRESH_BINARY, 41, -0.1, binarizationMethod=cv2.ximgproc.BINARIZATION_NICK) # create background (text) and foreground (color markings) bg = cv2.bitwise_and(text, text, mask = mask_inv) fg = cv2.bitwise_and(image, image, mask = mask) out = cv2.add(cv2.cvtColor(bg, cv2.COLOR_GRAY2BGR), fg)

Si no necesita las marcas de color, puede simplemente binarizar la imagen en escala de grises:

image = cv2.imread(''mY7ep.jpg'') gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) text = cv2.ximgproc.niBlackThreshold(gray, 255, cv2.THRESH_BINARY, at_bs, -0.3, binarizationMethod=cv2.ximgproc.BINARIZATION_NICK)


El brillo y el contraste se pueden ajustar utilizando alfa (α) y beta (β), respectivamente. La expresión se puede escribir como

OpenCV ya implementa esto como cv2.convertScaleAbs() por lo que podemos usar esta función con valores alpha y beta definidos por el usuario.

magick image.jpg -colorspace gray -negate -lat 20x20+10% -negate tmp3.png

Pero la pregunta era

¿Cómo obtener una optimización automática de brillo / contraste de una foto en color?

Esencialmente la pregunta es cómo calcular automáticamente alpha y beta . Para ello, podemos mirar el histograma de la imagen. La optimización automática de brillo y contraste calcula alfa y beta para que el rango de salida sea [0...255] . Calculamos la distribución acumulativa para determinar dónde la frecuencia de color es inferior a algún valor de umbral (por ejemplo, 1%) y cortamos los lados derecho e izquierdo del histograma. Esto nos da nuestros rangos mínimos y máximos. Aquí hay una visualización del histograma antes (azul) y después del recorte (naranja).

Para calcular alpha , tomamos el rango de escala de grises mínimo y máximo después del recorte y lo dividimos de nuestro rango de salida deseado de 255

magick tmp3.png /( image.jpg tmp2.png -alpha off -compose copy_opacity -composite /) -compose over -composite result.png

Para calcular el beta, lo insertamos en la fórmula donde g(i, j)=0 y f(i, j)=minimum_gray

import cv2 import numpy as np from matplotlib import pyplot as plt image = cv2.imread(''1.jpg'') alpha = 1.95 # Contrast control (1.0-3.0) beta = 0 # Brightness control (0-100) manual_result = cv2.convertScaleAbs(image, alpha=alpha, beta=beta) cv2.imshow(''original'', image) cv2.imshow(''manual_result'', manual_result) cv2.waitKey()

que después de resolver los resultados en este

α = 255 / (maximum_gray - minimum_gray)

Por tu imagen conseguimos esto.

alfa 3.75

beta -311.25

Es posible que deba ajustar el valor del umbral de recorte para refinar los resultados. Aquí hay algunos resultados de ejemplo utilizando un umbral del 1% con otras imágenes

Código automatizado de brillo y contraste.

g(i,j) = α * f(i,j) + β

Imagen de resultado con este código:

Resultados con otras imágenes utilizando un umbral del 1%.


Este método debería funcionar bien para su aplicación. Primero, encuentra un valor de umbral que separa bien los modos de distribución en el histograma de intensidad y luego vuelve a escalar la intensidad utilizando ese valor.

from skimage.filters import threshold_yen from skimage.exposure import rescale_intensity from skimage.io import imread, imsave img = imread(''mY7ep.jpg'') yen_threshold = threshold_yen(img) bright = rescale_intensity(img, (0, yen_threshold), (0, 255)) imsave(''out.jpg'', bright)

Estoy aquí usando el método de Yen, puedo aprender más sobre este método en esta página .