web-dev-qa-db-fra.com

Réglage automatique du contraste et de la luminosité d'une photo couleur d'une feuille de papier avec OpenCV

Lorsque je photographie une feuille de papier (par exemple avec un appareil photo de téléphone), j'obtiens le résultat suivant (image de gauche) (téléchargement jpg ici ). Le résultat souhaité (traité manuellement avec un logiciel de retouche d'image) est à droite:

Je voudrais traiter l'image originale avec openCV pour obtenir un meilleur contraste/luminosité automatiquement (pour que l'arrière-plan soit plus blanc) .

Hypothèse: l'image a un format portrait A4 (nous n'avons pas besoin de la déformer en perspective dans ce sujet ici), et la feuille de papier est blanche avec éventuellement du texte/des images en noir ou en couleurs.

Ce que j'ai essayé jusqu'à présent:

  1. Diverses méthodes de seuillage adaptatif telles que la gaussienne, OTSU (voir OpenCV doc Image Thresholding ). Cela fonctionne généralement bien avec OTSU:

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

    mais cela ne fonctionne que pour les images en niveaux de gris et pas directement pour les images en couleur. De plus, la sortie est binaire (blanc ou noir), ce que je ne veux pas : je préfère garder une image couleur non binaire en sortie

  2. égalisation d'histogramme

    • appliqué sur Y (après RVB => transformation YUV)
    • ou appliqué sur V (après RVB => transformation HSV),

    comme suggéré par cette réponse ( l'égalisation de l'histogramme ne fonctionne pas sur l'image couleur - OpenCV ) ou cette ne ( 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)
    

    ou avec 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)
    

    Malheureusement, le résultat est assez mauvais car il crée localement des micro contrastes horribles (?):

    J'ai également essayé YCbCr à la place, et c'était similaire.

  3. J'ai également essayé CLAHE (égalisation d'histogramme adaptatif limité par contraste) avec divers tileGridSize de 1 À 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)
    

    mais le résultat était tout aussi horrible aussi.

  4. Faire cette méthode CLAHE avec l'espace colorimétrique LAB, comme suggéré dans la question Comment appliquer CLAHE sur des images couleur RVB :

    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)
    

    a donné un mauvais résultat aussi. Image de sortie:

  5. Faire un seuillage adaptatif ou une égalisation d'histogramme séparément sur chaque canal (R, G, B) n'est pas une option car cela dérangerait la balance des couleurs, comme expliqué ici .

  6. Méthode "Stretch de contraste" du didacticiel de scikit-image Sur égalisation d'histogramme :

    l'image est redimensionnée pour inclure toutes les intensités qui se situent dans les 2e et 98e centiles

    est un peu mieux, mais encore loin du résultat souhaité (voir image en haut de cette question).


TL; DR: comment obtenir une optimisation automatique de la luminosité/contraste d'une photo couleur d'une feuille de papier avec OpenCV/Python? Quel type de seuillage/l'égalisation d'histogramme/une autre technique pourrait-elle être utilisée?

46
Basj

enter image description here

Cette méthode devrait bien fonctionner pour votre application. Vous trouvez d'abord une valeur de seuil qui sépare bien les modes de distribution dans l'histogramme d'intensité, puis redimensionnez l'intensité en utilisant cette valeur.

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)

Je suis ici en utilisant la méthode de Yen, peut en savoir plus sur cette méthode sur cette page .

11
fireant

Binarisation souple robuste adaptable localement! Voilà comment je l'appelle.

J'ai déjà fait des trucs similaires auparavant, dans un but un peu différent, donc cela peut ne pas convenir parfaitement à vos besoins, mais j'espère que cela aide (j'ai également écrit ce code la nuit pour un usage personnel, donc c'est moche). Dans un sens, ce code était destiné à résoudre un cas plus général par rapport au vôtre, où nous pouvons avoir beaucoup de bruit structuré en arrière-plan (voir démo ci-dessous).

Qu'est-ce que ce code fait? Étant donné une photo d'une feuille de papier, il la blanchira afin qu'elle puisse être parfaitement imprimable. Voir des exemples d'images ci-dessous.

