OpenCV C ++ / Obj-C: detección de una hoja de papel / detección cuadrada

178

Implementé con éxito el ejemplo de detección cuadrada de OpenCV en mi aplicación de prueba, pero ahora necesito filtrar la salida, porque es bastante desordenado, ¿o está mal mi código?

Estoy interesado en las cuatro esquinas del papel para la reducción del sesgo (como que ) y su posterior procesamiento ...

De entrada y salida: De entrada y salida

Imagen original:

hacer clic

Código:

double angle( cv::Point pt1, cv::Point pt2, cv::Point pt0 ) {
    double dx1 = pt1.x - pt0.x;
    double dy1 = pt1.y - pt0.y;
    double dx2 = pt2.x - pt0.x;
    double dy2 = pt2.y - pt0.y;
    return (dx1*dx2 + dy1*dy2)/sqrt((dx1*dx1 + dy1*dy1)*(dx2*dx2 + dy2*dy2) + 1e-10);
}

- (std::vector<std::vector<cv::Point> >)findSquaresInImage:(cv::Mat)_image
{
    std::vector<std::vector<cv::Point> > squares;
    cv::Mat pyr, timg, gray0(_image.size(), CV_8U), gray;
    int thresh = 50, N = 11;
    cv::pyrDown(_image, pyr, cv::Size(_image.cols/2, _image.rows/2));
    cv::pyrUp(pyr, timg, _image.size());
    std::vector<std::vector<cv::Point> > contours;
    for( int c = 0; c < 3; c++ ) {
        int ch[] = {c, 0};
        mixChannels(&timg, 1, &gray0, 1, ch, 1);
        for( int l = 0; l < N; l++ ) {
            if( l == 0 ) {
                cv::Canny(gray0, gray, 0, thresh, 5);
                cv::dilate(gray, gray, cv::Mat(), cv::Point(-1,-1));
            }
            else {
                gray = gray0 >= (l+1)*255/N;
            }
            cv::findContours(gray, contours, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE);
            std::vector<cv::Point> approx;
            for( size_t i = 0; i < contours.size(); i++ )
            {
                cv::approxPolyDP(cv::Mat(contours[i]), approx, arcLength(cv::Mat(contours[i]), true)*0.02, true);
                if( approx.size() == 4 && fabs(contourArea(cv::Mat(approx))) > 1000 && cv::isContourConvex(cv::Mat(approx))) {
                    double maxCosine = 0;

                    for( int j = 2; j < 5; j++ )
                    {
                        double cosine = fabs(angle(approx[j%4], approx[j-2], approx[j-1]));
                        maxCosine = MAX(maxCosine, cosine);
                    }

                    if( maxCosine < 0.3 ) {
                        squares.push_back(approx);
                    }
                }
            }
        }
    }
    return squares;
}

EDITAR 17/08/2012:

Para dibujar los cuadrados detectados en la imagen, use este código:

cv::Mat debugSquares( std::vector<std::vector<cv::Point> > squares, cv::Mat image )
{
    for ( int i = 0; i< squares.size(); i++ ) {
        // draw contour
        cv::drawContours(image, squares, i, cv::Scalar(255,0,0), 1, 8, std::vector<cv::Vec4i>(), 0, cv::Point());

        // draw bounding rect
        cv::Rect rect = boundingRect(cv::Mat(squares[i]));
        cv::rectangle(image, rect.tl(), rect.br(), cv::Scalar(0,255,0), 2, 8, 0);

        // draw rotated rect
        cv::RotatedRect minRect = minAreaRect(cv::Mat(squares[i]));
        cv::Point2f rect_points[4];
        minRect.points( rect_points );
        for ( int j = 0; j < 4; j++ ) {
            cv::line( image, rect_points[j], rect_points[(j+1)%4], cv::Scalar(0,0,255), 1, 8 ); // blue
        }
    }

    return image;
}
dom
fuente
1
Creo que puede ajustar el título de la pregunta para algo como Detectar una hoja de papel , si cree que es más apropiado.
karlphillip
1
@moosgummi Estoy buscando tener la misma funcionalidad que ha implementado, es decir, "Detectar las esquinas de la imagen / documento capturado". ¿Cómo logró esto? ¿Podría usar OpenCV dentro de mi aplicación para iPhone? Por favor, me sugieren alguna mejor manera de tener este ..
Ajay Sharma
1
¿Alguna vez has hecho algo con OpenCV? ¿Alguna aplicación en absoluto?
karlphillip
66
Vale la pena señalar que la bandera CV_RETR_EXTERNAL se puede usar al encontrar los contornos para rechazar todos los contornos dentro de una forma cerrada.
mehfoos yacoob

