C++-порт plotparse — детерминированного (без
нейросетей) распознавания графиков в PDF и подбора аналитической формулы.
Алгоритм 1:1 с Python-версией, см. PORTING.md там же для теории; здесь —
только то, что изменилось при переносе.
Зависимости (Ubuntu 24.04, устанавливались как apt-пакеты):
apt-get install cmake ninja-build pkg-config \
libmupdf-dev mupdf-tools libeigen3-dev libceres-dev \
libgflags-dev libgoogle-glog-dev \
libfreetype-dev libjpeg-dev libjbig2dec0-dev libopenjp2-7-dev \
libharfbuzz-dev libgumbo-dev libmujs-dev \
libopencv-dev libtesseract-dev tesseract-ocr tesseract-ocr-ruscmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)Даёт build/analyze_pdf.
./build/analyze_pdf chart.pdf # текстовое описание
./build/analyze_pdf chart.pdf --json # машиночитаемый вывод
./build/analyze_pdf scan.pdf --csv out/ # + выгрузка точек кривой
./build/analyze_pdf chart.pdf --force raster # принудительно через CV| Файл | Соответствует | Библиотеки |
|---|---|---|
calib.{hpp,cpp} |
calib.py |
Eigen (QR вместо np.polyfit) |
pdf_backend.{hpp,cpp} |
pymupdf-вызовы внутри vector.py/raster.py |
MuPDF C API |
vector.cpp |
vector.py |
— (геометрия, чистый C++) |
raster.cpp |
raster.py |
OpenCV, Tesseract |
fit.{hpp,cpp} |
fit.py |
Eigen (полиномы), Ceres (нелинейные модели) |
report.cpp |
report.py |
— |
main.cpp |
analyze_pdf.py |
— |
pdf_backend.hpp и raster.hpp — единственные места, знающие про MuPDF и
OpenCV/Tesseract соответственно; vector.cpp/report.cpp работают только с
их структурами (PageContent, RawPath, TextSpan, PdfDocument::Raster).
Помимо замены библиотек (см. таблицу в ../plotparse-src/plotparse-1.0/PORTING.md):
- Солвер нелинейных моделей — Ceres вместо
scipy.curve_fit. Формулы и R² победившей модели совпадают с эталоном (reference/expected.txt) почти всегда посимвольно; 2-3-е места в списке «альтернативы» иногда отличаются — Ceres на заведомо плохих моделях (например, гауссиана поверх синусоиды) сходится к другому локальному оптимуму, чем LM у scipy. На итоговый выбор модели это не влияет. --force vectorтеперь действительно означает «только вектор». В оригиналеforceразветвлялся только для"raster";"vector"не отключал автоподхват растровой веткой при неудаче — это несогласованность с собственным CLI-хелпом Python-версии, здесь исправлено.- Отчёт на английском (в оригинале был русский). Перевод сверен по хэшу всех
числовых токенов вывода до и после — совпал, то есть изменились только слова.
Побочный эффект: побайтовая сверка с Python-эталоном больше невозможна, поэтому
добавлен отдельный эталон
reference/expected_cpp.txt. - Подписи кривых. Каждой серии ищется своя подпись (в Python-версии серии различались только цветом и порядковым номером). Два случая: легенда — если слева от текста стоит короткий цветной образец, подпись уходит серии того же цвета (а не геометрически ближайшей — иначе все записи легенды прилипают к кривой, проходящей мимо неё); подпись у самой кривой — берётся ближайшая по расстоянию серия, не дальше 15% меньшей стороны поля. Соответствие один-к-одному, жадно по возрастанию «цены». Спаны, уже занятые под числа осей/подписи осей/заголовок, в кандидаты не попадают. Работает в векторной ветке; в растровой подписи кривых пока не ищутся (нужен OCR по всей области графика, а не по узким полосам у осей).
- OCR кириллицы работает, если стоит
tesseract-ocr-rus. Эталонныйreference/expected.txtбыл получен в окружении, где эта языковая модель, похоже, отсутствовала (подписи остались нечитаемым набором латинских букв даже после OCR-фолбэка) — с установленным пакетом здесь подписи восстанавливаются корректно («Время, с», «Сигнал», …).
- Двойной flip координат страницы.
fz_bound_page/fz_run_pageуже отдают «page space» с origin в левом верхнем углу и y вниз — в отличие от сырых координат внутриfz_path(которые видитfz_path_walkerдо примененияctm). Матрица перехода должна быть только сдвигом (fz_make_matrix(1,0,0,1,-x0,-y0)), без переворота — переворот даёт зеркально перевёрнутую страницу. - Задвоение геометрии рамки осей. PDF-оператор «залить+обвести» (
B) MuPDF разбирает на два отдельных вызоваfz_device—fill_path, затемstroke_pathс тем жеfz_path*и тем жеctm. PyMuPDF'sget_drawings()отдаёт это одним объектом (type: "fs",fillиcolorвместе); без слияния вpdf_backend.cppрамка/фон осей задваивается и портит подсчёт grid-линий и тиков в скоринге. Слияние — по указателюfz_path*и совпадениюctmмежду последовательнымиfill_path→stroke_path.
reference/ — те же PDF и expected.txt, что и в Python-версии. Прогон:
for f in exp sin logy scatter_parabola power_en no_chart raster_exp; do
./build/analyze_pdf reference/$f.pdf
done