Teaser: voilà à quoi ressembleront vos pages après cet algorithme (avant et après). Notez que même les annotations du marqueur de couleur ont disparu, donc je ne sais pas si cela conviendra à votre cas d'utilisation, mais le code pourrait être utile:

Pour obtenir des résultats parfaitement propres , vous devrez peut-être jouer un peu avec les paramètres de filtrage, mais comme vous pouvez le voir, même avec les paramètres par défaut, cela fonctionne assez bien. bien.


Étape 0: Coupez les images pour qu'elles correspondent étroitement à la page

Supposons que vous ayez fait cette étape (cela semble être le cas dans les exemples que vous avez fournis). Si vous avez besoin d'un outil manuel d'annotation et de retorsion, envoyez-moi un message! ^^ Les résultats de cette étape sont ci-dessous (les exemples que j'utilise ici sont sans doute plus difficiles que celui que vous avez fourni, même si cela ne correspond pas exactement à votre cas):

De cela, nous pouvons immédiatement voir les problèmes suivants:

  • La condition d'éclaircissement n'est pas uniforme. Cela signifie que toutes les méthodes de binarisation simples ne fonctionneront pas. J'ai essayé beaucoup de solutions disponibles dans OpenCV, ainsi que leurs combinaisons, aucune d'entre elles n'a fonctionné!
  • Beaucoup de bruit de fond. Dans mon cas, je devais retirer la grille du papier, ainsi que l'encre de l'autre côté du papier qui est visible à travers la fine feuille.

Étape 1: correction gamma

Le raisonnement de cette étape est d'équilibrer le contraste de l'image entière (car votre image peut être légèrement surexposée/sous-exposée en fonction des conditions d'éclairage).

Cela peut sembler à première vue comme une étape inutile, mais son importance ne peut pas être sous-estimée: dans un sens, cela normalise les images aux distributions similaires d'expositions, afin que vous puissiez choisir des hyper-paramètres significatifs plus tard (par exemple, le DELTA paramètre dans la section suivante, les paramètres de filtrage du bruit, les paramètres des éléments morphologiques, etc.)

# 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)

Voici les résultats de l'ajustement gamma:

Vous pouvez voir que c'est un peu plus ... "équilibré" maintenant. Sans cette étape, tous les paramètres que vous choisirez à la main dans les étapes ultérieures deviendront moins robustes!


Étape 2: Binarisation adaptative pour détecter les taches de texte

Dans cette étape, nous allons binariser de manière adaptative les taches de texte. J'ajouterai plus de commentaires plus tard, mais l'idée est essentiellement la suivante:

  • Nous divisons l'image en blocs de taille BLOCK_SIZE. L'astuce consiste à choisir sa taille suffisamment grande pour que vous obteniez toujours un gros morceau de texte et d'arrière-plan (c'est-à-dire plus grand que tous les symboles que vous avez), mais suffisamment petit pour ne pas souffrir de variations de conditions d'éclaircissement (c'est-à-dire "grand, mais toujours local").
  • À l'intérieur de chaque bloc, nous faisons une binarisation adaptative localement: nous regardons la valeur médiane et supposons que c'est l'arrière-plan (parce que nous avons choisi le BLOCK_SIZE Suffisamment grand pour que la majorité soit l'arrière-plan). Ensuite, nous définissons plus en détail DELTA - fondamentalement juste un seuil de "à quelle distance de la médiane nous le considérerons toujours comme arrière-plan?".