Respuestas:

162

Este es un tema recurrente en Stackoverflow y, como no pude encontrar una implementación relevante, decidí aceptar el desafío.

Hice algunas modificaciones a la demostración de cuadrados presente en OpenCV y el código C ++ resultante a continuación es capaz de detectar una hoja de papel en la imagen:

void find_squares(Mat& image, vector<vector<Point> >& squares)
{
    // blur will enhance edge detection
    Mat blurred(image);
    medianBlur(image, blurred, 9);

    Mat gray0(blurred.size(), CV_8U), gray;
    vector<vector<Point> > contours;

    // find squares in every color plane of the image
    for (int c = 0; c < 3; c++)
    {
        int ch[] = {c, 0};
        mixChannels(&blurred, 1, &gray0, 1, ch, 1);

        // try several threshold levels
        const int threshold_level = 2;
        for (int l = 0; l < threshold_level; l++)
        {
            // Use Canny instead of zero threshold level!
            // Canny helps to catch squares with gradient shading
            if (l == 0)
            {
                Canny(gray0, gray, 10, 20, 3); // 

                // Dilate helps to remove potential holes between edge segments
                dilate(gray, gray, Mat(), Point(-1,-1));
            }
            else
            {
                    gray = gray0 >= (l+1) * 255 / threshold_level;
            }

            // Find contours and store them in a list
            findContours(gray, contours, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE);

            // Test contours
            vector<Point> approx;
            for (size_t i = 0; i < contours.size(); i++)
            {
                    // approximate contour with accuracy proportional
                    // to the contour perimeter
                    approxPolyDP(Mat(contours[i]), approx, arcLength(Mat(contours[i]), true)*0.02, true);

                    // Note: absolute value of an area is used because
                    // area may be positive or negative - in accordance with the
                    // contour orientation
                    if (approx.size() == 4 &&
                            fabs(contourArea(Mat(approx))) > 1000 &&
                            isContourConvex(Mat(approx)))
                    {
                            double maxCosine = 0;

                            for (int j = 2; j < 5; j++)
                            {
                                    double cosine = fabs(angle(approx[j%4], approx[j-2], approx[j-1]));
                                    maxCosine = MAX(maxCosine, cosine);
                            }

                            if (maxCosine < 0.3)
                                    squares.push_back(approx);
                    }
            }
        }
    }
}

Después de ejecutar este procedimiento, la hoja de papel será el cuadrado más grande en vector<vector<Point> >:

detección de hojas de papel opencv

Te dejo escribir la función para encontrar el cuadrado más grande. ;)

