Уже не помню, как я наткнулся на статью habr.com/ru/post/464337, но она запала мне в мозг и не давала покоя вплоть до минувшего дня. Несколько раз я пытался понять происходящее, пару раз пытался заставить это работать, но безрезультатно: я совершенно ничего не понимаю в нейронных сетях и даже программирую не как настоящий программист.
счастливая капча


   Наконец, несколько дней назад я осилил запуск питона и решил, а почему бы и не да и всё такое. Забыв всё, что я прочитал в упомянутой статье, пошёл своим путём.
   Вспоминая несметное количество решённых капч, я предположил, что можно решать их банальным сравнением с маской, что и подтвердилось впоследствии.
   Во-первых, вручную собрал тестовые капчи (83 штуки) и дал им очевидные имена. Скриптом превратил их в битовые изображения.

   Цифры в капчах бывают двух размеров по высоте с разницей в 1 пиксель и трёх-четырёх начертаний по ширине. Базовая линия всех символов во всех капчах одинаковая. Всё это разнообразие, как оказалось, имеет некую общую маску, сравнение с которой однозначно идентифицирует цифру. Вырезал по нескольку (сначала – по 5, потом добавлял ещё по 1-2; с «4» провозился дольше остальных) одинаковых цифр из разных капч. В paint.net наложил их друг на друга и получил общую для всех начертаний каждой цифры маску.

   Единственную проблему обнаружил позднее, уже при массовой обработке, но успешно её обошёл
при помощи костыля
   Первоначально, распознавание шло по порядку — по исходному образу прогонялась маска «1», потом «2» и т.д. до «9». Оказалось, что в некоторых случаях, когда толстая линия шума накладывается на ножку «4», то одинаково успешно распознаются и «4», и «1». Пришлось, во-первых, изменить порядок применения масок с «123456789» на «423156789» и во-вторых, при успешном распознавании «4» заливать это место белым, чтобы гарантированно исключить «1».

   Кроме этого небольшого недоразумения шум совершенно не мешает. Итогом этого этапа стал набор из 9 масок. Два вложенных цикла и вуаля! – все мои 83 капчи распознаются на ура!

   

   Дальше встал вопрос: где взять большой набор капч для проверки. И я скачал «29 000 капч» из упомянутой статьи.
Однако, это оказалось пустой тратой времени.
   Во-первых (точнее, во-вторых, т.к. я обнаружил уже позднее), там присутствуют идентичные файлы: один и тот же файл сохранён под разными именами: 6503 раза, 5420 раз, 760 и т.д. – т.е. всего уникальных файлов 14882, что, впрочем, тоже немало.
Во-вторых, а на самом деле – во-первых, – это не настоящие капчи. Сайт отдаёт картинку в формате PNG, а в наборе – JPG, причём крайне плохого качества, причём со сдвигом. Могу предположить, что именно такова была цель автора – статья же недаром называется «”зашумленная” капча».

   Так что пришлось расчехлить гугл и самостоятельно намайнить идеальных капч: за ночь набралось 3224 файла, в том числе 49 абсолютно пустых, как выяснилось позднее. Cпасибо Ганеше за код.

   Собственно распознавание капчи укладывается в 26 строк скучного кода на питоне. Из внешних модулей нужен только PIL. Скорость работы – примерно 1000 капч в минуту (одна тысяча капч в минуту) на стареньком Core 2 «четыре ядра четыре гига». На более приличном восьмипоточном i5 заметно быстрее, хотя дело, конечно, не в потоках.    Распознавание 100% или очень к тому близко: выборочная проверка не показала ошибок.

   Конечно, всё это не интересно в смысле нейронных сетей и прочих блокчейнов, но имеет совершенно определённое преимущество перед предложенным ранее вариантом: скорость и точность. Так же верно и то, что любое изменение параметров капчи – гарнитуры или размера шрифта, вид шума и т.д. – приведёт к полной неработоспособности моего решения.

Скачать архив с капчами с Яндекс.Диска (14МБ).

Исходный код
from PIL import Image, ImageTk

def recognize(filepath):
  Zlist = [] # [(x1, z1), (x2, z2), (x3, z3), etc.] - position and digit
  captcha = ""
  originalimage = Image.open(filepath).convert('L').point(lambda x : 255 if x > 20 else 0, mode='1').convert('1').convert('RGBA')
  if originalimage.getextrema() == ((0, 0), (0, 0), (0, 0), (255, 255)):
    return("empty image")
  for z in [4, 2, 3, 1, 5, 6, 7, 8, 9]: # reorder to exclude false 1 on 4
    mask = Image.open('mask' + str(z) + '.png').convert('RGBA')
    previ = 0
    for i in range(15, 120): # no digit in left part
      resultimage = Image.alpha_composite(originalimage.crop((i, 0, i + 30, 0 + 50)), mask)
      if resultimage.getextrema() == ((0, 0), (0, 0), (0, 0), (255, 255)):
        if z == 4: # delete 4 to exclude false 1 on 4
          maskx = Image.open('mask4x.png').convert('RGBA') 
          originalimage.paste(Image.alpha_composite(originalimage.crop((i, 0, i + 30, 0 + 50)), maskx), (i, 0))
        if previ == 0 or i > previ + 15: #no digit closer then 15 px
          Zlist.append((i, z))
          if len(Zlist) == 5:
              Zlist.sort()
              for z in Zlist:
                captcha = captcha + str(z[1])
              return(captcha)
          previ = i
          i = i + 15 #skip a little
  Zlist.sort()
  return(str(Zlist)) #if less then 5 digits recognized
	
def main():
  captcha = recognize(entry.path)
#----------------------------------------------#
#  в архиве полный код для массовой обработки  #
#----------------------------------------------#

main()




Дополнение от 13.02.2020.
   Ради чего всё затевалось? Не ради же спортивного распознавания сохранённых картинок? Нет, всё это было исключительно в прагматических целях.
   Готовое решение для работы. — локальный http-сервер распознавания плюс расширение для Chrome.
   Пока единственное, что оно умеет (я надеюсь, что умеет) — автоматически вставлять капчу в нужное место. В планах:
  — очистить интерфейс сайта, оставив необходимый минимум;
  — автоматизировать обновление капчи при просмотре сведений, т.к. одна капча даёт возможность открыть всего 4 объекта.
  — загружать сразу все готовые выписки, а не по одной.