OpenCV C ++ / Obj-C: Detectando uma folha de papel / Detecção quadrada

178

Eu implementei com sucesso o exemplo de detecção quadrada do OpenCV no meu aplicativo de teste, mas agora preciso filtrar a saída, porque é bastante confuso - ou meu código está errado?

Estou interessado nos quatro pontos de canto do papel para a redução da inclinação (como que ) e posterior processamento ...

Entrada e Saída: Entrada e Saída

Imagem original:

clique

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;
}

EDIT 17/08/2012:

Para desenhar os quadrados detectados na imagem, 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
fonte
1
Acho que você pode ajustar o título da pergunta para algo como Detectar uma folha de papel , se achar mais apropriado.
karlphillip
1
@moosgummi Estou procurando ter a mesma funcionalidade que você implementou, ou seja, "Detectar os cantos da imagem / documento capturado" .Como você conseguiu isso? Eu seria capaz de usar o OpenCV no meu aplicativo para iPhone? Por favor me sugerir alguma maneira melhor ter isso ..
Ajay Sharma
1
Você já fez alguma coisa com o OpenCV? Alguma aplicação?
precisa saber é o seguinte
6
Vale a pena notar que a bandeira CV_RETR_EXTERNAL pode ser usada ao encontrar os contornos para rejeitar todos os contornos dentro de uma forma fechada.
Mehfoos yacoob

Respostas:

162

Esse é um assunto recorrente no Stackoverflow e, como não consegui encontrar uma implementação relevante, decidi aceitar o desafio.

Fiz algumas modificações na demonstração de quadrados presente no OpenCV e o código C ++ resultante abaixo é capaz de detectar uma folha de papel na imagem:

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);
                    }
            }
        }
    }
}

Após a execução deste procedimento, a folha de papel será o maior quadrado em vector<vector<Point> >:

detecção de folhas de papel opencv

Estou deixando você escrever a função para encontrar o quadrado maior. ;)

karlphillip
fonte
4
É por isso que eu uso o controle de origem. A menor modificação acidental no código pode ser facilmente descoberta. Se você não mudou nada, tente testar com outras imagens e, finalmente, recompile / reinstale o opencv.
precisa saber é o seguinte
2
O OpenCV é praticamente o mesmo para todas as plataformas (Win / Linux / Mac / iPhone / ...). A diferença é que alguns não suportam o módulo GPU do OpenCV. Você já criou o OpenCV para iOS ? Você conseguiu testá-lo? Acho que essas são as perguntas que você precisa responder antes de tentar algo mais avançado. Passos de bebê!
karlphillip
1
@karlphillip Testei este código e consegui detectar claramente o papel, mas isso leva muito tempo. O código é realmente pesado? existe um aplicativo chamado SayText em que essa detecção ocorre em tempo real a partir de um fluxo de vídeo. Esse código seria impraticável em tempo real, estou certo?
precisa
1
Provavelmente. Esta é uma resposta acadêmica, pouco prática para a indústria. Há todos os tipos de otimizações que você pode tentar, começando com a definição do contador localizado em for (int c = 0; c < 3; c++), responsável pela iteração em todos os canais da imagem. Por exemplo, você pode configurá-lo para iterar em apenas um canal :) Não esqueça de votar.
karlphillip
3
@SilentPro angle()é uma função auxiliar . Como indicado na resposta, esse código é baseado em samples / cpp / squares.cpp presente no OpenCV.
precisa saber é o seguinte
40

A menos que haja outro requisito não especificado, eu simplesmente converteria sua imagem colorida em escala de cinza e trabalharia apenas com isso (não é necessário trabalhar nos três canais, o contraste presente já é muito alto). Além disso, a menos que haja algum problema específico em relação ao redimensionamento, eu trabalharia com uma versão reduzida das suas imagens, pois elas são relativamente grandes e o tamanho não acrescenta nada ao problema que está sendo resolvido. Finalmente, seu problema é resolvido com um filtro mediano, algumas ferramentas morfológicas básicas e estatísticas (principalmente para o limiar de Otsu, que já foi feito para você).