karlphillip
fuente
44
Por eso uso el control de fuente. La modificación accidental más pequeña del código se puede descubrir fácilmente. Si no cambió nada, intente probar con otras imágenes y finalmente recompile / reinstale opencv.
karlphillip
2
OpenCV es prácticamente igual para todas las plataformas (Win / Linux / Mac / iPhone / ...). La diferencia es que algunos no son compatibles con el módulo GPU de OpenCV. ¿Ya has construido OpenCV para iOS ? ¿Pudiste probarlo? Creo que estas son las preguntas que debe responder antes de intentar algo más avanzado. ¡Pequeños pasos!
karlphillip
1
@karlphillip Probé este código y pude detectar el papel claramente, pero lleva mucho tiempo. ¿El código es realmente pesado? hay una aplicación llamada SayText donde esta detección ocurre en tiempo real desde una transmisión de video. Este código no sería práctico en tiempo real, ¿estoy en lo cierto?
alandalusi
1
Probablemente. Esta es una respuesta académica, no muy práctica para la industria. Puede probar todo tipo de optimizaciones, comenzando con la definición del contador ubicado en for (int c = 0; c < 3; c++), que es responsable de iterar en cada canal de la imagen. Por ejemplo, puede configurarlo para que itere solo en un canal :) No olvide votar.
karlphillip
3
@SilentPro angle()es una función auxiliar . Como se indica en la respuesta, este código se basa en samples / cpp / squares.cpp presente en OpenCV.
karlphillip
40

A menos que haya algún otro requisito no especificado, simplemente convertiría su imagen en color a escala de grises y trabajaría solo con eso (no es necesario trabajar en los 3 canales, el contraste presente ya es demasiado alto). Además, a menos que haya algún problema específico con respecto al cambio de tamaño, trabajaría con una versión reducida de sus imágenes, ya que son relativamente grandes y el tamaño no agrega nada al problema que se está resolviendo. Luego, finalmente, su problema se resuelve con un filtro de mediana, algunas herramientas morfológicas básicas y estadísticas (principalmente para el umbral de Otsu, que ya está hecho para usted).

Esto es lo que obtengo con su imagen de muestra y alguna otra imagen con una hoja de papel que encontré alrededor:

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

El filtro de mediana se utiliza para eliminar detalles menores de la imagen, ahora en escala de grises. Posiblemente eliminará líneas finas dentro del papel blanquecino, lo cual es bueno porque luego terminará con pequeños componentes conectados que son fáciles de descartar. Después de la mediana, aplique un gradiente morfológico (simplemente dilation- erosion) y binarice el resultado por Otsu. El gradiente morfológico es un buen método para mantener bordes fuertes, debe usarse más. Luego, dado que este gradiente aumentará el ancho del contorno, aplique un adelgazamiento morfológico. Ahora puede descartar componentes pequeños.

En este punto, esto es lo que tenemos con la imagen derecha arriba (antes de dibujar el polígono azul), el izquierdo no se muestra porque el único componente restante es el que describe el papel:

ingrese la descripción de la imagen aquí

Dados los ejemplos, ahora el único problema que queda es distinguir entre componentes que parecen rectángulos y otros que no. Se trata de determinar una relación entre el área del casco convexo que contiene la forma y el área de su cuadro delimitador; la relación 0.7 funciona bien para estos ejemplos. Es posible que también deba descartar los componentes que están dentro del papel, pero no en estos ejemplos al usar este método (sin embargo, hacer este paso debería ser muy fácil, especialmente porque se puede hacer a través de OpenCV directamente).

Como referencia, aquí hay un código de muestra en Mathematica:

f = Import["http://thwartedglamour.files.wordpress.com/2010/06/my-coffee-table-1-sa.jpg"]
f = ImageResize[f, ImageDimensions[f][[1]]/4]
g = MedianFilter[ColorConvert[f, "Grayscale"], 2]
h = DeleteSmallComponents[Thinning[
     Binarize[ImageSubtract[Dilation[g, 1], Erosion[g, 1]]]]]
