AI Paper Archive Digitization Implementation

We design and deploy artificial intelligence systems: from prototype to production-ready solutions. Our team combines expertise in machine learning, data engineering and MLOps to make AI work not in the lab, but in real business.
Showing 1 of 1 servicesAll 1566 services
AI Paper Archive Digitization Implementation
Complex
~2-4 weeks
FAQ
AI Development Areas
AI Solution Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822

Разработка AI для оцифровки бумажных архивов

Оцифровка архивов — задача от массовой обработки стандартных документов до сложной реставрации исторических рукописей. AI автоматизирует цепочку: сканирование → предобработка изображений → OCR → структурирование → индексация. Типичный объём промышленного архива: сотни тысяч страниц, которые вручную обрабатываются годами. AI-pipeline сокращает время в 10–50 раз при сопоставимом качестве.

Предобработка отсканированных документов

import cv2
import numpy as np
from PIL import Image, ImageEnhance

class DocumentPreprocessor:
    """
    Предобработка влияет на качество OCR кардинально:
    правильная бинаризация может поднять accuracy с 70% до 95%.
    """
    def preprocess_scanned_page(self, image: np.ndarray,
                                  dpi: int = 300) -> np.ndarray:
        """
        Полный pipeline предобработки страницы.
        DPI 300 минимум для OCR, 400+ для мелкого текста.
        """
        # 1. Выравнивание наклона (deskew)
        deskewed = self._deskew(image)

        # 2. Удаление теней и неравномерного освещения
        shadowless = self._remove_shadows(deskewed)

        # 3. Адаптивная бинаризация (Sauvola)
        binary = self._binarize_sauvola(shadowless)

        # 4. Удаление шума и артефактов (пятна, пыль)
        cleaned = self._remove_noise(binary)

        # 5. Нормализация размера
        if dpi != 300:
            scale = 300 / dpi
            h, w = cleaned.shape[:2]
            cleaned = cv2.resize(cleaned, (int(w*scale), int(h*scale)),
                                 interpolation=cv2.INTER_AREA)
        return cleaned

    def _deskew(self, image: np.ndarray) -> np.ndarray:
        """Коррекция угла наклона через Hough Transform"""
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) == 3 else image
        edges = cv2.Canny(gray, 50, 150, apertureSize=3)
        lines = cv2.HoughLines(edges, 1, np.pi/180, threshold=200)

        if lines is None:
            return image

        angles = []
        for line in lines[:20]:  # берём только самые явные линии
            rho, theta = line[0]
            angle = np.degrees(theta) - 90
            if abs(angle) < 45:
                angles.append(angle)

        if not angles:
            return image

        median_angle = np.median(angles)
        if abs(median_angle) < 0.5:  # незначительный наклон — пропускаем
            return image

        h, w = image.shape[:2]
        M = cv2.getRotationMatrix2D((w/2, h/2), median_angle, 1.0)
        return cv2.warpAffine(image, M, (w, h),
                               flags=cv2.INTER_CUBIC,
                               borderMode=cv2.BORDER_REPLICATE)

    def _remove_shadows(self, image: np.ndarray) -> np.ndarray:
        """Устранение теней через CLAHE + морфология"""
        if len(image.shape) == 3:
            lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
            l, a, b = cv2.split(lab)
        else:
            l = image

        clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
        l_enhanced = clahe.apply(l)

        # Вычитание градиента освещённости
        kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (50, 50))
        bg = cv2.morphologyEx(l_enhanced, cv2.MORPH_DILATE, kernel)
        normalized = cv2.divide(l_enhanced, bg, scale=255)

        if len(image.shape) == 3:
            return cv2.cvtColor(cv2.merge([normalized, a, b]), cv2.COLOR_LAB2BGR)
        return normalized

    def _binarize_sauvola(self, image: np.ndarray,
                           window_size: int = 25,
                           k: float = 0.2) -> np.ndarray:
        """
        Sauvola бинаризация: лучше Otsu для неравномерного фона.
        window_size=25 — оптимум для большинства текстовых документов.
        """
        from skimage.filters import threshold_sauvola
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) == 3 else image
        thresh = threshold_sauvola(gray, window_size=window_size, k=k)
        binary = (gray > thresh).astype(np.uint8) * 255
        return binary

    def _remove_noise(self, binary: np.ndarray,
                       min_blob_area: int = 20) -> np.ndarray:
        """Удаление мелких артефактов (пыль, царапины)"""
        # Находим связные компоненты
        num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(
            binary, connectivity=8
        )
        # Удаляем компоненты меньше min_blob_area пикселей
        clean = np.zeros_like(binary)
        for i in range(1, num_labels):
            if stats[i, cv2.CC_STAT_AREA] >= min_blob_area:
                clean[labels == i] = 255
        return clean