Aqui está o que eu obtenho com sua imagem de amostra e outra imagem com uma folha de papel que encontrei ao redor:

insira a descrição da imagem aqui insira a descrição da imagem aqui

O filtro mediano é usado para remover pequenos detalhes da imagem, agora em escala de cinza. Possivelmente removerá linhas finas dentro do papel esbranquiçado, o que é bom, pois você terminará com pequenos componentes conectados, fáceis de descartar. Após a mediana, aplique um gradiente morfológico (simplesmente dilation- erosion) e binarize o resultado por Otsu. O gradiente morfológico é um bom método para manter bordas fortes, devendo ser usado mais. Então, como esse gradiente aumentará a largura do contorno, aplique um afinamento morfológico. Agora você pode descartar pequenos componentes.

Neste ponto, aqui está o que temos com a imagem correta acima (antes de desenhar o polígono azul), a esquerda não é mostrada porque o único componente restante é o que descreve o artigo:

insira a descrição da imagem aqui

Dados os exemplos, agora o único problema que resta é distinguir entre componentes que parecem retângulos e outros que não. Trata-se de determinar uma razão entre a área do casco convexo que contém a forma e a área de sua caixa delimitadora; a razão 0,7 funciona bem para esses exemplos. Pode ser que você também precise descartar componentes que estão dentro do papel, mas não nesses exemplos usando esse método (no entanto, executar esta etapa deve ser muito fácil, especialmente porque isso pode ser feito diretamente pelo OpenCV).

Para referência, aqui está um código de exemplo no 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}]]

Se houver situações mais variadas em que o retângulo do papel não estiver tão bem definido ou a abordagem o confundir com outras formas - essas situações podem ocorrer devido a vários motivos, mas uma causa comum é a má aquisição de imagem -, tente combinar o pré -processo de etapas com o trabalho descrito no artigo "Detecção de retângulos com base em uma transformação de vento em janela".

mmgp
fonte
1
existe alguma diferença importante na sua implementação e na acima (por exemplo, a resposta de @karlphilip)? Sinto muito, mas não consegui encontrar nenhum em uma olhada rápida (exceto 3 canais-1 canal e Mathematica-OpenCV).
Abid Rahman K
2
@AbidRahmanK sim, existem .. Eu não uso o canny nem "vários limites" para começar. Existem outras diferenças, mas pelo tom do seu comentário parece inútil colocar algum esforço no meu próprio comentário.
MMGP
1
Vejo que vocês dois encontram as arestas e determinam qual é a aresta quadrada. Para encontrar arestas, você usa métodos diferentes. Ele usa astuto, você usa alguma dilatação-erosão. E "vários limiares", pode ser que ele tenha obtido amostras de OpenCV, usadas para encontrar quadrados. O principal é que eu senti que o conceito geral é o mesmo. "Encontre arestas e detecte quadrados". E perguntei sinceramente: não sei qual "tom" você recebeu do meu comentário ou o que você (entendeu / entendeu mal). Então, se você acha que essa pergunta é sincera, eu gostaria de saber outras diferenças. Caso contrário, descarte meus comentários.
Abid Rahman K
1
@AbidRahmanK é claro que o conceito é o mesmo, a tarefa é a mesma. A filtragem mediana está sendo usada, o desbaste está sendo usado, não me importo de onde ele tirou a idéia de vários limites - ela simplesmente não é usada aqui (portanto, como não pode ser uma diferença?), A imagem é redimensionada aqui, o as medidas dos componentes são diferentes. "Alguma erosão por dilatação" não produz bordas binárias, o otsu é usado para isso. Não faz sentido mencionar isso, o código está lá.
MMGP
1
K. Obrigado. Tenho a resposta. Concept is the same. (Eu nunca usei o Mathematica, por isso não consigo entender o código.) E as diferenças que você mencionou são diferenças, mas não uma abordagem diferente ou as principais. Se você ainda não o fez, por exemplo, verifique o seguinte:
Abid Rahman K
14

