Распознаём капчу программно

Распознавание простой капчи [gOCR и PHP]

При прохождении одного из заданий на программирование была дана капча, которую нужно было распознать в течении 15 секунд. Вручную ввести данные нереально, поскольку состоит она из 96 символов, алфавитом являются 16-ричные значения. Да, такая вот длинная капча 🙂 Но задание поэтому и в ветке программирование, значит будем решать.

Внешне изображение выглядит так:

КапчаВ достоинство алгоритма генерации можно отнести мелкие символы и их количество. В остальном же разгадать такую не составит труда. Распознавание данной капчи, как и любой другой в виде изображения можно выполнить разными способами. Я решил воспользоваться готовой OCR. Это такая штука, которая большую часть работы делает за нас. Проведя эксперименты с различным софтом предназначенным для оптического распознавания символов, остановился на gOCR. Данная программа существует под все распространённые операционные системы, но что более приятно – есть в репозиториях убунты (на которой построен дистрибутив BackBox).

Установка gOCR

Я привык пользоваться менеджером пакетов APT, поэтому:

apt-get install gocr

Установив гоцр, можно проверить его работоспособность выполнив в терминале gocr <опции> /путь/к/изображению

Поскольку в нашем случае базой являются значения 0-9 и a-f, зададим их через параметр -C.

Распознавание капчи

Неплохо. Успешно распознаны почти все символы за исключением четверки, девятки и нуля, которые я выделил красным. Можно было бы оставить и так, но так дело не пойдёт, оставлять это дело на rand() судьбы плохая затея. Поэтому у нас есть два пути:

  1. обучить gocr для точного распознавания проблемных символов
  2. использовать свой вариант для коррекции

Я выбрал второй путь, поскольку заниматься поиском информации про обучение gOCR’a желания небыло. Выбор пал на PHP и модуль GD.

Вырезав 0, 4 и 9 в отдельные файлы я приступил к быдлокодингу.

Для начала вызовем через shell_exec gocr и переведём символы в массив.

$res = shell_exec("gocr -C 0-9a-f $p1"); // получаем результат
$answer = str_split($res); // разбиваем строку на символы
unset($answer[96]); // удаляем последний пробел

Затем будем проходить по полученной строке, и если ннаткнулись на спорный символ, который может быть 0, 4 или девяткой – вызывем нашу функцию  getChar(позиция)  передав в качестве аргумента позицию символа в строке.

foreach($answer as $k=>$v){
	if($v == '4' OR $v == '9' OR $v == '0') // если встретился нужный символ, вызовем функцию
		getChar($k);
}

И сама функция

function getChar($pos){
	global $p1,$answer;
	$img = imagecreatefromjpeg($p1); // создадим рабочую область
	$size = getimagesize($p1); // определим размер
	$w1 = $size[0]; // ширина капчи
	$h1 = $size[1]; // высота капчи

	$images = array(0,4,9); // символы которые стоит проверить
	foreach($images as $im){
		$p2 = 'chars/'.$im.'.jpg'; // открываем поочередно вырезанные символы из файла (эталон)
		$img2 = imagecreatefromjpeg($p2);
		$size2 = getimagesize($p2);
		$w2 = $size2[0];
		$h2 = $size2[1];

		for ($i=0;$i<$w1;$i++){
			$c = 0;
			for ($j=0;$j<$h1;$j++) {
				$rgb = imagecolorat($img, $i, $j);
				$r = ($rgb >> 16) & 0xFF;
				$g = ($rgb >> 8) & 0xFF;
				$b = $rgb & 0xFF;
				$data[$i][$j] = $r;
			}
		}

		for ($i=0;$i<$w1;$i++){
			$cont = 0;
				for ($j=0;$j<$h1;$j++) {
					if($data[$i][$j] >= 240){
						$cont++;
					}
				}

			if($cont > 0 && $started == false){
				$started = true;
				$begin = $i;
			}
			if($cont == 0){
				if(isset($begin) && $started == true)
					$arr[] = array($begin,$i - 1);
				$started = false;
			}
		}

		$w = $arr[$pos][1] - $arr[$pos][0] + 1;
		$char = imagecreatetruecolor ($w, $h1);
		imagecopy( $char, $img, 0, 0, $arr[$pos][0], 0, $w, $h1);
		for ($i=0;$i<$w;$i++){
			for ($j=0;$j<$h1;$j++) {
				$rgb = imagecolorat($char, $i, $j);
				$image1[$i][$j] = imagecolorsforindex($char, $rgb);
			}
		}
		for ($i=0;$i<$w2;$i++){
			for ($j=0;$j<$h1;$j++) {
				$rgb = imagecolorat($img2, $i, $j);
				$image2[$i][$j] = imagecolorsforindex($img2, $rgb);
			}
		}
		$x = 0;
		$y = 0;
		$test = 0;

		for($x=0; $x<count($image1) - 1; $x++){
			for($y=0;$y<count($image1[$x]);$y++){
				if($image1[$x][$y]['red'] > 200 && $image2[$x][$y]['red'] > 200){
					$test++;
				}
			}	
		}

		$return = '';

		if($test > 8){ // если количество белых точек о обоих изображениях больше 8
			$answer[$pos] = $im; // предположим что у данного символа приоритет выше
		}
		imagedestroy ( $img2); // удаляем мусор, освобождаем ресурсы
		imagedestroy ( $char); // удаляем мусор, освобождаем ресурсы
	}
	imagedestroy ( $img);  // удаляем мусор, освобождаем ресурсы
	return $return;
}

 

Задача функции заключается в сравнении куска изображения, полученного из полной капчи со всеми вырезанными мной символами вручную. Если количество белых пикселей в обоих кусках на одинаковых позициях более 8 штук, то перезаписываем  $answer[$pos] .

Под конец выведем результат после обработки.

foreach($answer as $k=>$v){
		$res2 .= $v;
}
echo $res2;

Таким образом результаты распознавания значительно выросли. Я не проверял работу на многих капчах просто потому, что после первой же отправки данных сайт принял результат как верный. Просмотрев глазами различий не обнаружил, значит работает система хорошо.
Возможно кому-то пригодится.