Ainsi, la fonction process_image Fait le travail. De plus, vous pouvez modifier les fonctions preprocess et postprocess pour répondre à vos besoins (cependant, comme vous pouvez le voir dans l'exemple ci-dessus, l'algorithme est assez robuste , c'est-à-dire que cela fonctionne assez bien hors de la boîte sans trop modifier les paramètres).

Le code de cette partie suppose que le premier plan est plus sombre que l'arrière-plan (c'est-à-dire l'encre sur papier). Mais vous pouvez facilement changer cela en modifiant la fonction preprocess: au lieu de 255 - image, Retournez juste image.

# These are probably the only important parameters in the
# whole pipeline (steps 0 through 3).
BLOCK_SIZE = 40
DELTA = 25

# Do the necessary noise cleaning and other stuffs.
# I just do a simple blurring here but you can optionally
# add more stuffs.
def preprocess(image):
    image = cv2.medianBlur(image, 3)
    return 255 - image

# Again, this step is fully optional and you can even keep
# the body empty. I just did some opening. The algorithm is
# pretty robust, so this stuff won't affect much.
def postprocess(image):
    kernel = np.ones((3,3), np.uint8)
    image = cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel)
    return image

# Just a helper function that generates box coordinates
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)

# Here is where the trick begins. We perform binarization from the 
# median value locally (the img_in is actually a slice of the image). 
# Here, following assumptions are held:
#   1.  The majority of pixels in the slice is background
#   2.  The median value of the intensity histogram probably
#       belongs to the background. We allow a soft margin DELTA
#       to account for any irregularities.
#   3.  We need to keep everything other than the background.
#
# We also do simple morphological operations here. It was just
# something that I empirically found to be "useful", but I assume
# this is pretty robust across different datasets.
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

# This function just divides the image into local regions (blocks),
# and perform the `adaptive_mean_threshold(...)` function to each
# of the regions.
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

# This function invokes the whole pipeline of Step 2.
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

Les résultats sont de jolis blobs comme celui-ci, qui suivent de près la trace d'encre:


Étape 3: La partie "douce" de la binarisation

Ayant les taches qui recouvrent les symboles et un peu plus, nous pouvons enfin faire la procédure de blanchiment.

Si nous regardons de plus près les photos de feuilles de papier avec du texte (en particulier celles qui ont des écritures à la main), la transformation de "l'arrière-plan" (papier blanc) en "premier plan" (l'encre de couleur foncée) n'est pas nette, mais très progressive . D'autres réponses basées sur la binarisation dans cette section proposent un seuillage simple (même si elles sont adaptatives localement, c'est toujours un seuil), qui fonctionne bien pour le texte imprimé, mais produira des résultats pas si jolis avec les écritures à la main.

Donc, la motivation de cette section est que nous voulons préserver cet effet de transmission progressive du noir au blanc, tout comme des photos naturelles de feuilles de papier avec encre naturelle. Le but final pour cela est de le rendre imprimable.

L'idée principale est simple: plus la valeur en pixels (après le seuil ci-dessus) diffère de la valeur min locale, plus elle est susceptible d'appartenir à l'arrière-plan. Nous pouvons l'exprimer en utilisant une famille de fonctions Sigmoid , redimensionnées à la plage du bloc local (de sorte que cette fonction soit mise à l'échelle de manière adaptative à travers l'image).

# This is the function used for composing
def sigmoid(x, orig, rad):
    k = np.exp((x - orig) * 5 / rad)
    return k / (k + 1.)

# Here, we combine the local blocks. A bit lengthy, so please
# follow the local comments.
def combine_block(img_in, mask):
    # First, we pre-fill the masked region of img_out to white
    # (i.e. background). The mask is retrieved from previous section.
    img_out = np.zeros_like(img_in)
    img_out[mask == 255] = 255
    fimg_in = img_in.astype(np.float32)

    # Then, we store the foreground (letters written with ink)
    # in the `idx` array. If there are none (i.e. just background),
    # we move on to the next block.
    idx = np.where(mask == 0)
    if idx[0].shape[0] == 0:
        img_out[idx] = img_in[idx]
        return img_out

    # We find the intensity range of our pixels in this local part
    # and clip the image block to that range, locally.
    lo = fimg_in[idx].min()
    hi = fimg_in[idx].max()
    v = fimg_in[idx] - lo
    r = hi - lo

    # Now we use good old OTSU binarization to get a rough estimation
    # of foreground and background regions.
    img_in_idx = img_in[idx]
    ret3,th3 = cv2.threshold(img_in[idx],0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)

    # Then we normalize the stuffs and apply sigmoid to gradually
    # combine the stuffs.
    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)

    # Finally, we re-normalize the result to the range [0..255]
    img_out[idx] = (255. * f).astype(np.uint8)
    return img_out

