Время прочтения: 8 мин.

При разработке «распознавателя» паспортов без достаточного количества реальных образцов возникла необходимость генерации паспортных данных и соответствующих им изображений отдельных полей (серия/номер, дата выдачи паспорта и т.д).

Формат паспортных данных

Рассмотрим пример главного разворота паспорта (2 и 3 страницы):

Поля в паспорте нельзя генерировать отдельно друг от друга, они взаимосвязаны. Помимо самых очевидных связей (ФИО зависит от пола, дата выдачи должна быть минимум на 14 лет позже даты рождения), существуют и другие, например:

  • первые 2 цифры серии паспорта — ОКАТО‑код субъекта, где был изготовлен бланк (как правило совпадает с регионом выпуска) или специальная серия 09;
  • первые две цифры кода подразделения — код субъекта, третья цифра принимает значения от 0 до 4;
  • дата выдачи паспорта без МЧЗ (находящаяся внизу 3-ей страницы машиночитаемая запись, на изображении паспорта выделена черной рамкой, подробнее о ней в разделе о генерации изображений) находится в интервале от 1 октября 1997 года по 30 июня 2011 года, с МЧЗ — с 1 июля 2011 года по настоящий день;
  • год печати бланка (последние 2 цифры серии), в большинстве случаев, равен году выдачи паспорта или предыдущему;
  • каждому значению поля «Код подразделения» соответствует до 15 возможных названий подразделения.

Генерация данных

Для генерации использовались следующие данные:

На основе этих данных и выявленных правил генерируется объект data-класс Passport. Сначала случайно выбирается пол и соответствующее ФИО. Вероятность выбора имени, отчества или фамилии рассчитывается с учетом его распространенности.

Затем выбирается дата выдачи в интервале от 1 июля 2011 года (день введения МЧЗ) до настоящего дня. Затем высчитывается день рождения (минимум 14 лет до даты выдачи). Случайным образом выбирается регион, на основе него (и даты выдачи) генерируется серия. Потом среди всех отделений ФМС в выбранном регионе случайно выбирается одно.

import random
import math

def generateData(n =None,batch=False,batchSize = 1000)->Passport|list[Passport]
    # Опущено получение вышеописанных данных (ФИО, вероятности их 
    # выбора, список регионов и отделений ФМС) и функции, генерирующие 
    # отдельные элементы паспорта (серию, номер и др.). Полный код ниже.
    gender = random.choice(["M","F"])
    if gender =="F":
        surname = random.choices(femaleSurnames,fSurnameProb)[0]
        name = random.choices(femaleNames,fNameProb)[0]
        midname = random.choices(femaleMidnames,fMidnameProb)[0]
    else:
        surname = random.choices(maleSurnames,mSurnameProb)[0]
        name = random.choices(maleNames,mNameProb)[0]
        midname = random.choices(maleMidnames,mMidnameProb)[0]
    issueDate = genIssueDate()
    birthday = genBirthday(issueDate)
    # Случайный выбор региона, где был выдан паспорт
    dep = deps.sample(1).to_dict(orient="records")[0] 
    series= genSeries(issueDate,dep)
    dfNeededCode = codes[codes["code"].str.match(f'{str(dep["ГИБДД"])}')]
    neededCode = dfNeededCode.code.to_list()
    selCode = random.choice(neededCode)
    number = genNumber()
    return Passport(surname,name,midname,series,number,
              birthday,issueDate,gender,selCode)

Для быстрой генерации большого количества (свыше 10 тысяч) паспортов в функции был реализован пакетный режим генерации паспортов. Ускорение генерации происходит за счет одновременной генерации пола и ФИО для всего пакета:

batchNum = math.ceil(n/batchSize)
dep= deps.sample(n,replace=True).to_dict(orient="records")
for i in range(batchNum):
    print(f"Batch № {i+1} started")
    gender = random.choice(["M","F"])
    if gender =="F":
        surname = random.choices(femaleSurnames,fSurnameProb,k=batchSize)
        name = random.choices(femaleNames,fNameProb,k=batchSize)
        midname = random.choices(femaleMidnames,fMidnameProb,k=batchSize)
    else:
        surname = random.choices(maleSurnames,mSurnameProb,k=batchSize)
        name = random.choices(maleNames,mNameProb,k=batchSize)
        midname = random.choices(maleMidnames,mMidnameProb,k=batchSize)
    for j in range(batchSize):
        issueDate = genIssueDate()
        birthday = genBirthday(issueDate)
        series= genSeries(issueDate,dep[i*batchSize+j])
        dfNeededCode = codes[codes["code"].str.match(f'{str(dep[i*batchSize+j]["ГИБДД"])}')]
        neededCode = dfNeededCode.code.to_list()
        selCode = random.choice(neededCode)
        number = genNumber()
        passport= Passport(surname[j],name[j],midname[j], series, number,birthday,issueDate,gender,selCode)
        res.append(passport)
        print(f"{i} out of {n},elapsed time={time.time()-start}",flush=True,end="\r")
        if i*batchSize+j+1==n:
            break