convexvert = ComponentMeasurements[SelectComponents[
     h, {"ConvexArea", "BoundingBoxArea"}, #1 / #2 > 0.7 &], 
     "ConvexVertices"][[All, 2]]
(* To visualize the blue polygons above: *)
Show[f, Graphics[{EdgeForm[{Blue, Thick}], RGBColor[0, 0, 1, 0.5], 
     Polygon @@ convexvert}]]

Si hay situaciones más variadas en las que el rectángulo del papel no está tan bien definido, o el enfoque lo confunde con otras formas, estas situaciones podrían ocurrir debido a varias razones, pero una causa común es la mala adquisición de imágenes, entonces intente combinar el pre -procesos de procesamiento con el trabajo descrito en el documento "Detección de rectángulos basada en una transformación de ventana de Hough".

mmgp
fuente
1
¿Hay alguna diferencia importante en la implementación de la suya y la anterior (es decir, la respuesta de @karlphilip)? Lo siento, no pude encontrar ninguno en un vistazo rápido (excepto 3 canales-1 canal y Mathematica-OpenCV).
Abid Rahman K
2
@AbidRahmanK sí, hay ... No uso canny ni "varios umbrales" para empezar. Hay otras diferencias, pero por el tono de su comentario, parece inútil hacer un esfuerzo en mi propio comentario.
mmgp
1
Veo que ambos encuentran primero los bordes y determinan qué borde es cuadrado. Para encontrar bordes, ustedes usan diferentes métodos. Él usa astuto, tú usas algo de dilatación-erosión. Y "varios umbrales", pueden ser obtenidos de muestras de OpenCV, usadas para encontrar cuadrados. Lo principal es que sentí que el concepto general es el mismo. "Buscar bordes y detectar cuadrados". Y lo pregunté sinceramente, no sé qué "tono" obtuviste de mi comentario o qué (entendiste / entendiste mal). Entonces, si siente que esta pregunta es sincera, me gustaría saber otras diferencias. De lo contrario, descarte mis comentarios.
Abid Rahman K
1
@AbidRahmanK, por supuesto, el concepto es el mismo, la tarea es la misma. Se está utilizando el filtrado medio, se está utilizando adelgazamiento, no me importa de dónde tomó la idea de varios umbrales: simplemente no se usa aquí (¿cómo puede no ser una diferencia?), La imagen se redimensiona aquí, el Las medidas de los componentes son diferentes. "Algunas dilataciones-erosiones" no dan bordes binarios, otsu se usa para eso. No tiene sentido mencionar esto, el código está ahí.
mmgp
1
K. Gracias. Tengo la respuesta Concept is the same. (Nunca usé Mathematica, por lo que no puedo entender el código). Y las diferencias que mencionó son diferencias, pero no un enfoque diferente o uno importante. Si aún no lo hizo, por ejemplo, verifique esto:
Abid Rahman K
14

Bueno, llego tarde.


En su imagen, el papel es white, mientras que el fondo es colored. Por lo tanto, es mejor detectar que el papel está Saturation(饱和度)canalizado HSV color space. Tome referirse a wiki HSL_and_HSV primero. Luego, copiaré la mayoría de las ideas de mi respuesta en este Detectar segmento de color en una imagen .


Pasos principales:

  1. Interpretar BGR
  2. Convertir la imagen a partir bgrde hsvespacio
  3. Umbral del canal S
  4. Luego encuentre el contorno externo máximo (o hacer Canny, o HoughLinescomo quiera, elijo findContours), aproximadamente para obtener las esquinas.

Este es mi resultado:

ingrese la descripción de la imagen aquí


El código de Python (Python 3.5 + OpenCV 3.3):

#!/usr/bin/python3
# 2017.12.20 10:47:28 CST
# 2017.12.20 11:29:30 CST

import cv2
import numpy as np

##(1) read into  bgr-space
img = cv2.imread("test2.jpg")

##(2) convert to hsv-space, then split the channels
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h,s,v = cv2.split(hsv)

##(3) threshold the S channel using adaptive method(`THRESH_OTSU`) or fixed thresh
th, threshed = cv2.threshold(s, 50, 255, cv2.THRESH_BINARY_INV)

##(4) find all the external contours on the threshed S
#_, cnts, _ = cv2.findContours(threshed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cv2.findContours(threshed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]

canvas  = img.copy()
#cv2.drawContours(canvas, cnts, -1, (0,255,0), 1)

## sort and choose the largest contour
cnts = sorted(cnts, key = cv2.contourArea)
cnt = cnts[-1]

## approx the contour, so the get the corner points
arclen = cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, 0.02* arclen, True)
cv2.drawContours(canvas, [cnt], -1, (255,0,0), 1, cv2.LINE_AA)
cv2.drawContours(canvas, [approx], -1, (0, 0, 255), 1, cv2.LINE_AA)