Пакетная обработка с очередью

import asyncio
from pathlib import Path
from paddleocr import PaddleOCR
import json
import sqlite3

class ArchiveDigitizationPipeline:
    def __init__(self, db_path: str, output_dir: str,
                  lang: str = 'ru'):
        self.ocr = PaddleOCR(use_angle_cls=True, lang=lang,
                              use_gpu=True, show_log=False)
        self.preprocessor = DocumentPreprocessor()
        self.db = sqlite3.connect(db_path)
        self.output_dir = Path(output_dir)
        self._init_db()

    def _init_db(self):
        self.db.execute('''
            CREATE TABLE IF NOT EXISTS documents (
                id INTEGER PRIMARY KEY,
                file_path TEXT UNIQUE,
                status TEXT DEFAULT 'pending',
                ocr_text TEXT,
                metadata TEXT,
                processed_at TIMESTAMP
            )
        ''')
        self.db.commit()

    def process_batch(self, image_paths: list[str],
                       n_workers: int = 4) -> dict:
        """Параллельная обработка пакета документов"""
        from concurrent.futures import ProcessPoolExecutor
        from tqdm import tqdm

        results = {'success': 0, 'failed': 0, 'errors': []}

        with ProcessPoolExecutor(max_workers=n_workers) as executor:
            futures = {
                executor.submit(self._process_single, p): p
                for p in image_paths
            }

            for future in tqdm(futures, total=len(futures),
                               desc='Digitizing archive'):
                path = futures[future]
                try:
                    result = future.result(timeout=120)
                    self._save_to_db(path, result)
                    results['success'] += 1
                except Exception as e:
                    results['failed'] += 1
                    results['errors'].append(str(e))

        return results

    def _process_single(self, image_path: str) -> dict:
        import cv2
        image = cv2.imread(image_path)
        preprocessed = self.preprocessor.preprocess_scanned_page(image)

        ocr_result = self.ocr.ocr(preprocessed, cls=True)
        if not ocr_result or not ocr_result[0]:
            return {'text': '', 'lines': [], 'confidence': 0}

        lines = []
        confidences = []
        for line in ocr_result[0]:
            bbox, (text, conf) = line
            lines.append({'text': text, 'confidence': conf, 'bbox': bbox})
            confidences.append(conf)

        full_text = '\n'.join(l['text'] for l in lines)
        mean_confidence = float(np.mean(confidences)) if confidences else 0

        return {
            'text': full_text,
            'lines': lines,
            'confidence': mean_confidence,
            'low_quality': mean_confidence < 0.7
        }

Полнотекстовая индексация результатов

import elasticsearch

class ArchiveSearchIndex:
    def __init__(self, es_url: str, index_name: str = 'archive'):
        self.es = elasticsearch.Elasticsearch([es_url])
        self.index = index_name
        self._create_index()

    def _create_index(self):
        mapping = {
            'mappings': {
                'properties': {
                    'file_path': {'type': 'keyword'},
                    'text': {
                        'type': 'text',
                        'analyzer': 'russian'
                    },
                    'confidence': {'type': 'float'},
                    'metadata': {'type': 'object'},
                    'processed_at': {'type': 'date'}
                }
            }
        }
        if not self.es.indices.exists(index=self.index):
            self.es.indices.create(index=self.index, body=mapping)

    def index_document(self, doc_id: str, text: str,
                        file_path: str, metadata: dict = None):
        self.es.index(index=self.index, id=doc_id, body={
            'text': text, 'file_path': file_path,
            'metadata': metadata or {}
        })

    def search(self, query: str, size: int = 10) -> list:
        result = self.es.search(index=self.index, body={
            'query': {'match': {'text': query}},
            'highlight': {'fields': {'text': {}}},
            'size': size
        })
        return result['hits']['hits']
Метрика Типографский текст Рукопись Исторические документы
OCR CER (хорошее качество) 0.5–1.5% 3–8% 5–20%
OCR CER (плохое качество) 2–5% 8–20% 15–40%
После AI постобработки 0.3–1% 2–6% 3–15%
Объём архива Срок
10,000–50,000 страниц (стандартные документы) 4–8 недель
100,000+ страниц с pipeline и индексацией 10–18 недель
Исторический архив с рукописями и реставрацией 20–36 недель