В результате получаем реалистичные паспортные данные (кроме места рождения, полный код тут.

Машиночитаемая запись и генерация изображений

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

Наиболее интересным в рамках задачи фрагментом является упомянутая ранее МЧЗ — машиночитаемая зона. Данный формат появился в 80-х годах прошлого века и в настоящее время присутствует в большинстве паспортов различных стран. Он регламентируется международным стандартом ISO/IEC 7501–1 (ИСО/МЭК 7501–1), что позволяет использовать его по всему миру. Наиболее распространенный тип МЧЗ (тип 3), который применяется в паспорте гражданина РФ, состоит из двух строк по 44 символа.

Допустимый алфавит включает в себя символы латиницы, цифры и символ <. Каждому символу русского алфавита соответствует один символ из данного алфавита, что позволяет однозначно преобразовывать полученные паспортные данные в МЧЗ (и обратно).

АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ
ABVGDE2JZIQКLMNOPRSTUFHC34WXY9678

В паспорте гражданина Российской Федерации МЧЗ хранит содержимое полей «Дата выдачи», «Код подразделения», «Фамилия», «Имя», «Отчество», «Пол», «Дата рождения», серию и номер. Таким образом, в МЧЗ нет информации о полях «Паспорт выдан» и «Место рождения». Кроме того, если суммарная длина имени, фамилии и отчества превышает 38 символов, то информация о них хранится в МЧЗ лишь частично. Такая ситуация обрабатывается в функции handleLong:

def handleLong(surname, name, patronymic):
    person = ""
    if len(surname) + len(name) + len(patronymic) > 36:
        if len(surname) > 34:
            person = f"{surname[:34]}<<{name[0]}<{patronymic[0]}"
        elif len(surname) + len(name) >= 36:
            lim = 37 - 2 - len(surname)
            person = f"{surname}<<{name[:lim]}<{patronymic[0]}"
        elif len(surname) + len(name) <= 35:
            limPatr = 39 - 2 - 1 - (len(surname) + len(name))
            person = f"{surname}<<{name}<{patronymic[:limPatr]}"
    elif len(surname) + len(name) + len(patronymic) == 36:
        person = f"{surname}<<{name}<{patronymic}"
    else:
        person = f"{surname}<<{name}<{patronymic}" + "<" * (
            36 - (len(surname) + len(name) + len(patronymic))
        )
        
    return person

Помимо непосредственно паспортных данных в МЧЗ присутствуют 4 контрольные цифры (формально 5, но информация о дате истечения срока действия всегда заполняется символом <, из‑за чего контрольная цифра тоже всегда <). Они рассчитываются по модулю 10 с постоянно повторяющейся весовой функцией 731 731 731… следующим образом:

Этап 1. Слева направо умножить каждую цифру соответствующего цифрового элемента данных на весовой показатель, стоящий в соответствующей последовательной позиции.

Этап 2. Сложить результаты каждого умножения.

Этап 3. Разделить полученную сумму на 10 (модуль).

Этап 4. Остаток деления является контрольной цифрой.

Контрольные цифры рассчитываются для позиций 1 — 9 (серия и номер), 14 — 19 (дата рождения), 29 — 42 (дополнительные элементы данных — последняя цифра серии паспорта, дата выдачи паспорта, код подразделения) и позиций 1–43 (вся МЧЗ, включая предыдущие контрольные цифры).

Зная все это можно получить строковое представление МЧЗ из сгенерированных ранее паспортных данных.

import datetime
import re

def formMRZ(surname: str,name: str,patronymic: str,serie: str | int,number: str | int,birthday: datetime.date,gender: str,issueDate: datetime.date,departament: str | int,) -> tuple[str, str]:
    '''Returns first and second lines in Russian National Passport implementation of MRZ,according to personal data provided'''

    topConst = "PNRUS"
    surname = re.sub("[-, ]", "<", surname)
    name = re.sub("[-, ]", "<", name)
    patronymic = re.sub("[-, ]", "<", patronymic)
    person = handleLong(surname, name, patronymic)
    topRow = topConst + person
    serieNumber = str(serie)[:-1] + str(number)
    birthdayMRZ = dateToString(birthday)
    issueMRZ = dateToString(issueDate)
    lastPart = f"{str(serie)[-1]}{issueMRZ}{departament}"
    checkSum1, checkSum2, checkSum3, finalCheckSum = checkAll(
        [serieNumber, birthdayMRZ, lastPart]
    )
    bottomRow = f"{serieNumber}{checkSum1}RUS{birthdayMRZ}{checkSum2}{gender}<<<<<<<{lastPart}<{checkSum3}{finalCheckSum}"
    return topRow, bottomRow

Для того, чтобы получить изображения на основе текстовых данных, использовался TextRecognitionDataGenerator (trdg) — генератор синтетических данных, используемый для задач оптического распознавания символов. Важным плюсом этого инструмента является возможность добавления любых шрифтов формата.ttf, что позволяет генерировать текст на любом языке. Также он позволяет настраивать размытие, расстояние между символами и тип фонового изображения, чтобы сгенерированные изображения как можно больше соответствовали реальным.

Рассмотрим процесс генерации на примере МЧЗ. Так как она регламентируется международным стандартом, то известен используемый шрифт — OCR type B. Из‑за того, что МЧЗ расположена на 3 (ламинированной) странице паспорта, то на ней могут присутствовать блики. Чтобы отразить это в полученных изображениях была создана папка с реальными изображениями пустой нижней части паспорта, которые будут использоваться в качестве фоновых изображений.

Пример фонового изображения

Для генерации изображений используется класс GeneratorFromStrings из trdg.generators со следующими параметрами:

import pandas as pd
import numpy as np
from PIL import Image
from trdg.generators import GeneratorFromStrings
from mrzCheck import formMRZ,latinize
from genPassportData import generateData

count = 10
passports  = generateData(count)
strings=[]
for passport in passports:
    print(passport)
    first,second=formMRZ(
            latinize(passport.surname),
            latinize(passport.name),
            latinize(passport.patronymic),
            passport.series,
            passport.number,
            passport.birthday,
            passport.gender,
            passport.issueDate,
            passport.codeDep,
        )
    strings.append(first)
    strings.append(second)


generator = GeneratorFromStrings(
    strings, # непосредственно текстовые строки МЧЗ
    count=len(strings), # количество строк
    fonts=["data/ocr-b.ttf"], # регламентированный шрифт
    blur=1, # размытие для симуляции сканированного изображения
    character_spacing=5, # подобранное значение расстояния между символами для размера итогового изображения 580 на 96
    background_type=4, # фоновое изображение берется случайное из папки
    image_dir = 'data/bg' # путь к папке с фоновыми изображениями
)

Так как МЧЗ в паспорте состоит из 2 строк, итоговое изображение собирается из трех частей: сгенерированное изображение первой строки, пустое изображение, сгенерированное изображение второй строки. Для каждой МЧЗ в файл labels.csv для последующего обучения записывается ее истинное значение:

mid= Image.open("data/MRZBack.jpg")
i=1
dfMrz=pd.DataFrame(columns=['Filename', 'Words'])

for img, lbl in generator:
    if lbl[0]=="P": # Проверка на первую строку (она всегда начинается с P)
        first=img
        prev=lbl
    else:
        wholeImg = np.vstack([first,mid.resize(first.size),img.resize(first.size)])
        dfMrz = dfMrz.append({'Filename' : f'{i}.png', 
          'Words' : f"{prev}\n{lbl}",}, ignore_index = True)
        im=Image.fromarray(wholeImg)
        im.save(f'output_images/test/{i}.png')
        i+=1
dfMrz.to_csv('output_images/test/labels.csv')

В результате получаем изображения, подобные сканам нижней части 3 страницы паспорта, содержащей МЧЗ, и которые могут быть использованы для обучения модели:

Пример сгенерированной МЧЗ

Аналогично генерируются изображения серии и номера.

Заключение

Таким образом, в результате применения вышеописанных инструментов, были получены 11 тысяч синтетических изображений МЧЗ, каждой из которых соответствует реалистичные паспортные данные и которые могут быть в дальнейшем использованы для тренировки ML‑моделей.

Главным недостатком является отсутствие генерации места рождения и изображений, кроме серии/номера и МЧЗ. В дальнейшем планируется работа в этом направлении.