Обнаружение объектов OpenCV с помощью функции обнаружения и гомографии

Я пытаюсь проверить, если это изображение:

Изображение шаблона

содержится внутри изображений, подобных этому:

Исходное изображение

Я использую обнаружение признаков (SURF) и гомографию, потому что сопоставление шаблонов не является инвариантным к масштабу. К сожалению, все ключевые точки, за исключением нескольких, находятся в неправильных позициях. Должен ли я, возможно, попробовать сопоставление шаблонов, многократно масштабируя изображение? Если да, то как лучше всего попытаться масштабировать изображение?

Код:

import java.util.ArrayList;
import java.util.List;
import org.opencv.calib3d.Calib3d;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.DMatch;
import org.opencv.core.KeyPoint;
import org.opencv.core.Mat;
import org.opencv.core.MatOfByte;
import org.opencv.core.MatOfDMatch;
import org.opencv.core.MatOfKeyPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.features2d.DescriptorMatcher;
import org.opencv.features2d.Features2d;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.xfeatures2d.SURF;
class SURFFLANNMatchingHomography {
    public void run(String[] args) {
        String filenameObject = args.length > 1 ? args[0] : "../data/box.png";
        String filenameScene = args.length > 1 ? args[1] : "../data/box_in_scene.png";
        Mat imgObject = Imgcodecs.imread(filenameObject, Imgcodecs.IMREAD_GRAYSCALE);
        Mat imgScene = Imgcodecs.imread(filenameScene, Imgcodecs.IMREAD_GRAYSCALE);
        if (imgObject.empty() || imgScene.empty()) {
            System.err.println("Cannot read images!");
            System.exit(0);
        }
        //-- Step 1: Detect the keypoints using SURF Detector, compute the descriptors
        double hessianThreshold = 400;
        int nOctaves = 4, nOctaveLayers = 3;
        boolean extended = false, upright = false;
        SURF detector = SURF.create(hessianThreshold, nOctaves, nOctaveLayers, extended, upright);
        MatOfKeyPoint keypointsObject = new MatOfKeyPoint(), keypointsScene = new MatOfKeyPoint();
        Mat descriptorsObject = new Mat(), descriptorsScene = new Mat();
        detector.detectAndCompute(imgObject, new Mat(), keypointsObject, descriptorsObject);
        detector.detectAndCompute(imgScene, new Mat(), keypointsScene, descriptorsScene);
        //-- Step 2: Matching descriptor vectors with a FLANN based matcher
        // Since SURF is a floating-point descriptor NORM_L2 is used
        DescriptorMatcher matcher = DescriptorMatcher.create(DescriptorMatcher.FLANNBASED);
        List<MatOfDMatch> knnMatches = new ArrayList<>();
        matcher.knnMatch(descriptorsObject, descriptorsScene, knnMatches, 2);
        //-- Filter matches using the Lowe's ratio test
        float ratioThresh = 0.75f;
        List<DMatch> listOfGoodMatches = new ArrayList<>();
        for (int i = 0; i < knnMatches.size(); i++) {
            if (knnMatches.get(i).rows() > 1) {
                DMatch[] matches = knnMatches.get(i).toArray();
                if (matches[0].distance < ratioThresh * matches[1].distance) {
                    listOfGoodMatches.add(matches[0]);
                }
            }
        }
        MatOfDMatch goodMatches = new MatOfDMatch();
        goodMatches.fromList(listOfGoodMatches);
        //-- Draw matches
        Mat imgMatches = new Mat();
        Features2d.drawMatches(imgObject, keypointsObject, imgScene, keypointsScene, goodMatches, imgMatches, Scalar.all(-1),
                Scalar.all(-1), new MatOfByte(), Features2d.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS);
        //-- Localize the object
        List<Point> obj = new ArrayList<>();
        List<Point> scene = new ArrayList<>();
        List<KeyPoint> listOfKeypointsObject = keypointsObject.toList();
        List<KeyPoint> listOfKeypointsScene = keypointsScene.toList();
        for (int i = 0; i < listOfGoodMatches.size(); i++) {
            //-- Get the keypoints from the good matches
            obj.add(listOfKeypointsObject.get(listOfGoodMatches.get(i).queryIdx).pt);
            scene.add(listOfKeypointsScene.get(listOfGoodMatches.get(i).trainIdx).pt);
        }
        MatOfPoint2f objMat = new MatOfPoint2f(), sceneMat = new MatOfPoint2f();
        objMat.fromList(obj);
        sceneMat.fromList(scene);
        double ransacReprojThreshold = 3.0;
        Mat H = Calib3d.findHomography( objMat, sceneMat, Calib3d.RANSAC, ransacReprojThreshold );
        //-- Get the corners from the image_1 ( the object to be "detected" )
        Mat objCorners = new Mat(4, 1, CvType.CV_32FC2), sceneCorners = new Mat();
        float[] objCornersData = new float[(int) (objCorners.total() * objCorners.channels())];
        objCorners.get(0, 0, objCornersData);
        objCornersData[0] = 0;
        objCornersData[1] = 0;
        objCornersData[2] = imgObject.cols();
        objCornersData[3] = 0;
        objCornersData[4] = imgObject.cols();
        objCornersData[5] = imgObject.rows();
        objCornersData[6] = 0;
        objCornersData[7] = imgObject.rows();
        objCorners.put(0, 0, objCornersData);
        Core.perspectiveTransform(objCorners, sceneCorners, H);
        float[] sceneCornersData = new float[(int) (sceneCorners.total() * sceneCorners.channels())];
        sceneCorners.get(0, 0, sceneCornersData);
        //-- Draw lines between the corners (the mapped object in the scene - image_2 )
        Imgproc.line(imgMatches, new Point(sceneCornersData[0] + imgObject.cols(), sceneCornersData[1]),
                new Point(sceneCornersData[2] + imgObject.cols(), sceneCornersData[3]), new Scalar(0, 255, 0), 4);
        Imgproc.line(imgMatches, new Point(sceneCornersData[2] + imgObject.cols(), sceneCornersData[3]),
                new Point(sceneCornersData[4] + imgObject.cols(), sceneCornersData[5]), new Scalar(0, 255, 0), 4);
        Imgproc.line(imgMatches, new Point(sceneCornersData[4] + imgObject.cols(), sceneCornersData[5]),
                new Point(sceneCornersData[6] + imgObject.cols(), sceneCornersData[7]), new Scalar(0, 255, 0), 4);
        Imgproc.line(imgMatches, new Point(sceneCornersData[6] + imgObject.cols(), sceneCornersData[7]),
                new Point(sceneCornersData[0] + imgObject.cols(), sceneCornersData[1]), new Scalar(0, 255, 0), 4);
        //-- Show detected matches
        HighGui.imshow("Good Matches & Object detection", imgMatches);
        HighGui.waitKey(0);
        System.exit(0);
    }
}
public class SURFFLANNMatchingHomographyDemo {
    public static void main(String[] args) {
        // Load the native OpenCV library
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
        new SURFFLANNMatchingHomography().run(args);
    }

Итоговое изображение: Результирующее изображение


person butexa    schedule 05.06.2021    source источник
comment
просто используйте matchTemplate. это игровой интерфейс, это 2D-оверлей. он всегда будет иметь один и тот же масштаб. предусмотрите масштаб, соответствующим образом измените размер шаблона   -  person Christoph Rackwitz    schedule 05.06.2021
comment
Хотя я не знаю, как предвидеть масштаб. Любое предложение?   -  person butexa    schedule 05.06.2021
comment
@butexa пробовали ли вы сопоставление шаблонов?   -  person Bilal    schedule 05.06.2021
comment
Однако сопоставление шаблонов @Bilal не является инвариантным к масштабу. Я получаю много неправильных совпадений. Я не смог установить порог для фильтрации ложных срабатываний.   -  person butexa    schedule 05.06.2021


Ответы (2)


Вот возможное решение. Код находится в Python, но операции очень просты, надеюсь, вы сможете перенести его в Java. Я использую сопоставление шаблонов. Суть, я думаю, в том, что я выполняю сопоставление шаблонов с бинарной маской, полученной из компонента Cyan (C) входного изображения. Шаги следующие:

