9 votes

Utilisation de .Net pour redresser une image

J'ai cherché partout un moyen fiable de redresser une image en .Net, mais je n'ai pas eu beaucoup de chance.

Pour l'instant, j'utilise Aforge. C'est un problème car je travaille avec WPF, donc les images avec lesquelles je travaille sont des objets BitmapImage, par opposition aux objets Bitmap, ce qui signifie que je dois commencer avec un objet BitmapImage, l'enregistrer dans un flux de mémoire, créer un nouvel objet Bitmap à partir du flux de mémoire, passer par le processus d'élimination de la distorsion, enregistrer l'image modifiée dans un nouveau flux de mémoire et ensuite créer un nouvel objet BitmapImage à partir dudit flux de mémoire. En plus de cela, le redressement n'est pas génial.

J'essaie de lire les données OMR d'un morceau de papier numérisé par un scanner, et je dois donc compter sur le fait qu'une case OMR particulière se trouve toujours aux mêmes coordonnées, de sorte que le désalignement doit être fiable.

J'utilise donc Aforge pour le moment, je n'arrive pas à trouver d'autres bibliothèques gratuites/open source pour le désalignement d'images en .Net, tout ce que j'ai trouvé est soit très cher, soit en C/C++.

Ma question est la suivante : existe-t-il d'autres bibliothèques gratuites/open source qui aident à la correction des images dans .Net ? Si oui, comment s'appellent-elles ? Sinon, comment dois-je aborder ce problème ?

Edit : Par exemple, disons que j'ai la page ci-dessous :

Initial Image

Note : Ceci n'est qu'à titre d'illustration, mais l'image réelle a effectivement un rectangle noir à chaque coin de la page, peut-être que cela vous aidera.

Lorsque j'imprime ce document et que je le numérise dans mon scanner, il ressemble à ceci :

Scanned Image

Je dois redresser cette image pour que ma boîte soit à la même place à chaque fois. Dans le monde réel, il y a beaucoup de boîtes, elles sont plus petites et proches les unes des autres, donc la précision est importante.

La méthode que j'utilise actuellement pour cela est un casse-tête inefficace :

using AForge.Imaging;
using AForge.Imaging.Filters;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Windows.Media.Imaging;

public static BitmapImage DeskewBitmap(BitmapImage skewedBitmap)
{
    //Using a memory stream to minimise disk IO
    var memoryStream = BitmapImageToMemoryStream(skewedBitmap);

    var bitmap = MemoryStreamToBitmap(memoryStream);
    var skewAngle = CalculateSkewAngle(bitmap);

    //Aforge needs a Bppp indexed image for the deskewing process
    var bitmapConvertedToBbppIndexed = ConvertBitmapToBbppIndexed(bitmap);

    var rotatedImage = DeskewBitmap(skewAngle, bitmapConvertedToBbppIndexed);

    //I need to convert the image back to a non indexed format to put it back into a BitmapImage object
    var imageConvertedToNonIndexed = ConvertImageToNonIndexed(rotatedImage);

    var imageAsMemoryStream = BitmapToMemoryStream(imageConvertedToNonIndexed);
    var memoryStreamAsBitmapImage = MemoryStreamToBitmapImage(imageAsMemoryStream);

    return memoryStreamAsBitmapImage;
}

private static Bitmap ConvertImageToNonIndexed(Bitmap rotatedImage)
{
    var imageConvertedToNonIndexed = rotatedImage.Clone(
        new Rectangle(0, 0, rotatedImage.Width, rotatedImage.Height), PixelFormat.Format32bppArgb);
    return imageConvertedToNonIndexed;
}

private static Bitmap DeskewBitmap(double skewAngle, Bitmap bitmapConvertedToBbppIndexed)
{
    var rotationFilter = new RotateBilinear(-skewAngle) { FillColor = Color.White };

    var rotatedImage = rotationFilter.Apply(bitmapConvertedToBbppIndexed);
    return rotatedImage;
}

private static double CalculateSkewAngle(Bitmap bitmapConvertedToBbppIndexed)
{
    var documentSkewChecker = new DocumentSkewChecker();

    double skewAngle = documentSkewChecker.GetSkewAngle(bitmapConvertedToBbppIndexed);

    return skewAngle;
}

private static Bitmap ConvertBitmapToBbppIndexed(Bitmap bitmap)
{
    var bitmapConvertedToBbppIndexed = bitmap.Clone(
        new Rectangle(0, 0, bitmap.Width, bitmap.Height), PixelFormat.Format8bppIndexed);
    return bitmapConvertedToBbppIndexed;
}

private static BitmapImage ResizeBitmap(BitmapImage originalBitmap, int desiredWidth, int desiredHeight)
{
    var ms = BitmapImageToMemoryStream(originalBitmap);
    ms.Position = 0;

    var result = new BitmapImage();
    result.BeginInit();
    result.DecodePixelHeight = desiredHeight;
    result.DecodePixelWidth = desiredWidth;

    result.StreamSource = ms;
    result.CacheOption = BitmapCacheOption.OnLoad;

    result.EndInit();
    result.Freeze();

    return result;
}

