Devido ao artigo "Quebrando CAPTCHAs" recebi dezenas de e-mails querendo mais informações de como eu fiz. Um dos pontos mais questionados foi a respeito do filtro de bandeira utilizado. Então resolvi disponibilizar o filtro para quem tiver interesse.
O filtro ainda está na sua versão alpha 😀 e ainda precisa de várias melhorias como a remoção de variáveis desnecessárias, passar oas variáveis da função cosseno por parâmetros, remoção da matrix de conversão (não é necessária) entre outros. Quem quiser colaborar é só avisar.
O primeiro passo foi criar uma interface para implementação de Filtros, pois estou utilizando essa mesma interface para outros filtros.
package br.com.thiagovespa.image.utils.filter; import java.awt.image.BufferedImage; /** * Interface à ser utilizada em todos os filtros de imagem * * @author Thiago Galbiatti Vespa - <a * href="mailto:thiago@thiagovespa.com.br">thiago@thiagovespa.com.br</a> * @version 1.0 * */ public interface ImageFilter { /** * Aplica o filtro à imagem * @param src Imagem original * @return Imagem com o filtro aplicado */ public BufferedImage applyTo(BufferedImage src); }
Após isso criei a implementação do filtro de bandeira. Lembrando novamente que esse é apenas um primeiro protótipo, há algumas melhorias a serem feitas.
package br.com.thiagovespa.image.utils.filter; import java.awt.image.BufferedImage; import java.util.Arrays; /** * Versão inicial do filtro de bandeira. Licenciado sob GPL: * http://www.gnu.org/licenses/gpl.html * * @author Thiago Galbiatti Vespa - <a * href="mailto:thiago@thiagovespa.com.br">thiago@thiagovespa.com.br</a> * @version 0.6 * */ public class FlagFilter implements ImageFilter { private boolean x; private boolean y; /** * Construtor padrão. O efeito é aplicado no eixo x e y */ public FlagFilter() { this.x = true; this.y = true; } /** * Construtor que especifica onde aplicar o filtro * * @param x * se verdadeiro aplica no eixo x * @param y * se verdadeiro aplica no eixo y */ public FlagFilter(boolean x, boolean y) { this.x = x; this.y = y; } @Override public BufferedImage applyTo(BufferedImage src) { int w = src.getWidth(); int h = src.getHeight(); // TODO: Remover uso de matrizes desnecessárias int[][] resultMatrix = new int[w][h]; // Copia para matriz for (int i = 0; i < w; i++) { for (int j = 0; j < h; j++) { int value = src.getRGB(i, j); resultMatrix[i][j] = value; } } int[] transformY = new int[w]; if (this.y) { // Cosseno horizontal for (int i = 0; i < w; i++) { // TODO: Colocar variáveis como parâmetro double x = (i / (double) (w - 1)) * 3 * Math.PI; double valor = 0.27 * (((Math.cos(x) + 1) * ((double) (h - 1) / 2.0))); transformY[i] = (int) valor; } } MinMaxReturn increment = getNormValue(transformY); int[] transformX = new int[h + increment.getDelta()]; int[][] transformMatrix = new int[(w)][h + increment.getDelta()]; for (int i = 0; i < w; i++) { Arrays.fill(transformMatrix[i], 0xffffff); for (int j = 0; j < h; j++) { int desloc = (j - transformY[i]); transformMatrix[i][increment.getSum() + desloc] = resultMatrix[i][j]; } } if (this.x) { // Cosseno vertical for (int i = 0; i < h + increment.getDelta(); i++) { // TODO: Colocar variáveis como parâmetro double y = (i / (double) (h - 1)) * 1.5 * Math.PI + 16; double valor = (((2 * Math.cos(y) + 1) * ((double) (w - 1) / 2.0)) * 0.07) + 7; transformX[i] = (int) valor; } } MinMaxReturn incrementX = getNormValue(transformX); int[][] transform2Matrix = new int[(w + incrementX.getDelta())][h + increment.getDelta()]; for (int i = 0; i < transform2Matrix.length; i++) { Arrays.fill(transform2Matrix[i], 0xffffff); } for (int j = h + increment.getDelta() - 1; j >= 0; j--) { for (int i = w - 1; i >= 0; i--) { int desloc = (i - transformX[j]); transform2Matrix[incrementX.max + desloc][j] = transformMatrix[i][j]; } } int[] resultImage = new int[(w + incrementX.getDelta()) * (h + increment.getDelta())]; // Matrix de conversão for (int i = 0; i < transform2Matrix.length; i++) { for (int j = 0; j < transform2Matrix[0].length; j++) { resultImage[j * (transform2Matrix.length) + i] = transform2Matrix[i][j]; } } BufferedImage dest = new BufferedImage(transform2Matrix.length, transform2Matrix[0].length, BufferedImage.TYPE_INT_RGB); dest.setRGB(0, 0, transform2Matrix.length, transform2Matrix[0].length, resultImage, 0, transform2Matrix.length); return dest; } class MinMaxReturn { int min; int max; int getDelta() { return max - min; } int getSum() { return max + min; } } // TODO: Colocar esse método em classe utilitária private MinMaxReturn getNormValue(int[] numbers) { MinMaxReturn ret = new MinMaxReturn(); ret.max = numbers[0]; ret.min = numbers[0]; for (int i = 1; i < numbers.length; i++) { if (numbers[i] > ret.max) { ret.max = numbers[i]; } if (numbers[i] < ret.min) { ret.min = numbers[i]; } } return ret; } }
Agora para utilizar é só instanciar o filtro e chamar o método applyTo passando a imagem a ser alterada.
ImageFilter flagFilter = new FlagFilter(true,true); BufferedImage flaggedImage = flagFilter.applyTo(ci.getImage());
Fiz outro teste com a bandeira do Brasil.
Aplicando somente ao eixo x temos o seguinte:
Aplicando somente ao eixo y temos o seguinte:
E aplicando aos dois eixos temos o seguinte:
Agora é só utilizar no seu projeto e se fizer algum melhoria no código é só me avisar que atualizo o post.
Thiago, boa tarde. Obrigado mais uma vez o que você fez e o que já tem publicado. Gostaria de saber que objeto é este que você aplicou o filtro. BufferedImage flaggedImage = flagFilter.applyTo(ci.getImage());
O que é este ci da linha ci.getImage()?
Abraço. Obrigado.
É um objeto do tipo java.awt.Image.
Você pode criá-lo utilizando o código descrito nessa post: http://www.thiagovespa.com.br/blog/2010/09/26/quebrando-captchas/
Thiago, bom noite. blz.
Eu não entendi bem, porque você colocou uma matriz unidimensional recebendo uma matriz bidimensional.
resultImage[j * (transform2Matrix.length) + i] = transform2Matrix[i][j];
Obrigado.
Porque o método setRGB recebe uma matriz unidimensional (array int[]) e não uma matriz bidimensional.
http://docs.oracle.com/javase/6/docs/api/java/awt/image/BufferedImage.html#setRGB%28int,%20int,%20int,%20int,%20int%5B%5D,%20int,%20int%29
Você pode verificar pelo método como acessar o valor do pixel da matriz RGB utilizando as posições x e y.
pixel = rgbArray[offset + (y-startY)*scansize + (x-startX)];
Esse foi o único motivo dessa conversão... Poderia ter trabalhado direto com o array unidmensional, para otimizar o uso da variável, mas acho mais fácil trabalhar com a matriz bidimensional, mas como o parâmetro do método setRGB não aceita, tive que realizar a conversão.
Prezado Thiago,
Muito bom este post, sobre a forma como você acertou o efeito bandeira. Teria como por favor, você mandar mais material, sobre a formula do coseno, no sentido de enteder melhor a partir de qual posiçao você começa a alterar a imagem? Muito obrigado. Wagner
MUito bom o seu post, como eu poderia lhe sugerir um produto que estou imaginando.
Qual seria o seu email ?
Cara , meus parabéns pelo tutorial , apliquei ele em uma aplicação que fiz e ficou muito massa ... muito bom mesmo grande Mestre
Olá Thiago, você sabe qual o quais comando(s) poderia usar para quebrar o captcha deste link?
eu utilizei o threadshold de 100 e agora tenho que corrigir as letras.
estou utilizando o imagemagick.
link do captcha:
http://consultanumero.abrtelecom.com.br/consultanumero/consulta/consultaSituacaoAtualCtg
obrigado!
Pra corrigir as letras vai ser um pouco complicado. Eu tentaria traçar uma curva com os pontos inferiores (ou médio talvez) das letras e deslocaria os pixels igual feito no filtro de bandeira.