  1. Обрежьте изображение, чтобы избавиться от нежелательных шумов.
  2. Преобразуйте изображение в цветовое пространство CMYK и получите голубой канал
  3. Очистите голубой канал
  4. Читать шаблон
  5. Преобразование шаблона в бинарное изображение
  6. Выполните сопоставление с шаблоном

Давайте посмотрим. Положение шаблона на целевом изображении кажется постоянным, поэтому мы можем обрезать изображение, чтобы избавиться от некоторых частей, в которых, как мы уверены, мы не найдем шаблон. Я обрезал изображение, чтобы удалить часть верхнего и нижнего колонтитула, указав координаты (top left x, top left y, width, height) области интереса (ROI), например:

# imports:
import numpy as np
import cv2

# image path
path = "D://opencvImages//"
fileName = "screen.png"

# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)

# Deep copy for results:
inputImageCopy = inputImage.copy()

# Get image dimensions:
(imageHeight, imageWidth) = inputImage.shape[:2]

# Set the ROI location:
roiX = 0
roiY = 225
roiWidth = imageWidth
roiHeight = 1390

# Crop the ROI:
imageROI = inputImage[roiY:roiHeight,roiX:roiWidth]

# Store a deep copy of this image for results:
imageROIcopy = imageROI.copy()

Вы получите следующее обрезанное изображение:

Вы можете обрезать еще больше, но я не уверен в ваших требованиях. Давайте поработаем с этим и преобразуем новое изображение в цветовое пространство CYMK. Затем извлеките канал Cyan, так как в шаблоне больше всего контента именно на этом канале. В OpenCV нет прямого преобразования в цветовое пространство CYMK, поэтому я применяю напрямую формула преобразования. Мы можем получить каждый компонент цветового пространства из этой формулы, но нас интересует только канал C, для которого требуется предварительное вычисление только канала K (Key). Его можно рассчитать следующим образом:

# Convert the image to float and divide by 255:
floatImage = imageROI.astype(np.float)/255.

# Calculate channel K (Key):
kChannel = 1 - np.max(floatImage, axis=2)

# Calculate  channel C (Cyan):
cChannel = np.where(kChannel < 0.9, (1-floatImage[..., 2] - kChannel)/(1 - kChannel), 0)

# Convert Cyan channel to uint 8:
cChannel = (255*cChannel).astype(np.uint8)

Будьте осторожны с типами данных. Нам нужно работать с float массивами, так что это первое преобразование, которое я выполняю. Получив канал C, мы конвертируем изображение обратно в массив unsigned 8-bit. Это изображение, которое вы получаете для канала C:

Затем получите двоичную маску из этого с помощью порогового значения Otsu:

# Threshold via Otsu:
_, binaryImage = cv2.threshold(cChannel, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

Это маска:

Есть несколько белых зон, которые мы могли бы устранить с помощью флуд- заливка черным цветом. Давайте применим четыре операции заливки к бинарному изображению: верхний левый угол, верхний правый угол, нижний левый угол и нижний правый угол:

# Get the dimensions of the cropped image:
(imageHeight, imageWidth) = binaryImage.shape[:2]

# Apply flood-fill at seed point (0,0) - Top Left:
cv2.floodFill(binaryImage, mask=None, seedPoint=(0, 0), newVal=0)

# Apply flood-fill at seed point (imageWidth - 1, 0) - Top Right:
cv2.floodFill(binaryImage, mask=None, seedPoint=(imageWidth - 1, 0), newVal=0)

# Apply flood-fill at seed point (0, imageHeight - 1) - Bottom Left:
cv2.floodFill(binaryImage, mask=None, seedPoint=(0, imageHeight - 1), newVal=0)

# Apply flood-fill at seed point (imageWidth - 1, imageHeight - 1) - Bottom Right:
cv2.floodFill(binaryImage, mask=None, seedPoint=(imageWidth - 1, imageHeight - 1), newVal=0)

Это результат. Обратите внимание, что фрагмент изображения, который мы ищем, изолирован, и большая часть большого шума исчезла:

Вы, вероятно, могли бы использовать фильтр области, чтобы избавиться от меньших (и больших) пятен шума, но давайте пока остановимся на этом результате. Всё, первая часть готова. Давайте прочитаем шаблон и выполним сопоставление с шаблоном. Теперь у вашего шаблона есть альфа-канал, который здесь бесполезен. Я открыл ваше изображение в GIMP и заменил альфа-канал на обычный белый, вот такой шаблон у меня получился:

Давайте прочитаем это, преобразуем в оттенки серого и выполним пороговое значение Оцу, чтобы получить бинарное изображение:

# Read template:
template = cv2.imread(path+"colorTemplate.png")

# Convert it to grayscale:
template = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)

# Threshold via Otsu:
_, template = cv2.threshold(template, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

Это бинарный шаблон:

Теперь вы можете внедрить здесь механизм прогрессивного масштабирования, чтобы изменить размер этого шаблона на процент масштаба и запустить сопоставление шаблона с целевым изображением пошагово, а затем найти наилучший результат совпадения во всем прогоне и сравнить его с минимальным порогом. Но давайте протестируем шаблон как есть:

# Get template dimensions:
(templateHeight, templateWidth) = template.shape[:2]

# Run Template Matching:
result = cv2.matchTemplate(binaryImage, template, cv2.TM_CCOEFF_NORMED)

# Get Template Matching Results:
(minVal, maxVal, minLoc, maxLoc) = cv2.minMaxLoc(result)

# Get Matching Score:
matchScore = maxVal
print("Match Score: "+str(matchScore))

С этим шаблоном я получаю matchScore из:

Match Score: 0.806335985660553

Выглядит очень приемлемо. Давайте нарисуем красивый прямоугольник в том месте, где было найдено наибольшее количество совпадений, просто чтобы визуализировать результат:

# Set ROI where the largest matching score was found:
matchX = maxLoc[0]
matchY = maxLoc[1]
matchWidth = matchX + templateWidth
matchHeight = matchY + templateHeight

# Draw the ROI on the copy of the cropped BGR image:
cv2.rectangle(imageROIcopy, (matchX, matchY), (matchWidth, matchHeight), (0, 0, 255), 2)
# Show the result:
cv2.imshow("Result (Local)", imageROIcopy)
cv2.waitKey(0)

Это (обрезанный) результат:

Выглядит хорошо. Поскольку мы обрезали изображение для выполнения этой операции, давайте найдем соответствующий ROI на фактическом необрезанном изображении:

# Show result on original image:
matchX = roiX + matchX
matchY = roiY + matchY
matchWidth = matchX + templateWidth
matchHeight = matchY + templateHeight

# Draw the ROI on the copy of the cropped BGR image:
cv2.rectangle(inputImage, (matchX, matchY), (matchWidth, matchHeight), (0, 0, 255), 2)

Кроме того, мы можем нарисовать красивую метку с соответствующей оценкой внутри. Это необязательно, просто чтобы вся информация отображалась на исходном изображении:

# Draw label with match result:
# Round up match score to two significant digits:
matchScore = "{:.2f}".format(matchScore)

# Draw a filled rectangle:
labelOrigin = (matchX-1, matchY - 40)
(labelWidth, labelHeight) = (matchWidth+1, matchY)
cv2.rectangle(inputImage, labelOrigin, (labelWidth, labelHeight), (0, 0, 255), -1)

# Draw the text:
labelOrigin = (matchX-1, matchY - 10)
cv2.putText(inputImage, str(matchScore), labelOrigin, cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 0), 2)

cv2.imshow("Result (Global)", inputImage)
cv2.waitKey(0)

Это (полноразмерный) результат:


Изменить: обработка новых изображений

Я заметил, что ваше новое изображение отличается от исходного изображения. Такое впечатление, что вы снимаете скрины с разных телефонов с разным разрешением экрана. Теперь проблема заключается в том, что шаблон необходимо повторно масштабировать, если вы изменяете размер целевого изображения, иначе шаблон будет слишком маленьким (или слишком большим) для нового сопоставления, что приведет к плохим результатам. Вы можете реализовать механизм повторного масштабирования, о котором я упоминал выше, чтобы увеличить масштаб шаблона, в конечном итоге вы получите достойный результат при определенном масштабированном размере - это существующий вариант.

Параметр другой позволяет повторно масштабировать новое изображение до размера исходного изображения. Ваше исходное изображение имело размер 1125 x 2001 против размера 1600 x 2560. Это важное отличие. Давайте resize сделаем новое изображение такой же ширины, как исходное изображение. Начало кода будет изменено на это:

# image path
path = "D://opencvImages//"
fileName = "newScreen.png"

# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)

# Set the reference width:
referenceWidth = 1125

# Get image dimensions:
(imageHeight, imageWidth) = inputImage.shape[:2]

# Check input width vs reference width:
if imageWidth != referenceWidth:

