Esse artigo demonstra uma alternativa para desenvolvedores que pretendem trabalhar com Visão Computacional (Computer Vision - CV) em Java sem utilizar código nativo (Java Native Interface - JNI).
Uma das bibliotecas mais utilizadas hoje é o OpenCV que pode ser integrado com Java usando JNI, porém existem alguns problemas com essa abordagem: mais difícil de configurar, distribuir e não é multi-plataforma. Exigindo configurações e ajustes para cada sistema operacional e forçando a lançar releases diferentes para cada plataforma.
A alternativa apresentada nesse artigo é o BoofCV, uma biblioteca de código aberto para Visão Computacional em tempo real usando apenas Java.
O que é Visão Computacional?
Muitos confundem visão computacional com processamento de imagens, então é necessário esclarecer que a Visão Computacional é uma campo da computação que tenta simular a visão humana, não apenas aplicando efeitos em imagens, mas extraindo informações relevantes e de alto nível como a identificação de objetos.
A visão computacional desenvolve teoria e tecnologia para a construção de sistemas artificiais que obtém informações de imagens ou quaisquer dados multi-dimensionais. Exemplos de aplicações incluem inspeção industrial ou navegação de robôs móveis, envolve, quase sempre, a execução de um determinado conjunto de transformações em dados obtidos de sensores como câmeras e sonares.
Com os avanços das tecnologias de visão computacional, a medicina se coloca como uma das maiores beneficiadas. O processo de extração de informações das imagens com o objetivo de estabelecer diagnósticos médicos mais precisos vem ganhando força. O avanço das técnicas de processamento de imagens auxilia no resultado de diagnósticos com maior índice de acerto.
Porque BoofCV?
Além das vantagens citadas em relação as alternativas com JNI (como OpenCV), essa biblioteca mostrou ter um desempenho bastante satisfatório para uma aplicação comercial.
Vantagens:
- Multi-plataforma;
- Código Java puro, sem utilização de JNI;
- Integração com Maven;
- Suporte para Android;
- Open source (com atualizações frequentes);
- Possui vários exemplos e tem uma boa documentação.
A BoofCV é bem completa, incluindo módulos para:
- Processamento de imagens;
- Visão geométrica;
- Reconhecimento de objetos;
- Componentes para visualização de imagens;
- APIs I/O para imagens (leitura e escrita com suporte a vários formatos);
- Algoritmos de extração de características;
- Rastreamento de objetos;
- Calibração da câmera;
- Integração com Kinect/RGB-D (exemplo de odometria visual: YouTube).
O código fonte da BoofCV está disponível no GitHub, bem como os applets de demonstracão.
Mãos na massa
Em alguns momentos no desenvolvimento do Sistema de Gestão Educacional (Edu3) da CriativaSoft, o núcleo de pesquisa e inovação identificou alguns projetos e tarefas (como leitura de gabaritos, identificação de documentos escaneados), que precisariam de técnicas de CV, mas foram descartadas, pois as soluções sempre caminhavam para o OpenCV.
Com a BoofCV alguns desses projetos se tornaram viáveis. O exemplo anterior mostra uma aplicação prática no qual a BoofCV pode ser utilizada.
Descrição do Problema: Identificar a existência ou não das assinaturas na ficha de frequência a fim de gerar registro de falta para os alunos. Não está no escopo validar se a assinatura é do aluno em questão ou comparar assinaturas com de outras listas de presenças.
A imagem a seguir apresenta um exemplo de folha de lista de presença que foi assinada por alguns alunos e scaneada para identificar quais nomes foram assinados:
Imagem: https://github.com/CriativaSoft/TableAnalysisBoofCV/blob/master/data/frequencia.png
Como proceder?
O primeiro passo é observar a imagem e montar uma estratégia para resolver o problema. Na imagem anterior observe que a assinatura está dentro de uma tabela, mais precisamente na quarta coluna, os passos seguintes são: 1) identificar a região da tabela; 2) separar a tabela em um grid de imagens; 3) identificar em quais celulas estão as assinaturas; 4) aplicar um algoritmo de detecção de linhas/segmentos.
Pode-se resolver esse problema seguindo o algoritmo:
- Identificação das Linhas (Tutorial: Detecting Lines and Line Segments);
- Filtrar linhas relevantes (Horizontal e Vertical).
- Identificar a intersecção entre as linhas;
- Montar o grid com base nas intersecções;
- Extrair (sub-imagem) das células das assinaturas;
- Verificar a presença da assinatura (Tutorial: Detecting Lines and Line Segments);
- Melhorias.
Preparação do Ambiente
Os códigos com os exemplos e dependências desse artigo podem ser encontrados no GitHub.
Observação: O projeto Java foi criado no Eclipse e pode ser importado em outras IDE's.
Não esqueça de baixar o código do BoofCV, para ter a documentação no autocomplete faça download da última versão estável do "arquivo Jar, código-fonte do Jar e dependências", pois ajuda muito durante o desenvolvimento.
Detalhes do algoritmo e exemplos
Identificação das linhas
Existem vários algoritmos de detecção de linhas implementados, a escolha da implementação depende do tipo de problema que será tratado, nesse exemplo será utilizado o DetectLineHoughFoot, que implementa o algoritmo Hough Transform, muito utilizado na detecção de linhas.
Como descobrir que é o correto? Não existe bala de prata, é necessário testar e ajustar os parâmetros até encontrar uma boa solução.
A imagem a seguir mostra o resultado após aplicar o algoritmo DetectLineHoughFoot na figura da lista de presença:
Note como o resultado não é bem que se esperava, mas depois de alguns ajustes nos parâmetros é possível obter a seguinte figura:
Observe que todas as linhas da tabela que contém as assinaturas foram detectadas, esse é o resultado esperado.
A seguir o código utilizado para esse exemplo:
Código Step1.java:
public static void main( String args[] ) { /* Configuração dos parâmetros do DetectLineHoughFoot, faça os ajustes até achar um bom resultado. */ int edgeThreshold = 30, maxLines = 25; BufferedImage image = UtilImageIO.loadImage("data/frequencia.png"); // Conversão para o formato interno do BoofCV. ImageFloat32 input = ConvertBufferedImage.convertFromSingle(image, null, ImageFloat32.class); // Variável no qual será aplicado o algoritmo de detecção de linhas. ImageFloat32 out = new ImageFloat32(image.getWidth(), image.getHeight()); // Seguindo conselhos da documentação, aplicar o blur pode melhorar o desempenho. GBlurImageOps.gaussian(input, out, -1, 2, null); DetectLineHoughFootalg = FactoryDetectLineAlgs.houghFoot(new ConfigHoughFoot(6, 12, 5, edgeThreshold, maxLines), ImageFloat32.class, ImageFloat32.class); // Executar processamento da imagem e extração das linhas. List lines = alg.detect(out); // Visualização das imagens e linhas detectadas. ImageLinePanel gui = new ImageLinePanel(); gui.setBackground(image); gui.setLines(lines); gui.setPreferredSize(new Dimension(image.getWidth(), image.getHeight())); BufferedImage renderedBinary = VisualizeBinaryData.renderBinary(alg.getBinary(), null); ShowImages.showWindow(renderedBinary, "Detected Edges"); ShowImages.showWindow(gui, "Detected Lines"); }
O código completo do Step1.java está disponível no projeto de exemplo.
Algumas dicas e observações:
Vale destacar algumas vantagens em relação ao OpenCV: a BoofCV já possui inúmeras facilidades para visualização das imagens (no módulo: boofcv.gui), em comparação no OpenCV é necessário salvar a imagem em arquivo, o que torna muito lento o processo de testes.
Para fazer o processamento é preciso converter a imagem para um formato da BoofCV (veja mais detalhes nesse tutorial de tratamento de imagens) usando o ConvertBufferedImage.convertFrom, esse formato oferece um desempenho bastante superior em relação ao BufferedImage do Java.
Filtrar linhas relevantes (horizontal e vertical)
Nesse exemplo foi possível obter as linhas corretamente através da implementação do FactoryDetectLineAlgs.houghFoot. Dependendo do problema será necessário utilizar o código a seguir para filtrar apenas as linhas horizontais e verticais.
Listlines = alg.detect(out); List hlines = new LinkedList (); List vlines = new LinkedList (); for (LineParametric2D_F32 pline : lines) { long angle = Math.abs(Math.round(UtilAngle.radianToDegree(pline.getAngle()))); if (angle == 0 || angle == 180) { hlines.add(pline); } else if (angle > 80 && angle <= 100) { vlines.add(pline); } }
A BoofCV disponibiliza alguns exemplos e aplicações teste no seu repositório. Segue o exemplo do DetectLineApp.
Identificar a intersecção entre as linhas
Para identificar cada célula da tabela, é preciso identificar os pontos de intersecção das linhas verticais e horizontais.
ListintersectionPoints = new ArrayList (); for (LineParametric2D_F32 hline : hlines) { for (LineParametric2D_F32 vline : vlines) { Point2D_F32 intersection = Intersection2D_F32.intersection(hline, vline, null); if (intersection.x > 0) { intersectionPoints.add(intersection); } } } Collections.sort(intersectionPoints, new PointComparator(2));
A ordenação deve ser feita de uma forma especial, pois temos as coordenadas x e y de cada ponto, foi preciso criar o PointComparator.
Resultados obtidos com o exemplo Step2.java
Observe na imagem que todos os pontos de intersecção foram encontrados .
O código completo está disponível em Step2.java (do projeto exemplo).
Montar o grid com base nas intersecções
Nessa etapa serão analisadas as intersecções para extrair os retângulos/células da tabela.
Foi criada a função/classe: util.RectangleUtils.find, que montará um retângulo com base nos pontos de intersecção, para em seguida fazer o recorte da célula que contém a assinatura.
O exemplo abaixo mostra como utilizar esse método e como visualizar nosso progresso.
Graphics2D g2 = (Graphics2D) image.getGraphics(); Listcells = RectangleUtils.find(intersectionPoints, vlines.size(), 3); Random rand = new Random(234); for (RectangleLength2D_F32 rect : cells) { g2.setColor(new Color(rand.nextInt())); g2.fillRect((int) rect.x0, (int) rect.y0, (int) rect.width, (int) rect.height); }
Resultado:
O código completo está disponível em Step3.java e util/RectangleUtils.java (do projeto exemplo)
Extrair (sub-imagem) das células das assinaturas
Essa etapa é bem simples, basta usar os retângulos extraídos na etapa anterior e subdividir a imagem:
Listcells = RectangleUtils.find(intersectionPoints, vlines.size(), 3); List images = RectangleUtils.splitImages(image, cells); System.out.println("Total de Imagens: " + images.size()); ListDisplayPanel panel = new ListDisplayPanel(); int count = 0; for (BufferedImage cellimage : images) { panel.addImage(cellimage, count + " : " + cells.get(count).x0 + "x" + cells.get(count).y0); count++; } ShowImages.showWindow(panel, "Imagens");
O ListDisplayPanel ajuda a visualizar uma lista de imagens sem a necessidade de salvar no disco, observe que apenas foi adicionado o BufferedImage (panel.addImage(...)).
Resultado:
Observe que na imagem (19, última) temos uma assinatura.
O código completo está disponível em Step4.java
Verificar a presença da assinatura
Agora que todas as imagens foram separadas, para localizar as assinaturas é só utilizar um algoritmo de Line Segments (FactoryDetectLineAlgs.lineRansac), serão utilizados a quantidade de linhas e o tamanho para identificar se existe ou não uma assinatura
Agora, algumas modificações no exemplo anterior.
As variáveis: int start = 19, interval = 5, ajudam a selecionar apenas as assinaturas.
Listcells = RectangleUtils.find(intersectionPoints, vlines.size(), 3); int start = 19, interval = 5; List assinaturasrect = new LinkedList (); for (int i = start; i < cells.size(); i = i + interval) { assinaturasrect.add(cells.get(i)); } List assinaturas = RectangleUtils.splitImages(image, assinaturasrect); System.out.println("Total de Imagens: " + assinaturas.size()); ListDisplayPanel panel = new ListDisplayPanel(); int count = 0; for (BufferedImage cellimage : assinaturas) { long linesSeg = detectLineSegments(cellimage); panel.addImage(cellimage, count + " : " + linesSeg); count++; } ShowImages.showWindow(panel, "Imagens");
public static long detectLineSegments(BufferedImage image) { ImageFloat32 input = ConvertBufferedImage.convertFromSingle(image, null, ImageFloat32.class); // Config A: // DetectLineSegmentsGridRansacdetector = FactoryDetectLineAlgs.lineRansac(20, 50, 2.36, true, ImageFloat32.class, ImageFloat32.class); // Config B: DetectLineSegmentsGridRansac detector = FactoryDetectLineAlgs.lineRansac(5, 50, 90, true, ImageFloat32.class, ImageFloat32.class); List lines = detector.detect(input); Graphics2D g2 = (Graphics2D) image.getGraphics(); g2.setStroke(new BasicStroke(3)); int scale = 1; long length = 0; for (LineSegment2D_F32 s : lines) { length += s.getLength(); g2.setColor(Color.RED); g2.drawLine((int) (scale * s.a.x), (int) (scale * s.a.y), (int) (scale * s.b.x), (int) (scale * s.b.y)); g2.setColor(Color.BLUE); g2.fillOval((int) (scale * s.a.x) - 1, (int) (scale * s.a.y) - 1, 3, 3); g2.fillOval((int) (scale * s.b.x) - 1, (int) (scale * s.b.y) - 1, 3, 3); } return length; }
O código completo está disponível em Step5.java.
Resultado Etapa5 (para as configurações A e B):
Observe que o resultado B usa o mesmo algoritmo, apenas foi ajustado os parâmetros (modo de tentativa e erro).
Melhorias
Como nem tudo são flores, algumas assinaturas (e principalmente as não preenchidas) contem algumas imperfeições: o recorte pega um pouco das bordas e restos de outras assinaturas.
Isso gerou um problema, pois essas bordas (Imagem 13) foram identificadas como segmentos de linha e ficaram quase do mesmo "tamanho" das assinaturas normais, gerando falsos positivos.
Imagem 13 ampliada:
No processo de "Computação Visual" é necessário aplicar técnicas de pré-processamento da imagem para facilitar a utilização dos algoritmos.
Nessa etapa será desenvolvido um filtro (filter.SimpleBorderFilter) para remoção das bordas da imagem, caso as mesmas existam.
A lógica desse filtro é: analisar os pixels nas bordas da imagem e verificar se existem X pixels escuros, caso exista ele substitui por pixel branco.
Alguns problemas são específicos e precisam de soluções específicas. Então o método detectSegments foi alterado para:
public static long detectSegments( BufferedImage image ) { ImageFloat32 input = ConvertBufferedImage.convertFromSingle(image, null, ImageFloat32.class); new SimpleBorderFilter(3, 180, 10).apply(input, input); // ... }
O código completo está disponível em Step6.java (final).
Resultado:
Observe que todas as assinaturas foram detectadas corretamente.
Conclusão e considerações finais
Agora existe mais uma alternativa para Visão Computacional em Java sem usar JNI, sendo multi plataforma, contendo uma documentação razoável com vários exemplos e podendo ser utilizada inclusive em Android.
Apesar da documentação relativamente boa e ter vários exemplos, nem chega perto da quantidade de material sobre o OpenCV. Recomenda-se primeiro entender o processo de como é feito no OpenCV (através dos exemplos e artigos) e aplicar no Java com o BoofCV, como foi feito em alguns casos nesse exemplo; algumas ideias foram tiradas de tutorias do OpenCV.
Os exemplos desse artigo estão disponíveis no repositório do GitHub.
Sobre o autor
Ricardo Rufino é CEO e fundador da CriativaSoft. Trabalha no desenvolvimento de software desde 2007, focado em soluções corporativas na plataforma JavaEE. Desenvolvedor de softwares de código aberto como Mentawai (Framework Web MVC), ProjectNCode, OpenDevice (Platform IoT) e Arduino IDE.
Mestrado em Gestão de computadores pela UFPE, Engenharia de Software pela CEUT, Processamento de Dados pela AESPI.
Empreendedor e apaixonado por tecnologia, robótica e música. Foco no desenvolvimento de soluções corporativas em plataforma Web com alta qualidade, usabilidade e segurança, atuando principalmente na área de gestão acadêmica.