Реализация AI-извлечения данных из чеков и квитанций

Проектируем и внедряем системы искусственного интеллекта: от прототипа до production-ready решения. Наша команда объединяет экспертизу в машинном обучении, дата-инжиниринге и MLOps, чтобы AI работал не в лаборатории, а в реальном бизнесе.
Показано 1 из 1 услугВсе 1566 услуг
Реализация AI-извлечения данных из чеков и квитанций
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы
Направления AI-разработки
Этапы разработки AI-решения
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1218
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    853
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1047
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    561
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    825

AI-извлечение данных из кассовых чеков

Чеки — самый сложный вид документов для OCR: термобумага со временем выгорает, качество печати низкое, форматы у каждого ритейлера свои, структура чека нелинейная (позиции, скидки, итоги могут идти в любом порядке). На чеках SROIE-датасете LayoutLMv3 даёт F1 0.974 — но это чистые, сканированные документы. На мобильных фотографиях реального качества — 0.87–0.91.

Предобработка: основной источник ошибок

Мобильная фотография чека страдает от: перспективного искажения, смазанности, тени от пальцев, пересветов. Без исправления OCR ошибается на 15–25% символов.

import cv2
import numpy as np
from PIL import Image

def preprocess_receipt_photo(
    image: np.ndarray,
    target_width: int = 768   # ширина нормализованного чека
) -> np.ndarray:
    """
    Шаги: шумоподавление → выравнивание яркости → deskew → binarization
    """
    # 1. Шумоподавление
    denoised = cv2.fastNlMeansDenoisingColored(
        image, h=10, hColor=10,
        templateWindowSize=7, searchWindowSize=21
    )

    # 2. CLAHE для выравнивания освещённости
    lab = cv2.cvtColor(denoised, cv2.COLOR_BGR2LAB)
    clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
    lab[:, :, 0] = clahe.apply(lab[:, :, 0])
    enhanced = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)

    # 3. Автоматическое определение контура чека и deskew
    gray = cv2.cvtColor(enhanced, cv2.COLOR_BGR2GRAY)
    edges = cv2.Canny(gray, 50, 150)
    contours, _ = cv2.findContours(
        edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )

    if contours:
        # Находим самый большой контур — предположительно чек
        largest = max(contours, key=cv2.contourArea)
        hull = cv2.convexHull(largest)

        if cv2.contourArea(hull) > 0.1 * image.shape[0] * image.shape[1]:
            rect = cv2.minAreaRect(hull)
            angle = rect[2]
            if abs(angle) > 5:
                M = cv2.getRotationMatrix2D(
                    (image.shape[1]//2, image.shape[0]//2), angle, 1
                )
                enhanced = cv2.warpAffine(
                    enhanced, M, (image.shape[1], image.shape[0])
                )

    # 4. Адаптивная бинаризация для термопечати
    gray = cv2.cvtColor(enhanced, cv2.COLOR_BGR2GRAY)
    binary = cv2.adaptiveThreshold(
        gray, 255,
        cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
        cv2.THRESH_BINARY,
        blockSize=11, C=2
    )

    # 5. Нормализация ширины
    h, w = binary.shape
    scale = target_width / w
    resized = cv2.resize(
        binary, (target_width, int(h * scale)),
        interpolation=cv2.INTER_LANCZOS4
    )

    return resized

Парсинг структуры чека

После OCR нужно разобрать структуру: строки товаров, скидки, итоги, кассир, ИНН.

import re
from dataclasses import dataclass, field
from typing import Optional

@dataclass
class ReceiptLineItem:
    name: str
    quantity: float
    unit_price: float
    total_price: float
    discount: float = 0.0

@dataclass
class ParsedReceipt:
    store_name: Optional[str]
    inn: Optional[str]          # ИНН продавца
    datetime_str: Optional[str]
    items: list[ReceiptLineItem] = field(default_factory=list)
    subtotal: Optional[float] = None
    tax_amount: Optional[float] = None
    total: Optional[float] = None
    payment_method: Optional[str] = None
    fiscal_sign: Optional[str] = None

class ReceiptParser:
    # Паттерны для российских чеков (ФФД 1.05/1.1)
    PATTERNS = {
        'inn': r'ИНН\s*[:№]?\s*(\d{10,12})',
        'total': r'(?:ИТОГО|ИТОГО К ОПЛАТЕ|ИТОГ)\s*[:=]?\s*([\d\s,\.]+)',
        'tax': r'(?:НДС|В т\.ч\. НДС)\s+\d+%\s*[:=]?\s*([\d\s,\.]+)',
        'fiscal_sign': r'ФП\s*[::]?\s*(\d+)',
        'line_item': r'^(.+?)\s+([\d,\.]+)\s*[xх×]\s*([\d,\.]+)\s*=?\s*([\d,\.]+)',
        'datetime': r'(\d{2}[\.\/]\d{2}[\.\/]\d{4})\s+(\d{2}:\d{2}(?::\d{2})?)',
    }

    def parse(self, ocr_text: str) -> ParsedReceipt:
        lines = ocr_text.split('\n')
        receipt = ParsedReceipt(
            store_name=lines[0].strip() if lines else None,
            inn=self._extract(ocr_text, 'inn'),
            datetime_str=self._extract_datetime(ocr_text),
            total=self._parse_amount(self._extract(ocr_text, 'total')),
            tax_amount=self._parse_amount(self._extract(ocr_text, 'tax')),
            fiscal_sign=self._extract(ocr_text, 'fiscal_sign')
        )

        for line in lines:
            item = self._parse_line_item(line)
            if item:
                receipt.items.append(item)

        return receipt

    def _extract(self, text: str, key: str) -> Optional[str]:
        m = re.search(self.PATTERNS[key], text, re.IGNORECASE)
        return m.group(1).strip() if m else None

    def _extract_datetime(self, text: str) -> Optional[str]:
        m = re.search(self.PATTERNS['datetime'], text)
        if m:
            return f'{m.group(1)} {m.group(2)}'
        return None

    def _parse_amount(self, text: Optional[str]) -> Optional[float]:
        if not text:
            return None
        cleaned = re.sub(r'\s', '', text).replace(',', '.')
        try:
            return float(cleaned)
        except ValueError:
            return None

    def _parse_line_item(self, line: str) -> Optional[ReceiptLineItem]:
        m = re.match(self.PATTERNS['line_item'], line.strip())
        if not m:
            return None
        try:
            return ReceiptLineItem(
                name=m.group(1).strip(),
                quantity=float(m.group(2).replace(',', '.')),
                unit_price=float(m.group(3).replace(',', '.')),
                total_price=float(m.group(4).replace(',', '.'))
            )
        except (ValueError, AttributeError):
            return None

Сравнение подходов

Подход CER (хорошее фото) CER (плохое фото) Скорость Стоимость
Tesseract 5 (без препроц.) 4.2% 18.7% 200ms Бесплатно
Tesseract 5 + preprocessing 1.8% 8.3% 350ms Бесплатно
PaddleOCR v4 0.9% 4.1% 280ms Бесплатно
Azure Read API 0.6% 2.8% 1.2s $1.5/1000
GPT-4V 0.4% 1.9% 3–5s $10+/1000

Сроки

Задача Срок
Парсер для конкретной сети магазинов 1–2 недели
Универсальный парсер (100+ форматов) 4–6 недель
Мобильное приложение с real-time OCR чеков 6–10 недель