Реализация распознавания текста с фотографий документов
Фотография документа — не скан. Типичные проблемы: перспективные искажения, тени от пальцев и переплёта, неравномерное освещение, смазанность при движении, отражения на глянцевых поверхностях. Качественная система распознавания должна уметь исправлять все эти искажения до передачи изображения в OCR-движок.
Полный пайплайн обработки фото документа
import cv2
import numpy as np
from paddleocr import PaddleOCR
class DocumentPhotoOCR:
def __init__(self):
self.ocr = PaddleOCR(use_angle_cls=True, lang='ru', use_gpu=True)
def process(self, image_path: str) -> dict:
image = cv2.imread(image_path)
# 1. Детекция документа в кадре
doc_corners = self.detect_document(image)
# 2. Перспективная коррекция
if doc_corners is not None:
image = self.four_point_transform(image, doc_corners)
# 3. Улучшение качества изображения
image = self.enhance_document(image)
# 4. OCR
result = self.ocr.ocr(image, cls=True)
return {
'text': self._extract_text(result),
'words': self._extract_words_with_positions(result),
'corrected': doc_corners is not None
}
Детекция и выравнивание документа
def detect_document(self, image: np.ndarray) -> np.ndarray | None:
"""Нахождение углов документа для перспективной коррекции"""
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
edges = cv2.Canny(blur, 75, 200)
# Расширяем для соединения разрывов
dilated = cv2.dilate(edges, np.ones((3, 3)), iterations=1)
contours, _ = cv2.findContours(dilated, cv2.RETR_LIST,
cv2.CHAIN_APPROX_SIMPLE)
contours = sorted(contours, key=cv2.contourArea, reverse=True)[:5]
for contour in contours:
peri = cv2.arcLength(contour, True)
approx = cv2.approxPolyDP(contour, 0.02 * peri, True)
if len(approx) == 4:
area_ratio = cv2.contourArea(approx) / (image.shape[0] * image.shape[1])
if area_ratio > 0.2: # документ занимает > 20% кадра
return approx.reshape(4, 2)
return None
def four_point_transform(self, image: np.ndarray,
pts: np.ndarray) -> np.ndarray:
"""Перспективная трансформация по 4 точкам"""
rect = self._order_points(pts)
tl, tr, br, bl = rect
# Вычисляем размеры результирующего изображения
width = int(max(np.linalg.norm(br - bl), np.linalg.norm(tr - tl)))
height = int(max(np.linalg.norm(tr - br), np.linalg.norm(tl - bl)))
dst = np.array([
[0, 0], [width - 1, 0],
[width - 1, height - 1], [0, height - 1]
], dtype='float32')
M = cv2.getPerspectiveTransform(rect.astype('float32'), dst)
return cv2.warpPerspective(image, M, (width, height))
Коррекция освещения и теней
def enhance_document(self, image: np.ndarray) -> np.ndarray:
"""Адаптивное улучшение для неравномерного освещения"""
# Разбиваем на каналы LAB
lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)
# CLAHE для яркостного канала
clahe = cv2.createCLAHE(clipLimit=4.0, tileGridSize=(16, 16))
l_enhanced = clahe.apply(l)
enhanced = cv2.merge([l_enhanced, a, b])
enhanced = cv2.cvtColor(enhanced, cv2.COLOR_LAB2BGR)
# Дополнительное sharpen для размытого текста
kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]])
sharpened = cv2.filter2D(enhanced, -1, kernel)
return sharpened
Работа с отражениями (глянцевые документы)
Ламинированные удостоверения и карты создают блики. Решение: поляризационный фильтр на камеру (hardware) или алгоритмически — inpainting зон с пересвеченными пикселями (значения > 250 в RGB) с последующей интерполяцией из соседних областей.
Качество распознавания на реальных фото
| Условия съёмки | CER (PaddleOCR) |
|---|---|
| Хорошее освещение, фронтальный снимок | 2–5% |
| Небольшой наклон (до 20°) | 3–7% |
| Тень на части документа | 8–15% |
| Размытие движения | 15–30% |
| После перспективной коррекции | -3–5% к CER |
| Задача | Срок |
|---|---|
| OCR с базовой предобработкой | 1–2 недели |
| Полный пайплайн с детекцией документа | 3–4 недели |
| Мобильное приложение с live preview | 5–7 недель |