# We do the combination routine on local blocks, so that the scaling
# parameters of Sigmoid function can be adjusted to local setting
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

# Postprocessing (should be robust even without it, but I recommend
# you to play around a bit and find what works best for your data.
# I just left it blank.
def combine_postprocess(image):
    return image

# The main function of this section. Executes the whole pipeline.
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

Certains éléments sont commentés car ils sont facultatifs. La fonction combine_process Reprend le masque de l'étape précédente et exécute l'ensemble du pipeline de composition. Vous pouvez essayer de jouer avec eux pour vos données spécifiques (images). Les résultats sont nets:

J'ajouterai probablement plus de commentaires et d'explications au code dans cette réponse. Va télécharger le tout (avec le code de recadrage et de déformation) sur Github.

21
FalconUA

enter image description hereenter image description here

La luminosité et le contraste peuvent être ajustés en utilisant respectivement alpha (α) et bêta (β). L'expression peut s'écrire

enter image description here

OpenCV implémente déjà cela en tant que cv2.convertScaleAbs() afin que nous puissions simplement utiliser cette fonction avec des valeurs définies par l'utilisateur alpha et beta.

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()

Mais la question était

Comment obtenir une optimisation automatique de la luminosité/contraste d'une photo couleur?

Essentiellement, la question est de savoir comment calculer automatiquement alpha et beta. Pour ce faire, nous pouvons regarder l'histogramme de l'image. L'optimisation automatique de la luminosité et du contraste calcule l'alpha et le bêta afin que la plage de sortie soit [0...255]. Nous calculons la distribution cumulative pour déterminer où la fréquence des couleurs est inférieure à une valeur seuil (disons 1%) et coupons les côtés droit et gauche de l'histogramme. Cela nous donne nos gammes minimum et maximum. Voici une visualisation de l'histogramme avant (bleu) et après écrêtage (orange). Remarquez comment les sections les plus "intéressantes" de l'image sont plus prononcées après le découpage.

Pour calculer alpha, nous prenons la plage de niveaux de gris minimum et maximum après le découpage et la divisons de notre plage de sortie souhaitée de 255

α = 255 / (maximum_gray - minimum_gray)

Pour calculer la version bêta, nous le connectons à la formule où g(i, j)=0 et f(i, j)=minimum_gray

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

qui après avoir résolu les résultats dans ce

β = -minimum_gray * α

Pour votre image, nous obtenons ceci

Alpha: 3,75

Bêta: -311,25

Vous devrez peut-être ajuster la valeur du seuil d'écrêtage pour affiner les résultats. Voici quelques exemples de résultats utilisant un seuil de 1% avec d'autres images

Code de luminosité et de contraste automatisé

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()

Image de résultat avec ce code:

enter image description here

Résultats avec d'autres images en utilisant un seuil de 1%

enter image description hereenter image description here

enter image description hereenter image description here