    # Get original aspect ratio:
    aspectRatio = imageWidth / imageHeight
    # Compute new height using the reference width:
    newHeight = referenceWidth / aspectRatio
    # Set the new dimensions as a tuple:
    dim = (int(referenceWidth), int(newHeight))
    # Resize the image:
    inputImage = cv2.resize(inputImage, dim, interpolation=cv2.INTER_AREA)
    # Get new dimensions for further processing:
    (imageHeight, imageWidth) = inputImage.shape[:2]


# Deep copy for results:
inputImageCopy = inputImage.copy()

# Set the ROI location:
roiX = 0
roiY = 225
roiWidth = imageWidth
roiHeight = 1390

Здесь я устанавливаю опорную ширину в 1125 пикселей, получаю размеры входного изображения через shape и проверяю, отличается ли входная ширина от эталонной. Если это так, я resize изображение в соответствии с эталонной шириной и исходным соотношением сторон. Остальной код не имеет модификаций. Результат на вашем новом изображении будет таким:

person stateMachine    schedule 06.06.2021
comment
Большое спасибо! Я собираюсь проверить это прямо сейчас! Где вы определили maxLoc, потому что я получаю сообщение об ошибке при запуске скрипта. Извините, я не очень хорошо знаком с python. - person butexa; 06.06.2021
comment
@butexa cv2.minMaxLoc возвращает 4 вещи: (minVal, maxVal, minLoc, maxLoc) = cv2.minMaxLoc(result). Минимальное значение совпадения, максимальное значение, местонахождение минимального совпадения и местонахождение максимального совпадения. В Python возвращаемые значения хранятся в кортеже из 4 параметров. Кроме того, местоположения являются координатами (x, y), например, maxLoc также является кортежем из двух значений. Таким образом, значение x будет храниться в maxLoc[0], значение y в maxLoc[1]. - person stateMachine; 06.06.2021
comment
Я получаю cv2.error: OpenCV(4.5.2) /tmp/opencv-20210603-28323-1e2w9q5/opencv-4.5.2/modules/imgproc/src/floodfill.cpp:509: error: (-211:One of the arguments' values is out of range) Seed point is outside of image in function 'floodFill' с этим изображением: i.stack.imgur.com/VyL1L.jpg Как же это возможно? imageHeight был получен из inputImage.shape[:2] . - person butexa; 07.06.2021
comment
@butexa АА, да, чувак, извини, я сделал две ошибки в своем ответе: 1) Порядок возвращаемых параметров из функции shape равен height, width. У меня было наоборот. 2) Я забыл включить строку перед первой операцией flood-fill. Строка получает новую высоту и ширину обрезанного изображения перед заливкой. С тех пор я исправил эти ошибки в ответе и добавил процедуру заливки с четырьмя углами (вместо исходной процедуры с двумя углами). Теперь я заметил пару вещей с вашим новым изображением, подробности смотрите в моем редактировании. - person stateMachine; 08.06.2021

Если вы ищете определенные цвета, вы можете положиться на сегментацию, чтобы быстро найти кандидатов, независимо от размера. Но вам придется добавить некоторую пост-фильтрацию.

введите здесь описание изображения

person Yves Daoust    schedule 05.06.2021
comment
Мне не удалось воспроизвести тот же результат, что и у вас: i.stack.imgur.com/RiIL9 .jpg Вы имеете в виду, что после сегментации все еще используется обнаружение серфинга? Кстати, не могли бы вы прикрепить код, пожалуйста? - person butexa; 05.06.2021
comment
@butexa: зависит от того, как именно вы сравниваете цвета. Ваш подход также кажется работоспособным (три одинаковых капли близко друг к другу). Я использовал проприетарное программное обеспечение. - person Yves Daoust; 05.06.2021