Como melhorar o reconhecimento de dígitos de um modelo treinado no MNIST?

12

Estou trabalhando no reconhecimento de vários dígitos Java, usando a OpenCVbiblioteca para pré-processamento e segmentação, e um Kerasmodelo treinado no MNIST (com precisão de 0,98) para reconhecimento.

O reconhecimento parece funcionar muito bem, além de uma coisa. A rede geralmente falha em reconhecer esses (número "um"). Não consigo descobrir se isso ocorre devido ao pré-processamento / implementação incorreta da segmentação ou se uma rede treinada no MNIST padrão simplesmente não viu o número um que se parece com os meus casos de teste.

Veja como são os dígitos problemáticos após o pré-processamento e a segmentação:

insira a descrição da imagem aquitorna insira a descrição da imagem aqui- se e é classificado como 4.

insira a descrição da imagem aquitorna insira a descrição da imagem aqui- se e é classificado como 7.

insira a descrição da imagem aquitorna insira a descrição da imagem aqui- se e é classificado como 4. E assim por diante...

Isso pode ser corrigido com a melhoria do processo de segmentação? Ou melhor, aprimorando o conjunto de treinamento?

Editar: o aprimoramento do conjunto de treinamento (aumento de dados) definitivamente ajudaria, que eu já estou testando, a questão do pré-processamento correto ainda permanece.

Meu pré-processamento consiste em redimensionar, converter em escala de cinza, binarização, inversão e dilatação. Aqui está o código:

Mat resized = new Mat();
Imgproc.resize(image, resized, new Size(), 8, 8, Imgproc.INTER_CUBIC);

Mat grayscale = new Mat();
Imgproc.cvtColor(resized, grayscale, Imgproc.COLOR_BGR2GRAY);

Mat binImg = new Mat(grayscale.size(), CvType.CV_8U);
Imgproc.threshold(grayscale, binImg, 0, 255, Imgproc.THRESH_OTSU);

Mat inverted = new Mat();
Core.bitwise_not(binImg, inverted);

Mat dilated = new Mat(inverted.size(), CvType.CV_8U);
int dilation_size = 5;
Mat kernel = Imgproc.getStructuringElement(Imgproc.CV_SHAPE_CROSS, new Size(dilation_size, dilation_size));
Imgproc.dilate(inverted, dilated, kernel, new Point(-1,-1), 1);

A imagem pré-processada é então segmentada em dígitos individuais da seguinte maneira:

List<Mat> digits = new ArrayList<>();
List<MatOfPoint> contours = new ArrayList<>();
Imgproc.findContours(preprocessed.clone(), contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);

// code to sort contours
// code to check that contour is a valid char

List rects = new ArrayList<>();

for (MatOfPoint contour : contours) {
     Rect boundingBox = Imgproc.boundingRect(contour);
     Rect rectCrop = new Rect(boundingBox.x, boundingBox.y, boundingBox.width, boundingBox.height);

     rects.add(rectCrop);
}

for (int i = 0; i < rects.size(); i++) {
    Rect x = (Rect) rects.get(i);
    Mat digit = new Mat(preprocessed, x);

    int border = 50;
    Mat result = digit.clone();
    Core.copyMakeBorder(result, result, border, border, border, border, Core.BORDER_CONSTANT, new Scalar(0, 0, 0));

    Imgproc.resize(result, result, new Size(28, 28));
    digits.add(result);
}
youngpanda
fonte
11
você está usando a máscara ou os pixels da escala de cinza original (mascarada?) como entrada para sua classificação?
Micka
@Micka Estou usando a versão pré-processada (binarizada, invertida, dilatada). Aqueles que correspondem ao conjunto de treinamento MNIST. Existem exemplos do número "1" após o pré-processamento na minha postagem.
youngpanda

Respostas:

5

Acredito que seu problema é o processo de dilatação. Entendo que você deseja normalizar os tamanhos de imagem, mas não deve quebrar as proporções, redimensionar para o máximo desejado por um eixo (aquele que permite maior redimensionamento sem permitir que outra dimensão de eixo exceda o tamanho máximo) e preencha com a cor de fundo, o resto da imagem. Não é que "o MNIST padrão simplesmente não tenha visto o número um que se parece com os seus casos de teste", você faz suas imagens parecerem com números treinados diferentes (aqueles que são reconhecidos)

Sobreposição da fonte e das imagens processadas

Se você manteve a proporção correta de suas imagens (de origem e pós-processada), poderá ver que não apenas redimensionou a imagem, mas a "distorceu". Pode ser o resultado de dilatação não homogênea ou redimensionamento incorreto

