Выбрать кадр для OCR: почему самый резкий снимок может оказаться бесполезным

Приложение снимает короткое видео накладной и выбирает самый резкий кадр для OCR. У выбранного снимка действительно чёткие границы, но нижняя строка с итоговой суммой обрезана. Второй кадр немного мягче, зато документ виден целиком. Сравнение одной резкости оптимизировало технический показатель, а нужное поле исчезло ещё до распознавания.

Выбор кадра должен учитывать задачу: видна ли обязательная область, достаточно ли она крупная, нет ли блика или сильной перспективы. Резкость помогает ранжировать уже пригодные варианты. Если сначала выбрать максимум одного общего показателя, резкая рамка стола или обрезанный край могут победить читаемый документ. Полезно разделить обязательные условия и предпочтения.

Сначала проверим видимость области

В мини-примере изображение документа — массив серых пикселей с полосами, похожими на строки. Второй кадр содержит весь документ и слегка размыт. Первый оставлен резким, но его окно обрезает нижнюю часть. Координаты документа в системе выровненной страницы считаем известными от предыдущего этапа. По ним вычисляем долю видимой площади. Это проверка геометрии выбора, OCR здесь не запускается.

import numpy as np
from scipy.ndimage import gaussian_filter, laplace
page = np.ones((64, 64), dtype=float)
for y in range(8, 57, 8):
    page[y:y+2, 8:56] = 0
sharp_crop = page[:40, :].copy()
soft_full = gaussian_filter(page, sigma=0.8)
# Visible windows in the same rectified document coordinates.
windows = {"sharp_crop": (0, 0, 64, 40), "soft_full": (0, 0, 64, 64)}
images = {"sharp_crop": sharp_crop, "soft_full": soft_full}
def coverage(box):
    x1, y1, x2, y2 = box
    return max(0, min(x2, 64)-max(x1, 0))*max(0, min(y2, 64)-max(y1, 0))/(64*64)
quality = {name: {"sharpness": float(laplace(image).var()),
                  "coverage": coverage(windows[name])}
           for name, image in images.items()}
sharpest = max(quality, key=lambda n: quality[n]["sharpness"])
eligible = [n for n in quality if quality[n]["coverage"] >= .95]
selected = max(eligible, key=lambda n: quality[n]["sharpness"]) if eligible else None
print(quality)
print("sharpest", sharpest, "selected", selected)
assert sharpest == "sharp_crop" and selected == "soft_full"
assert quality["sharp_crop"]["coverage"] == .625

Первый кадр выигрывает по дисперсии лапласиана, но показывает лишь 62,5% площади. Проверка видимости исключает его, и остаётся полный кадр. Порог 95% выбран для демонстрации. Для извлечения одной суммы полезнее проверять именно область суммы, чем требовать весь лист: если она видна полностью, обрезанный пустой край может не мешать задаче.

Почему общий показатель резкости обманывает

Лапласиан реагирует на быстрые изменения яркости. Их дают и текст, и шум, и контрастный фон. Считайте показатель внутри выровненной области документа, в одинаковом масштабе и числовом типе. Сравнение исходного 4K кадра с уменьшенной картинкой без нормализации размера смешивает детализацию и резкость. В примере используется float, чтобы фильтр не потерял отрицательные значения на границах.

Затем проверьте дополнительные причины непригодности: блик на обязательном поле, слишком маленькие буквы, сильную перспективу и перекрытие пальцем. Для каждой проверки задайте наблюдаемое условие и сохраните причину отказа. Если все кадры плохие, попросите переснять конкретную область. Выбор «лучшего из плохих» без статуса приведёт к уверенной отправке неполного документа в следующий этап.

Свяжем отбор с результатом OCR

Соберите короткие видеосерии с размеченными нужными полями и сравните несколько правил выбора. Меряйте точность извлечения суммы или номера документа, долю успешных серий и задержку. Хорошая средняя резкость не является конечным результатом. Для серий, где нужное поле видно только в одном кадре, особенно важна проверка того, что алгоритм сохраняет этот кадр среди кандидатов.

Иногда выгодно отправить в OCR два лучших пригодных кадра, затем сопоставить извлечённые поля и их источники. Это увеличивает вычисления, поэтому измеряйте эффект на качестве и стоимости. При конфликте результатов не выбирайте число только по confidence OCR: разные снимки могут показывать разные части документа. Сохраните кадр, область и исходную строку для проверки.

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

Если документ движется, сначала убедитесь, что кандидаты относятся к одной странице. Максимальная резкость на следующем листе не помогает извлечь поле предыдущего. Идентификатор эпизода страницы и проверка смены содержимого должны предшествовать сравнению кадров по качеству.

Для повторной проверки сохраняйте исходное время выбранного кадра. Тогда инженер сможет восстановить соседние варианты и проверить причину их отбраковки.

В курсе вы работаете с Doc-AI, OCR, видеопотоками и контролем задержки сервиса. Следующая задача — собрать отбор пригодного кадра, оценить точность нужных полей и включить результат в воспроизводимый CV-сервис с понятными метриками. Откройте программу платного курса «Computer Vision Engineer: от детекции и Doc-AI до продакшна».

Продолжить на практике

  • Computer Vision Engineer: от детекции и Doc-AI до продакшна ↗

    Практический курс по Computer Vision. Соберёте продакшн-сервис: датасет → обучение (YOLO/RT-DETR, SAM), трекинг, OCR/Doc-AI, веб-приложение (UI+API) и мониторинг. Экспорт в ONNX/TensorRT/OpenVINO, батчинг/квантование под CPU/GPU. Плюс профессиональные блоки: mAP/IoU/Dice и калибровка доверия, QA разметки и аугментации, p95/стоимость, дрейф данных и алерты, guardrails (NMS/правила), A/B-тесты, Triton/gRPC/FastAPI.