Улучшите качество подписи, извлеченной с помощью OpenCV из отсканированного листа бумаги

Я извлек подпись из отсканированного листа бумаги. Пользователи знают, что подпись должна быть только на белом листе бумаги.

Обрезку подписи из загруженного изображения я реализовал с помощью Java-библиотеки OpenCV.

Я хочу улучшить качество извлеченной подписи. Я понял, что лучшая версия вместе с исходным файлом — это версия в оттенках серого (grayROI). Например, я бы сделал фон белее.

Не могли бы вы подсказать, как я могу улучшить окончательный вид подписи?

Вверху будет преобразование подписи в PNG с прозрачным фоном.

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.imageio.ImageIO;

import org.apache.commons.io.FilenameUtils;
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.utils.Converters;

import nu.pattern.OpenCV;


    public static synchronized void extractSignature(File signature) {

        if (signature == null)
            return;

        String fileExtension = FilenameUtils.getExtension(signature.getName());

        // check if the file extension is correct!
        if (!fileExtension.toLowerCase().equals("jpg") && !fileExtension.toLowerCase().equals("jpeg") && !fileExtension.toLowerCase().equals("png")) {
            return;
        }
        
        try {
            
            if (!isLoaded) {
                OpenCV.loadLocally();
                isLoaded = true;
            }
            
        } catch (Exception e) {
            System.out.println(e.toString());
        }

        // Load image
        Mat image = Imgcodecs.imread(signature.getPath());
        Mat imageOriginal = image.clone();

        // Convert image to HSV color space
        Mat hsv = new Mat();
        Imgproc.cvtColor(image, hsv, Imgproc.COLOR_BGR2HSV);

        // Define lower and upper bounds for color threshold
        Scalar lower = new Scalar(90, 38, 0);
        Scalar upper = new Scalar(145, 255, 255);

        // Threshold the HSV image to get only desired colors
        Mat mask = new Mat();
        Core.inRange(hsv, lower, upper, mask);

        // Find contours
        List<MatOfPoint> contours = new ArrayList<>();
        Mat hierarchy = new Mat();
        Imgproc.findContours(mask, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);

        // Combine all contours into one and get bounding box
        MatOfPoint allContours = new MatOfPoint();
        for (MatOfPoint contour : contours) {
            List<Point> pts = contour.toList();
            allContours.push_back(new MatOfPoint(Converters.vector_Point_to_Mat(pts)));
        }

        Rect boundingBox = Imgproc.boundingRect(allContours);

        
        // Add 5 pixels to each dimension of the bounding box
        int padding = 10;
        int x = Math.max(boundingBox.x - padding, 0);
        int y = Math.max(boundingBox.y - padding, 0);
        int width = Math.min(boundingBox.width + 2 * padding, image.cols() - x);
        int height = Math.min(boundingBox.height + 2 * padding, image.rows() - y);
        Rect paddedBoundingBox = new Rect(x, y, width, height);

        
        // Extract ROI
        //Mat ROI = new Mat(imageOriginal, boundingBox);
        Mat ROI = new Mat(imageOriginal, paddedBoundingBox);
        
        
        // Convert ROI to grayscale
        Mat grayROI = new Mat();
        Imgproc.cvtColor(ROI, grayROI, Imgproc.COLOR_BGR2GRAY);
        
        // Apply histogram equalization to improve contrast
        Mat equalizedROI = new Mat();
        Imgproc.equalizeHist(grayROI, equalizedROI);
        
        // Apply adaptive thresholding to binarize the image
        Mat binaryROI = new Mat();
        Imgproc.adaptiveThreshold(grayROI, binaryROI, 255, Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C, Imgproc.THRESH_BINARY, 11, 2);

        // Apply morphological transformations to improve the signature appearance
        Mat morphROI = new Mat();
        Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(2, 2));
        Imgproc.morphologyEx(binaryROI, morphROI, Imgproc.MORPH_CLOSE, kernel);
        Imgproc.morphologyEx(morphROI, morphROI, Imgproc.MORPH_OPEN, kernel);


        // Save and display images
        Imgcodecs.imwrite(FileUtils.getSignaturesFolder().getAbsolutePath() + File.separator + signature.getName(), ROI);
        Imgcodecs.imwrite(FileUtils.getSignaturesFolder().getAbsolutePath() + File.separator + "morphROI.jpg", morphROI);
        Imgcodecs.imwrite(FileUtils.getSignaturesFolder().getAbsolutePath() + File.separator + "grayROI.jpg", grayROI);
        Imgcodecs.imwrite(FileUtils.getSignaturesFolder().getAbsolutePath() + File.separator + "binaryROI.jpg", binaryROI);
        Imgcodecs.imwrite(FileUtils.getSignaturesFolder().getAbsolutePath() + File.separator + "equalizedROI.jpg", equalizedROI);
        
        return;
    }

Вот пример из этого исходного изображения:

Я получаю такой результат:

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
0
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

HSV слишком сильно связан с фоном, поэтому это не лучший выбор.

В конце концов я перешел к решению с использованием регионов MSER. Мое окончательное решение, кажется, работает точно для всех моих тестовых случаев.

Вот основные реализованные шаги:

  1. Преобразуйте изображение в серый
  2. Обнаруженные регионы MSER
  3. Рассчитан центроид региона
  4. Добавлены два порога: вертикальный и горизонтальный (для пропуска областей, не связанных с подписью).
  5. Создан ограничивающий прямоугольник со всеми областями, соблюдающими пороговые расстояния от центроида.
  6. Извлекли рентабельность инвестиций с помощью bbox