Une autre version consiste à ajouter un biais et un gain à une image en utilisant des arithmétiques de saturation au lieu d'utiliser cv2.convertScaleAbs D'OpenCV. La méthode intégrée ne prend pas de valeur absolue, ce qui conduirait à des résultats absurdes (par exemple, un pixel à 44 avec alpha = 3 et bêta = -210 devient 78 avec OpenCV, alors qu'en fait il devrait devenir 0).

import cv2
import numpy as np
# from matplotlib import pyplot as plt

def convertScale(img, alpha, beta):
    """Add bias and gain to an image with saturation arithmetics. Unlike
    cv2.convertScaleAbs, it does not take an absolute value, which would lead to
    nonsensical results (e.g., a pixel at 44 with alpha = 3 and beta = -210
    becomes 78 with OpenCV, when in fact it should become 0).
    """

    new_img = img * alpha + beta
    new_img[new_img < 0] = 0
    new_img[new_img > 255] = 255
    return new_img.astype(np.uint8)

# Automatic brightness and contrast optimization with optional histogram clipping
def automatic_brightness_and_contrast(image, clip_hist_percent=25):
    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 = convertScale(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.imwrite('auto_result.png', auto_result)
cv2.imshow('image', image)
cv2.waitKey()
19
nathancy

Je pense que la façon de le faire est 1) Extraire le canal de chrominance (saturation) de l'espace colorimétrique HCL. (HCL fonctionne mieux que HSL ou HSV). Seules les couleurs doivent avoir une saturation non nulle, de sorte que les nuances lumineuses et grises seront sombres. 2) Seuil résultant de l'utilisation du seuillage otsu à utiliser comme masque. 3) Convertissez votre entrée en niveaux de gris et appliquez un seuil local (c'est-à-dire adaptatif). 4) placez le masque dans le canal alpha de l'original, puis composez le résultat seuillé de la zone locale avec l'original, de sorte qu'il conserve la zone colorée de l'original et utilise partout ailleurs le résultat seuillé de la zone locale.

Désolé, je ne connais pas très bien OpeCV, mais voici les étapes d'utilisation d'ImageMagick.

Notez que les canaux sont numérotés en commençant par 0. (H = 0 ou rouge, C = 1 ou vert, L = 2 ou bleu)

Contribution:

enter image description here

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


enter image description here

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


enter image description here

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


enter image description here

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


enter image description here

UNE ADDITION:

Voici Python Code Wand, qui produit le même résultat de sortie. Il a besoin d'Imagemagick 7 et Wand 0.5.5.

#!/bin/python3.7

from wand.image import Image
from wand.display import display
from wand.version import QUANTUM_RANGE

with Image(filename='text.jpg') as img:
    with img.clone() as copied:
        with img.clone() as hcl:
            hcl.transform_colorspace('hcl')
            with hcl.channel_images['green'] as mask:
                mask.auto_threshold(method='otsu')
                copied.composite(mask, left=0, top=0, operator='copy_alpha')
                img.transform_colorspace('gray')
                img.negate()
                img.adaptive_threshold(width=20, height=20, offset=0.1*QUANTUM_RANGE)
                img.negate()
                img.composite(copied, left=0, top=0, operator='over')
                img.save(filename='text_process.jpg')
10
fmw42

Nous séparons d'abord le texte et les marques de couleur. Cela peut être fait dans un espace colorimétrique avec un canal de saturation des couleurs. J'ai utilisé à la place une méthode très simple inspirée de cet article : la ration min (R, G, B)/max (R, G, B) sera proche de 1 pour les zones grises (claires) et << 1 pour les zones colorées. Pour les zones gris foncé, nous obtenons quelque chose entre 0 et 1, mais cela n'a pas d'importance: soit ces zones vont au masque de couleur et sont ensuite ajoutées telles quelles, soit elles ne sont pas incluses dans le masque et contribuent à la sortie du binarisé texte. Pour le noir, nous utilisons le fait que 0/0 devient 0 lors de la conversion en uint8.

Le texte de l'image en niveaux de gris est seuillé localement pour produire une image en noir et blanc. Vous pouvez choisir votre technique préférée parmi cette comparaison ou cette enquête . J'ai choisi la technique NICK qui s'adapte bien avec un faible contraste et est plutôt robuste, c'est-à-dire que le choix du paramètre k entre environ -0,3 et -0,1 fonctionne bien pour une très large gamme de conditions, ce qui est bon pour le traitement automatique . Pour l'exemple de document fourni, la technique choisie ne joue pas un grand rôle car elle est éclairée de manière relativement uniforme, mais pour faire face aux images non uniformément éclairées, elle doit être un local technique de seuillage.

À l'étape finale, les zones de couleur sont ajoutées à l'image de texte binarisée.

Cette solution est donc très similaire à la solution de @ fmw42 (tout le mérite de l'idée pour lui) à l'exception des différentes méthodes de détection des couleurs et de binarisation.

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) 

enter image description here

Si vous n'avez pas besoin des repères de couleur, vous pouvez simplement binariser l'image en niveaux de gris:

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)

enter image description here

6
Stef