Document Page Detector
Модель находит страницы на длинном скриншоте документа и изображения внутри этих страниц.
Обучена для программы, которая режет длинные скриншоты на отдельные страницы: одна такая лента может содержать 8–20 страниц подряд.
Что делает
Модель возвращает рамки двух видов и различает их по уверенности:
| Уверенность | Что найдено |
|---|---|
| 0.9 – 0.99 | страница документа целиком |
| 0.5 – 0.8 | изображение внутри страницы |
| 0.2 – 0.4 | отдельный абзац, подпись, шум |
Это важная особенность: разделять страницы и картинки по ширине рамки нельзя — у них почти одинаковая ширина. Разделяет именно уверенность.
Зачем нужна именно модель
Длинный скриншот — не сплошная лента: внутри страниц тоже есть крупные пустые промежутки между разделами (200–560 px), того же размера, что и разрывы между страницами.
Поэтому простой алгоритм «резать по пустым полосам» ошибается: на тестовом документе из 8 страниц он давал 13 кусков. Модель этот случай различает.
Архитектура
Faster R-CNN + ResNet-50-FPN (torchvision)
num_classes = 2 (фон + страница)
вход: до 1024 px по длинной стороне
Как использовать
import torch
from PIL import Image
from torchvision import transforms as T
from torchvision.models.detection import fasterrcnn_resnet50_fpn
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
model = fasterrcnn_resnet50_fpn(weights=None)
in_features = model.roi_heads.box_predictor.cls_score.in_features
model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes=2)
state = torch.load("trained_cutter_model.pth", map_location="cpu")
if isinstance(state, dict) and "model_state_dict" in state:
state = state["model_state_dict"]
model.load_state_dict(state, strict=False)
model.eval()
image = Image.open("screenshot.png").convert("RGB")
width, height = image.size
scale = 1024 / max(width, height)
small = image.resize((int(width * scale), int(height * scale)), Image.BICUBIC)
with torch.no_grad():
prediction = model([T.ToTensor()(small)])
boxes = prediction[0]["boxes"].numpy()
scores = prediction[0]["scores"].numpy()
# Страницы — только рамки с высокой уверенностью.
# 0.9 строго, 0.75 мягче: подбирается под формат съёмки.
scale_back = 1 / scale
pages = [
tuple(int(c * scale_back) for c in box)
for box, score in zip(boxes, scores)
if score >= 0.75
]
print(f"найдено страниц: {len(pages)}")
Пороги уверенности
| Задача | Порог |
|---|---|
| Страницы | 0.7 – 0.9 |
| Изображения внутри страниц | 0.4 – 0.5 |
Порог 0.5 для страниц не подходит: в результат попадут абзацы и
подписи, и число «страниц» окажется завышенным.
Порог 0.9 строгий: если документ снят в другой ширине или с другой
плотностью текста, часть страниц может не попасть в результат.
На проверенном документе полный набор давал 0.7. Значение подбирается
под конкретный формат съёмки.
Тестовый документ
В репозитории лежит test_document.png — лента 2476×12964 на 8 страниц.
Содержимое синтетическое: текст про интернет-мемы о нейросетях, картинки
нарисованы кодом. Ничего чужого, можно использовать для проверки.
Что должно получаться:
| Порог | Найдено страниц |
|---|---|
| 0.9 | 6 — две последние теряются |
| 0.8 | 6 |
| 0.7 | 8 — полный набор |
| 0.5 | 8, но появляются лишние рамки на абзацах |
Если результат при 0.7 отличается — модель стоит дообучить на своих
документах.
Область применения
- длинные скриншоты документов, снятые одним снимком;
- страницы Google Docs, сохранённые одной лентой;
- подготовка документа к постраничному распознаванию (OCR / vision-LLM).
Модель обучена на скриншотах узкого формата — длинная вертикальная лента шириной 1300–2500 px. На обычных горизонтальных фотографиях документов результат не гарантируется.
Ограничения
- Документ должен быть снят одной лентой. Отдельные страницы A4 модель не ищет — для них нарезка не нужна.
- При очень большой высоте (более ~15 000 px) уверенность падает: изображение сильно ужимается перед подачей в сеть.
- Язык текста значения не имеет — модель работает с геометрией, а не с текстом.
Лицензия
MIT.