Он работает со отсканированными документами, в которых необходимо обрезать и сохранить только подпись.

public static synchronized void extractSignature(File signature) {

        if (signature == null)
            return;

        String fileExtension = FilenameUtils.getExtension(signature.getName());

        // check if the file extension is correct!
        if (!fileExtension.toLowerCase().equals("jpg") && !fileExtension.toLowerCase().equals("jpeg") && !fileExtension.toLowerCase().equals("png")) {
            return;
        }

        // Load OpenCV library
        try {

            if (!isLoaded) {
                // System.loadLibrary(Core.NATIVE_LIBRARY_NAME); // not working
                // OpenCV.loadShared(); // not working
                OpenCV.loadLocally();
                isLoaded = true;
            }

        } catch (Exception e) {
            System.out.println(e.toString());
        }

        // Load image from file
        Mat image = Imgcodecs.imread(signature.getPath());
        // Make a copy of the original image
        Mat originalImage = image.clone();

        // Convert the image to grayscale
        Mat gray = new Mat();
        Imgproc.cvtColor(image, gray, Imgproc.COLOR_BGR2GRAY);

        // Create MSER and Detect MSER regions
        MSER mser = MSER.create();
        List<MatOfPoint> regions = new ArrayList<>();
        MatOfRect bboxes = new MatOfRect();
        mser.detectRegions(gray, regions, bboxes);

        // Calculate the Centroid of all MSER regions
        int totalX = 0;
        int totalY = 0;
        int totalPoints = 0;
        for (MatOfPoint region : regions) {
            Point[] points = region.toArray();
            for (Point point : points) {
                totalX += point.x;
                totalY += point.y;
                totalPoints++;
            }
        }

        Point centroid = new Point(totalX / totalPoints, totalY / totalPoints);


        // Draw the Centroid
        Mat centroidM = image.clone();
        Imgproc.circle(centroidM, centroid, 5, new Scalar(255, 0, 0), -1); // Draw the Centroid as a blue dot

        
        // Define vertical and horizontal distance thresholds (adjust as needed)
        double verticalThreshold = 200.0; // Vertical distance threshold
        double horizontalThreshold = 700.0; // Horizontal distance threshold

        // Find the bounding box that covers filtered MSER regions
        int minX = Integer.MAX_VALUE;
        int minY = Integer.MAX_VALUE;
        int maxX = Integer.MIN_VALUE;
        int maxY = Integer.MIN_VALUE;

        for (MatOfPoint region : regions) {
            Point[] points = region.toArray();
            for (Point point : points) {
                double verticalDistance = Math.abs(point.y - centroid.y);
                double horizontalDistance = Math.abs(point.x - centroid.x);

                if (verticalDistance <= verticalThreshold && horizontalDistance <= horizontalThreshold) {
                    if (point.x < minX) minX = (int) point.x;
                    if (point.y < minY) minY = (int) point.y;
                    if (point.x > maxX) maxX = (int) point.x;
                    if (point.y > maxY) maxY = (int) point.y;
                }
            }
        }

        // Draw the bounding box if valid points were found
        if (minX < Integer.MAX_VALUE && minY < Integer.MAX_VALUE && maxX > Integer.MIN_VALUE && maxY > Integer.MIN_VALUE) {
            Imgproc.rectangle(image, new Point(minX, minY), new Point(maxX, maxY), new Scalar(0, 255, 0), 2);
        }

        // Draw convex hulls
        Mat grayImg = image.clone();
        for (MatOfPoint region : regions) {
            MatOfInt hullIndices = new MatOfInt();
            Imgproc.convexHull(region, hullIndices);

            // Convert hull indices to points
            Point[] regionArray = region.toArray();
            List<Point> hullPoints = new ArrayList<>();
            for (int index : hullIndices.toArray()) {
                hullPoints.add(regionArray[index]);
            }
            MatOfPoint hull = new MatOfPoint();
            hull.fromList(hullPoints);

            // Draw the hull on the image
            List<MatOfPoint> hulls = new ArrayList<>();
            hulls.add(hull);
            Imgproc.polylines(grayImg, hulls, true, new Scalar(0, 0, 255), 2);
        }

        // Extract ROI, crop the original image using the bounding box
        Rect ROI = new Rect(minX, minY, maxX - minX, maxY - minY);
        Mat croppedImage = new Mat(originalImage, ROI);

        // Convert ROI to grayscale
        Mat grayROI = new Mat();
        Imgproc.cvtColor(croppedImage, grayROI, Imgproc.COLOR_BGR2GRAY);

        Imgproc.threshold(croppedImage, croppedImage, 150, 255, Imgproc.THRESH_BINARY);

        // Save and display images
        Imgcodecs.imwrite(FileUtils.getSignaturesFolder().getAbsolutePath() + File.separator + "image_with_bbox.jpg", image);
        Imgcodecs.imwrite(FileUtils.getSignaturesFolder().getAbsolutePath() + File.separator + signature.getName(), croppedImage);
        Imgcodecs.imwrite(FileUtils.getSignaturesFolder().getAbsolutePath() + File.separator + "grayROI.jpg", grayROI);
        Imgcodecs.imwrite(FileUtils.getSignaturesFolder().getAbsolutePath() + File.separator + "last.jpg", grayImg);
        Imgcodecs.imwrite(FileUtils.getSignaturesFolder().getAbsolutePath() + File.separator + "centroid.jpg", centroidM);

        return;
    }

Другие вопросы по теме