Estou atrasado.


Na sua imagem, o papel é white, enquanto o fundo é colored. Portanto, é melhor detectar o papel Saturation(饱和度)no canal HSV color space. Consulte o wiki HSL_and_HSV primeiro. Em seguida, copiarei a maioria das ideias da minha resposta neste segmento Detectar cores em uma imagem .


Passos principais:

  1. Leia em BGR
  2. Converta a imagem de bgrpara o hsvespaço
  3. Limiar o canal S
  4. Em seguida, encontre o contorno externo máximo (ou faça Canny, ou HoughLinescomo quiser, eu escolho findContours), aproximadamente para obter os cantos.

Este é o meu resultado:

insira a descrição da imagem aqui


O código 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)

Respostas relacionadas:

  1. Como detectar manchas coloridas em uma imagem usando o OpenCV?
  2. Detecção de borda em fundo colorido usando OpenCV
  3. OpenCV C ++ / Obj-C: Detectando uma folha de papel / Detecção quadrada
  4. Como usar o `cv2.findContours` em diferentes versões do OpenCV?
Kinght 金
fonte
Eu tentei usar o espaço S, mas ainda não consegui obter sucesso. Veja isso: stackoverflow.com/questions/50699893/…
hchouhan02 06/06
3

O que você precisa é de um quadrilátero em vez de um retângulo girado. RotatedRectfornecerá resultados incorretos. Você também precisará de uma projeção em perspectiva.

Basicamente, o que deve ser feito é:

  • Passe por todos os segmentos poligonais e conecte aqueles que são quase iguais.
  • Classifique-os para ter os 4 maiores segmentos de linha.
  • Cruze essas linhas e você terá os 4 pontos de canto mais prováveis.
  • Transforme a matriz sobre a perspectiva reunida a partir dos pontos de canto e a proporção do objeto conhecido.

Eu implementei uma classe Quadrangleque cuida da conversão de contorno em quadrângulo e também a transformará na perspectiva correta.

Veja uma implementação de trabalho aqui: Java OpenCV limpando um contorno

Tim
fonte
1

Depois de detectar a caixa delimitadora do documento, você pode executar uma transformação de perspectiva de quatro pontos para obter uma visão de cima para baixo da imagem. Isso corrigirá a inclinação e isolará apenas o objeto desejado.


Imagem de entrada:

Objeto de texto detectado

Vista de cima para baixo do 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
fonte
-1

Detectar uma folha de papel é meio que uma velha escola. Se você deseja enfrentar a detecção de inclinação, é melhor se você objetiva imediatamente a detecção de linha de texto. Com isso, você obterá os extremas à esquerda, direita, superior e inferior. Descarte todos os gráficos da imagem, se você não quiser, e faça algumas estatísticas nos segmentos de linha de texto para encontrar a faixa de ângulo mais ocorrente ou melhor, o ângulo. É assim que você reduz o ângulo de inclinação. Agora, depois disso, você coloca esses parâmetros no ângulo de inclinação e nos extremas para inclinar e cortar a imagem conforme o necessário.

Quanto ao requisito de imagem atual, é melhor se você tentar CV_RETR_EXTERNAL em vez de CV_RETR_LIST.

Outro método para detectar bordas é treinar um classificador de florestas aleatórias nas bordas do papel e, em seguida, usar o classificador para obter o mapa das bordas. Este é de longe um método robusto, mas requer treinamento e tempo.

As florestas aleatórias funcionarão com cenários com diferenças de baixo contraste, por exemplo, white paper sobre fundo branco.

Anubhav Rohatgi
fonte