private static MemoryStream BitmapImageToMemoryStream(BitmapImage image)
{
    var ms = new MemoryStream();

    var encoder = new JpegBitmapEncoder();
    encoder.Frames.Add(BitmapFrame.Create(image));

    encoder.Save(ms);

    return ms;
}

private static BitmapImage MemoryStreamToBitmapImage(MemoryStream ms)
{
    ms.Position = 0;
    var bitmap = new BitmapImage();

    bitmap.BeginInit();

    bitmap.StreamSource = ms;
    bitmap.CacheOption = BitmapCacheOption.OnLoad;

    bitmap.EndInit();
    bitmap.Freeze();

    return bitmap;
}

private static Bitmap MemoryStreamToBitmap(MemoryStream ms)
{
    return new Bitmap(ms);
}

private static MemoryStream BitmapToMemoryStream(Bitmap image)
{
    var memoryStream = new MemoryStream();
    image.Save(memoryStream, ImageFormat.Bmp);

    return memoryStream;
}

En rétrospective, quelques autres questions :

  1. Est-ce que j'utilise AForge correctement ?
  2. AForge est-elle la meilleure bibliothèque à utiliser pour cette tâche ?
  3. Comment pourrais-je améliorer mon approche actuelle pour obtenir des résultats plus précis ?

7voto

mmgp Points 9153

Compte tenu de l'exemple fourni, il est clair que vous ne cherchez pas à redresser l'image. Ce type d'opération ne va pas corriger la distorsion que vous avez, vous devez plutôt effectuer une transformation de perspective. Ceci peut être clairement vu dans la figure suivante. Les quatre rectangles blancs représentent les bords de vos quatre boîtes noires, les lignes jaunes sont le résultat de la connexion des boîtes noires. Le quadrilatère jaune n'est pas un quadrilatère rouge oblique (celui que vous voulez obtenir).

enter image description here

Donc, si vous pouvez effectivement obtenir la figure ci-dessus, le problème devient beaucoup plus simple. Si vous ne disposiez pas des quatre boîtes d'angle, vous auriez besoin de quatre autres points de référence, qui vous sont donc d'une grande aide. Une fois que vous avez obtenu l'image ci-dessus, vous connaissez les quatre coins jaunes, et il vous suffit de les faire correspondre aux quatre coins rouges. C'est la transformation de perspective que vous devez faire, et selon votre bibliothèque, il pourrait y avoir une fonction prête à l'emploi pour cela (il y en a une au moins, vérifiez les commentaires à votre question).