Senhor
fonte
Acredito que o @SiR tenha algum peso. Tente não alterar a proporção dos literais numéricos.
ZdaR 21/10/19
Desculpe, eu não entendo direito. Você acha que meu processo de dilatação ou redimensionamento é o problema? Apenas redimensiono a imagem no início com esta linha Imgproc.resize(image, resized, new Size(), 8, 8, Imgproc.INTER_CUBIC);. Aqui a relação de aspecto permanece a mesma, onde eu quebro as proporções?
youngpanda
@SiR em resposta às suas edições acima: sim, não redimensiono a imagem, aplico diferentes operações, uma delas é a dilatação, que é morfológica, que causa leve "distorção", pois causa regiões brilhantes dentro de um imagem de “crescer" Ou você quer dizer redimensionamento na final, onde eu faço as imagens 28x28.?
youngpanda
@youngpanda, você pode encontrar a discussão aqui stackoverflow.com/questions/28525436/… interessante. Isso pode lhe dar uma pista do motivo pelo qual sua abordagem não traz bons resultados #
231
@SiR obrigado pelo link, eu estou familiarizado com LeNet, mas é bom para ler novamente
youngpanda
5

Já existem algumas respostas postadas, mas nenhuma delas responde à sua pergunta real sobre pré-processamento de imagem .

Por minha vez, não vejo problemas significativos com sua implementação, desde que seja um projeto de estudo, bem feito.

Mas uma coisa a notar você pode perder. Existem operações básicas em morfologia matemática: erosão e dilatação (usadas por você). E há operações complexas: várias combinações de operações básicas (por exemplo, abertura e fechamento). O link da Wikipedia não é a melhor referência de currículo, mas você pode começar com ele para ter uma idéia.

Geralmente, é melhor usar abertura em vez de erosão e fechamento em vez de dilatação, pois nesse caso a imagem binária original muda muito menos (mas é atingido o efeito desejado de limpar bordas afiadas ou preencher lacunas). Portanto, no seu caso, você deve verificar o fechamento (dilatação da imagem seguida de erosão com o mesmo núcleo). Caso a imagem extra-pequena 8 * 8 seja bastante modificada quando você dilatar mesmo com o núcleo 1 * 1 (1 pixel é mais do que 16% da imagem), o que é menor nas imagens maiores).

Para visualizar a ideia, veja as seguintes fotos (dos tutoriais do OpenCV: 1 , 2 ):

dilatação: símbolo original e um dilatado

fechamento: símbolo original e fechado

Espero que ajude.

f4f
fonte
Obrigado pela contribuição! Na verdade, não é um projeto de estudo, então qual seria o problema então? .. Minha imagem é bastante grande quando aplico dilatação, 8x8 não é o tamanho da imagem, é o fator de redimensionamento para altura e largura. Mas ainda pode ser uma opção de aprimoramento para experimentar diferentes operações matemáticas. Eu não sabia sobre abertura e fechamento, vou experimentar! Obrigado.
youngpanda
A culpa é minha, redimensionou a chamada como foi redimensionada como 8 * 8 como novo tamanho. Caso você queira usar o OCR no mundo real, considere uma opção de transferência, aprendendo sua rede original com dados típicos da sua área de uso. Pelo menos verifique se melhora a precisão, geralmente deve ser.
f4f
Outra coisa a verificar é a ordem de pré-processamento: escala de cinza -> binário -> inverso -> redimensionamento. O redimensionamento é uma operação dispendiosa e não vejo necessidade de aplicá-lo à imagem colorida. E a segmentação de símbolos pode ser feita sem detecção de contorno (com algo menos dispendioso) se você tiver algum formato de entrada específico, mas pode ser difícil de implementar.
f4f
Se eu tivesse outro conjunto de dados além do MNIST, poderia tentar transferir o aprendizado :) Vou tentar alterar a ordem de pré-processamento e retornar para você. Obrigado! Eu ainda não havia nenhuma opção mais fácil do que a detecção de contorno para o meu problema ...
youngpanda
11
Está bem. Você mesmo pode coletar dados a partir dessas imagens e usará o OCR, uma prática comum.
f4f
4

Portanto, você precisa de uma abordagem complexa, pois cada etapa de sua cascata de computação se baseia nos resultados anteriores. No seu algoritmo, você tem os seguintes recursos:

  1. Pré-processamento de imagem

Como mencionado anteriormente, se você aplicar o redimensionamento, perderá informações sobre as proporções da imagem. É necessário fazer o mesmo reprocessamento de imagens de dígitos para obter os mesmos resultados implícitos no processo de treinamento.

Melhor maneira, se você apenas recortar a imagem em imagens de tamanho fixo. Nessa variante, você não precisará de contornos para localizar e redimensionar a imagem do dígito antes do processo de treinamento. Em seguida, você pode fazer uma pequena alteração no algoritmo de corte para melhor reconhecimento: encontre o contorno e coloque seu dígito sem redimensionar no centro do quadro de imagem relevante para reconhecimento.

Além disso, você deve prestar mais atenção ao algoritmo de binarização. Tive experiência estudando o efeito dos valores limite de binarização no erro de aprendizado: posso dizer que esse é um fator muito significativo. Você pode tentar outros algoritmos de binarização para verificar essa ideia. Por exemplo, você pode usar esta biblioteca para testar algoritmos alternativos de binarização.

  1. Algoritmo de aprendizagem

