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.