Il existe plusieurs façons d'arriver à l'image ci-dessus, je vais donc me contenter d'en décrire une relativement simple. Tout d'abord, binarisez votre image en niveaux de gris. Pour ce faire, j'ai choisi un seuil global simple de 100 (votre image est dans la plage [0, 255]), ce qui permet de conserver les boîtes et les autres détails de l'image (comme les lignes fortes autour de l'image). Les intensités supérieures ou égales à 100 sont réglées sur 255, et les intensités inférieures à 100 sont réglées sur 0. Mais, comme il s'agit d'une image imprimée, l'intensité des boîtes est très susceptible de varier. Vous aurez donc besoin d'une meilleure méthode ici, quelque chose d'aussi simple qu'un gradient morphologique pourrait fonctionner potentiellement mieux. La deuxième étape consiste à éliminer les détails non pertinents. Pour ce faire, effectuez une fermeture morphologique avec un carré de 7x7 (environ 1% du minimum entre la largeur et la hauteur de l'image d'entrée). Pour obtenir le bord des cases, utilisez une érosion morphologique comme dans current_image - erosion(current_image) en utilisant un carré élémentaire de 3x3. Maintenant, vous avez une image avec les quatre contours blancs comme ci-dessus (ceci en supposant que tout sauf les cases ont été éliminées, une simplification des autres entrées je crois). Pour obtenir les pixels de ces contours blancs, vous pouvez faire un étiquetage en composantes connectées. Avec ces 4 composantes, déterminez la composante supérieure droite, la composante supérieure gauche, la composante inférieure droite et la composante inférieure gauche. Maintenant vous pouvez facilement trouver les points nécessaires pour obtenir les coins du rectangle jaune. Toutes ces opérations sont facilement disponibles dans AForge, il suffit donc de traduire le code suivant en C# :

import sys
import numpy
from PIL import Image, ImageOps, ImageDraw
from scipy.ndimage import morphology, label

# Read input image and convert to grayscale (if it is not yet).
orig = Image.open(sys.argv[1])
img = ImageOps.grayscale(orig)

# Convert PIL image to numpy array (minor implementation detail).
im = numpy.array(img)

# Binarize.
im[im < 100] = 0
im[im >= 100] = 255

# Eliminate undesidered details.
im = morphology.grey_closing(im, (7, 7))

# Border of boxes.
im = im - morphology.grey_erosion(im, (3, 3))

# Find the boxes by labeling them as connected components.
lbl, amount = label(im)
box = []
for i in range(1, amount + 1):
    py, px = numpy.nonzero(lbl == i) # Points in this connected component.
    # Corners of the boxes.
    box.append((px.min(), px.max(), py.min(), py.max()))
box = sorted(box)
# Now the first two elements in the box list contains the
# two left-most boxes, and the other two are the right-most
# boxes. It remains to stablish which ones are at top,
# and which at bottom.
top = []
bottom = []
for index in [0, 2]:
    if box[index][2] > box[index+1][2]:
        top.append(box[index + 1])
        bottom.append(box[index])
    else:
        top.append(box[index])
        bottom.append(box[index + 1])

# Pick the top left corner, top right corner,
# bottom right corner, and bottom left corner.
reference_corners = [
        (top[0][0], top[0][2]), (top[1][1], top[1][2]),
        (bottom[1][1], bottom[1][3]), (bottom[0][0], bottom[0][3])]

# Convert the image back to PIL (minor implementation detail).
img = Image.fromarray(im)
# Draw lines connecting the reference_corners for visualization purposes.
visual = img.convert('RGB')
draw = ImageDraw.Draw(visual)
draw.line(reference_corners + [reference_corners[0]], fill='yellow')
visual.save(sys.argv[2])

# Map the current quadrilateral to an axis-aligned rectangle.
min_x = min(x for x, y in reference_corners)
max_x = max(x for x, y in reference_corners)
min_y = min(y for x, y in reference_corners)
max_y = max(y for x, y in reference_corners)

# The red rectangle.
perfect_rect = [(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)]

# Use these points to do the perspective transform.
print reference_corners
print perfect_rect

Le résultat final du code ci-dessus avec votre image d'entrée est :

[(55, 30), (734, 26), (747, 1045), (41, 1036)]
[(41, 26), (747, 26), (747, 1045), (41, 1045)]

La première liste de points décrit les quatre coins du rectangle jaune, et la seconde est liée au rectangle rouge. Pour effectuer la transformation en perspective, vous pouvez utiliser AForge avec la fonction ready. J'ai utilisé ImageMagick pour plus de simplicité comme dans :

convert input.png -distort Perspective "55,30,41,26 734,26,747,26 747,1045,747,1045 41,1036,41,1045" result.png

Ce qui donne l'alignement que vous recherchez (avec des lignes bleues retrouvées comme précédemment pour mieux montrer le résultat) :

enter image description here

Vous pouvez remarquer que la ligne bleue verticale de gauche n'est pas complètement droite, en fait les deux cases les plus à gauche sont désalignées d'un pixel sur l'axe des x. Ceci peut être corrigé par une interpolation différente utilisée lors de la transformation en perspective.

1voto

Shahin Dohan Points 743

Pour ceux qui arrivent ici à partir de Google, le désalignement peut être réalisé facilement en utilisant la fonction Magick.NET bibliothèque.

Installez l'un des paquets nuget, par exemple Magick.NET-Q16-AnyCPU .

Utilisation :

private byte[] DeskewImage(byte[] imageFileBytes)
{
    var img = new MagickImage(imageFileBytes);
    // ImageMagick docs say 40% should work for most images
    img.Deskew(new Percentage(40d));
    return img.ToByteArray();
}

1voto

DermFrench Points 879

John, la bibliothèque Leptonica est censée être très rapide et stable.
Voici un lien sur la manière de l'appeler à partir de c# http://www.leptonica.com/vs2008doc/csharp-and-leptonlib.html . Je ne suis pas sûr que ce soit la réponse, je l'ai donc ajoutée en commentaire.

Il dispose d'une fonction LeptonicaCLR.Utils.DeskewBinaryImage() pour redresser réellement une image en noir et blanc.

Je ne suis pas sûr qu'il serait efficace avec les formulaires que vous essayez de traiter.

1voto

DermFrench Points 879

John, Je pense aussi que la correspondance des modèles pourrait aider à résoudre ce problème (si la bibliothèque Leptonica n'est pas assez bonne).

Aforge.net a intégré la correspondance des modèles : http://www.aforgenet.com/framework/docs/html/17494328-ef0c-dc83-1bc3-907b7b75039f.htm

D'après mes connaissances limitées en la matière, vous devez disposer d'une image source de la marque de découpe/enregistrement et la trouver en utilisant la correspondance des modèles dans l'image numérisée. Vous pourriez ensuite recadrer votre image pour obtenir une sous-image de la partie située à l'intérieur des marques d'enregistrement. Pour l'image que vous avez fournie ci-dessus, je pense que vous pouvez supposer un biais initial assez faible et n'effectuer la comparaison de modèles que sur une zone recadrée de l'image pour réduire le temps total.

Il y a une discussion à ce sujet ici : Comment localiser les marques d'alignement dans une image

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X