Para melhorar a qualidade do reconhecimento, você usa a validação cruzada no processo de treinamento. Isso ajuda a evitar o problema de ajuste excessivo nos dados de treinamento. Por exemplo, você pode ler este artigo, onde explicou como usá-lo com o Keras.

Às vezes, taxas mais altas de medida de precisão não dizem nada sobre a qualidade real do reconhecimento, pois a RNA treinada não encontrou o padrão nos dados de treinamento. Ele pode estar conectado ao processo de treinamento ou ao conjunto de dados de entrada, conforme explicado acima, ou pode causar a escolha da arquitetura da ANN.

  1. Arquitetura ANN

É um grande problema. Como definir a melhor arquitetura de RNA para resolver a tarefa? Não há maneiras comuns de fazer isso. Mas existem algumas maneiras de se aproximar do ideal. Por exemplo, você pode ler este livro . Isso ajuda você a ter uma visão melhor do seu problema. Além disso, você pode encontrar aqui algumas fórmulas heurísticas para ajustar o número de camadas / elementos ocultos para sua RNA. Também aqui você encontrará uma pequena visão geral sobre isso.

Espero que isso ajude.

Egor Zamotaev
fonte
1. Se o entendi corretamente, não posso cortar em tamanho fixo, é uma imagem de um número de vários dígitos e todos os casos são diferentes em tamanho / local etc. Ou você quis dizer algo diferente? Sim, tentei diferentes métodos de binarização e parâmetros aprimorados, se é isso que você quer dizer. 2. Na verdade, o reconhecimento no MNIST é ótimo, não há super ajuste, a precisão que eu mencionei é a precisão do teste. Nem a rede nem seu treinamento são o problema. 3. Obrigado por todos os links, mas estou muito feliz com minha arquitetura, é claro que sempre há espaço para melhorias.
youngpanda
Sim, você entendeu. Mas você sempre tem a possibilidade de tornar seu conjunto de dados mais unificado. No seu caso, é melhor recortar imagens de dígitos pelos contornos, como você já faz. Mas depois disso, será melhor expandir as imagens dos dígitos para o tamanho unificado, de acordo com o tamanho máximo de uma imagem de dígito nas escalas x e y. Você pode preferir fazer o centro da região do contorno dos dígitos. Isso fornecerá dados de entrada mais limpos para o seu algoritmo de treinamento.
Egor Zamotaev
Você quer dizer que eu tenho que pular a dilatação? No final, já centralizo a imagem, quando aplico a borda (50 px de cada lado). Depois disso, redimensiono cada dígito para 28x28, pois esse é o tamanho que precisamos para o MNIST. Quer dizer que posso redimensionar para 28x28 de maneira diferente?
youngpanda
11
Sim, a dilatação é indesejável. Seus contornos podem ter diferentes proporções por altura e largura, por isso é necessário aprimorar o algoritmo aqui. Pelo menos você deve criar tamanhos de imagens com as mesmas proporções. Como você tem tamanhos de imagem de entrada de 28x28, é necessário preparar imagens com a mesma proporção de 1: 1 nas escalas x e y. Você não deve obter uma borda de 50 px para cada lado da imagem, mas as bordas X, Y px que satisfaçam a condição: contourSizeX + borderSizeX == contourSizeY + borderSizeY. Isso é tudo.
Egor Zamotaev
Eu já tentei sem dilatação (esqueci de mencionar no post). Não alterou nenhum resultado ... Meu número de fronteira era experimental. Idealmente, eu precisaria dos meus dígitos para caber na caixa 20x20 (tamanho normalizado como tal no conjunto de dados) e depois mudá-lo usando o centro de massa ...
youngpanda
1

Após algumas pesquisas e experimentos, cheguei à conclusão de que o pré-processamento da imagem em si não era o problema (alterei alguns parâmetros sugeridos, como por exemplo, tamanho e forma da dilatação, mas eles não eram cruciais para os resultados). O que ajudou, no entanto, foram duas coisas a seguir:

  1. Como o @ f4f notou, eu precisava coletar meu próprio conjunto de dados com dados do mundo real. Isso já ajudou tremendamente.

  2. Fiz alterações importantes no meu pré-processamento de segmentação. Depois de obter contornos individuais, primeiro normalizo o tamanho das imagens para caber em uma 20x20caixa de pixels (como estão MNIST). Depois disso, centralizo a caixa no meio da 28x28imagem usando o centro de massa (que para imagens binárias é o valor médio em ambas as dimensões).

Obviamente, ainda existem casos de segmentação difíceis, como sobreposição ou dígitos conectados, mas as alterações acima responderam à minha pergunta inicial e melhoraram meu desempenho de classificação.

youngpanda
fonte