keyworddiscovery captcha

Dziś kolejne “łamanie” captcha – tym razem coś ciekawszego, bo nie będziemy wykorzystywali ocr. Zrobimy proste rozpoznawanie liter na podstawie dopasowania do wzorcow. Proste, niesamowicie skuteczne – dokładnie pokazujące wszystkie niedoskonałości captcha.

Co prawda obrazki pochodzą z keyworddiscovery.com, ale widziałem tego typu captcha na wielu innych stronach. Dla wszystkich można odczytać tekst z obrazka poniższym skryptem.

Pobierz pliki źródłowe.

Zasada odczytywania jest bardzo prosta. Każda generowana litera ma równą szerokość i wysokość (mieści się w okreslonym prostokącie), położone są zawsze w jednym miejscu i mają jeden kolor.

Na obrazku występują trzy kolory: biały, niebieski i granatowy. Interesuje nas tylko granatowy – więc będziemy “wyciągali” piksele, w których barwa czerwona nieprzekracza 32 (w skali 0-255). Z tak zmodyfikowanego obrazka możemy wyciąć poszczególne litery – zamienimy je na tablice zer i jedynek.

Teraz pozostaje już tylko porównanie tablic z tablicami wzorców liter. Poniżej przykładowy wzorzec dwóch liter:

<?php
$pLetters[ '6' ] = array(
'0000000001111000',
'0000000011111000',
'0000000011110000',
'0000000111110000',
'0000000111100000',
'0000001111100000',
'0000001111000000',
'0000011111000000',
'0000011110000000',
'0000111110000000',
'0000111100000000',
'0001111100000000',
'0001111101000000',
'0011111111111000',
'0011111111111100',
'0111111111111110',
'0111111111111110',
'0111110000111110',
'0111100000011111',
'0111100000011111',
'0111100000001111',
'0111100000011111',
'0111100000011110',
'0111110000111110',
'0011111111111110',
'0011111111111100',
'0001111111111000',
'0000111111110000',
'0000000110000000',
'0000000000000000',
'0000000000000000',
'0000000000000000',
);
$pLetters[ 'M' ] = array(
'1111000000011111',
'1111100000011111',
'1111100000011111',
'1111100000111111',
'1111110000111111',
'1111110000111111',
'1111110001111111',
'1111110001111111',
'1111111001111111',
'1111111001111111',
'1111111011111111',
'1111111111111111',
'1111011111101111',
'1111011111101111',
'1111001111001111',
'1111001111001111',
'1111001111001111',
'1111000110001111',
'1111000110001111',
'1111000110001111',
'1111000110001111',
'1111000000001111',
'1111000000001111',
'1111000000001111',
'1111000000001111',
'1111000000001111',
'1111000000001111',
'1111000000001111',
'1111000000001111',
'0000000000000000',
'0000000000000000',
'0000000000000000',
);

Porównanie jest proste – dla każdej pasującej pary (1=1 lub 0=0) zwiększamy licznkik poprawnych dopasowań, dla rozbieżności zwiększamy licznik rozbieżności. Dla każdej litery obliczamy procentowe dopasowanie. Spośród wszystkich wzorców wybieramy ten, którego procent dopasowania jest najwyższy. Zwracamy nazwę znalezionej litery/liczby. Powtarzamy czynność czterokrotnie i mamy odczytany tekst.

<?php
	/**
	 * Porównuje tablice 0 i 1 znaków zwracając procent odpowiadających sobie znaków
	 *
	 * @param array of array $aOne (oryginał)
	 * @param array of array $aTwo
	 * @return double % zgodności
	 */
	protected function Compare( $aOne, $aTwo )
	{
		$pWidth  = max( count( reset( $aOne ) ), count( reset( $aTwo ) ) );
		$pHeight = max( count( $aOne ), count( $aTwo ) );
 
		$aDebug = 0;
		$pSumOK = 0;
		$pSumBad = 0;
 
		static $pE =  0;
		$pE++;
 
		$pOrigOK = 0;
		for( $i = 0; $i < $pWidth; $i++ ) {
			$pOrigOK += array_sum( $aOne[ $i ] );
			for( $j = 0; $j < $pHeight; $j++ ) {
				if ( isset( $aOne[ $i ] ) && isset( $aOne[ $i ][ $j ] ) && isset( $aTwo[ $i ] ) && isset( $aTwo[ $i ][ $j ] ) &&
					$aOne[ $i ][ $j ] == 1 && $aTwo[ $i ][ $j ] == 1 )
									$pSumOK++;
				else
					if ( isset( $aOne[ $i ] ) && isset( $aOne[ $i ][ $j ] ) && $aOne[ $i ][ $j ] == 1 ) {
						if ( !isset( $aTwo[ $i ] ) || !isset( $aTwo[ $i ][ $j ] ) || $aTwo[ $i ][ $j ] == 0 )
							$pSumBad++;
					} else if ( isset( $aTwo[ $i ] ) && isset( $aTwo[ $i ][ $j ] ) && $aTwo[ $i ][ $j ] == 1 )
						if ( !isset( $aOne[ $i ] ) || !isset( $aOne[ $i ][ $j ] ) || $aOne[ $i ][ $j ] == 0 )
							$pSumBad++;
			}
		}
 
		$pRet = ( $pSumOK < $pSumBad ? 0 : ( $pSumOK - $pSumBad ) / $pOrigOK );
		if ( $aDebug )
			echo( '+' . $pSumOK . '-' . $pSumBad . '/ (' . $pWidth . '*'. $pHeight . ')' ) . '=' . $pRet . '<br />';
		return '' . $pRet;
	}

Trochę o kodzie. Nie jest on optymalny, wyszukałem go w archiwach – wg daty modyfikacji pliku powstał trzy lata temu :-) Przede wszystkim czyszczenie obrazka powinno w tym przypadku odbywać się “na literach” – a nie na obrazku (i tak wiemy, gdzie są litery). Kod jest dość niechlujny – wyciąłem go z większego skryptu odpytywania keyworddiscovery o najlepsze słowa kluczowe – doszedłem do wniosku, że nie warto go poprawiać – idee zrozumiecie, a nie jest to “gotowy program” (choć działa i dla zamieszczonych 6 przykładowych obrazków skuteczność jest 100%). Zauważyłem też pewne niezgodności w literach – najprawdopodobniej zmienił się sposób renderowania czcionek, albo sama czcionka – nie chciałem też poświęcać czasu na sprawdzenie wszystkich nowych literek. Traktujcie to wszystko raczej jako koncept, niż gotowe do wdrożenia rozwiązanie – choć działające i niezwykle skuteczne.