## Ok, you can see the result as tag(6)
cv2.imwrite("detected.png", canvas)

Respuestas relacionadas:

  1. ¿Cómo detectar parches de colores en una imagen usando OpenCV?
  2. Detección de bordes en fondo coloreado usando OpenCV
  3. OpenCV C ++ / Obj-C: detección de una hoja de papel / detección cuadrada
  4. ¿Cómo usar `cv2.findContours` en diferentes versiones de OpenCV?
Kinght 金
fuente
Intenté usar el espacio S pero todavía no pude tener éxito. Vea esto: stackoverflow.com/questions/50699893/…
hchouhan02
3

Lo que necesita es un cuadrángulo en lugar de un rectángulo girado. RotatedRectte dará resultados incorrectos. También necesitará una proyección en perspectiva.

Básicamente lo que se debe hacer es:

  • Recorra todos los segmentos de polígono y conecte aquellos que son casi equel.
  • Ordénelos para que tenga los 4 segmentos de línea más grandes.
  • Interseca esas líneas y tendrás los 4 puntos de esquina más probables.
  • Transforme la matriz sobre la perspectiva reunida desde los puntos de esquina y la relación de aspecto del objeto conocido.

Implementé una clase Quadrangleque se encarga de la conversión de contorno a cuadrilátero y también la transformará en la perspectiva correcta.

Vea una implementación funcional aquí: Java OpenCV que diseña un contorno

Tim
fuente
1

Una vez que haya detectado el cuadro delimitador del documento, puede realizar una transformación de perspectiva de cuatro puntos para obtener una vista de arriba hacia abajo de la imagen. Esto solucionará la inclinación y aislará solo el objeto deseado.


Imagen de entrada:

Objeto de texto detectado

Vista de arriba hacia abajo del documento de texto

Código

from imutils.perspective import four_point_transform
import cv2
import numpy

# Load image, grayscale, Gaussian blur, Otsu's threshold
image = cv2.imread("1.png")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (7,7), 0)
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]

# Find contours and sort for largest contour
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
displayCnt = None

for c in cnts:
    # Perform contour approximation
    peri = cv2.arcLength(c, True)
    approx = cv2.approxPolyDP(c, 0.02 * peri, True)
    if len(approx) == 4:
        displayCnt = approx
        break

# Obtain birds' eye view of image
warped = four_point_transform(image, displayCnt.reshape(4, 2))

cv2.imshow("thresh", thresh)
cv2.imshow("warped", warped)
cv2.imshow("image", image)
cv2.waitKey()
nathancy
fuente
-1

Detectar una hoja de papel es algo de la vieja escuela. Si desea abordar la detección de sesgo, es mejor que apunte inmediatamente a la detección de líneas de texto. Con esto obtendrás los extremos a la izquierda, derecha, arriba y abajo. Descarte cualquier gráfico en la imagen si no lo desea y luego haga algunas estadísticas en los segmentos de línea de texto para encontrar el rango de ángulo más frecuente o más bien ángulo. Así es como se reducirá a un buen ángulo de inclinación. Ahora, después de esto, coloque estos parámetros, el ángulo de inclinación y los extremos para enderezar y cortar la imagen a lo que se requiere.

En cuanto al requisito de imagen actual, es mejor si prueba CV_RETR_EXTERNAL en lugar de CV_RETR_LIST.

Otro método para detectar bordes es entrenar un clasificador aleatorio de bosques en los bordes del papel y luego usar el clasificador para obtener el Mapa de bordes. Este es, con mucho, un método robusto pero requiere capacitación y tiempo.

Los bosques aleatorios funcionarán con escenarios de diferencia de bajo contraste, por ejemplo, papel blanco sobre fondo más o menos blanco.

Anubhav Rohatgi
fuente