Содержание
  1. Что понадобится и как устроена проверка
  2. Полный код нативного запроса
  3. Независимая сверка с эталоном
  4. Два входа: доставка и итог
  5. Границы примера и альтернативная модель
  6. Частые вопросы
Гайды

Как распознать текст с картинки через Ollama

5 ноября 2026 · 10 минут чтения

Разберём, как распознать текст с картинки через Ollama: отправим base64-картинку нативным запросом к локальному /api/chat и сверим ответ построчно и по числам с заранее выписанным эталоном. Ниже — полный код запроса, два синтетических чека и независимый чекер.

Что понадобится и как устроена проверка

Задача — получить дословный текст PNG напрямую через Ollama, без n8n, LangChain и community web-обёртки. Используем vision-модель deepseek-ocr:3b. Её карточка требует Ollama v0.13.0+ и предупреждает о чувствительности к формулировке запроса; простой OCR-префикс ниже сохранён с атрибуцией. Проверка не измеряет качество всех моделей и не является прогоном редакции.

Вход — синтетический учебный чек ocr_fixture_a.png (1000×360, чёрные символы на белом). Ниже приведён его полный эталон, а затем генератор обоих PNG и файлов expected. Генератор выполняется обычным Python и не обращается к модели. Полный эталон A:

ТЕКСТ5 строк
TRAINING RECEIPT R-027
Date: 2026-11-05
Notebooks 3 x 240 = 720 RUB
Delivery 90 RUB
Total 810 RUB

Арифметика исходника: 3×240+90=810. Второй PNG отличается строками Delivery 120 RUB и Total 840 RUB. Создайте оба входа и эталоны следующим генератором make_fixture.py. Понадобится библиотека Pillow (python3 -m pip install Pillow) и путь к существующему TTF-шрифту, например DejaVuSansMono.

PYTHON20 строк
from pathlib import Path
import sys
from PIL import Image, ImageDraw, ImageFont

folder = Path(__file__).resolve().parent
font = ImageFont.truetype(sys.argv[1], 28)
for suffix, delivery in [("a", 90), ("b", 120)]:
    lines = [
        "TRAINING RECEIPT R-027",
        "Date: 2026-11-05",
        "Notebooks 3 x 240 = 720 RUB",
        f"Delivery {delivery} RUB",
        f"Total {720 + delivery} RUB",
    ]
    image = Image.new("RGB", (1000, 360), "white")
    draw = ImageDraw.Draw(image)
    for i, line in enumerate(lines):
        draw.text((36, 32 + i * 57), line, font=font, fill="black")
    image.save(folder / f"ocr_fixture_{suffix}.png")
    (folder / f"expected_{suffix}.txt").write_text("\n".join(lines) + "\n", encoding="utf-8")

Запустите python3 make_fixture.py /полный/путь/к/шрифту.ttf, подставив свой путь. Рядом со скриптом появятся оба PNG и файлы expected_a.txt/expected_b.txt. Откройте PNG и визуально сравните все пять строк с эталонами перед запросом к модели.

На компьютере читателя должен работать Ollama с моделью deepseek-ocr:3b; установить её можно командой ollama pull deepseek-ocr:3b. Доступность памяти проверяйте для своего оборудования. Сохраните скрипт run_ocr.py рядом с изображениями: он читает PNG, кодирует его в base64 и обращается к локальному /api/chat.

Документация Ollama по Vision описывает, что vision-модели принимают изображения вместе с текстом, а REST API ожидает base64-кодированные данные изображения. Документация по /api/chat описывает поле messages как массив объектов сообщений и параметр stream со значением по умолчанию true; в нашем скрипте stream выставлен в false, чтобы получить один JSON-ответ.

Что проверяем Инструмент Ожидаемый итог
Текст из PNG run_ocr.py → /api/chat JSON с полем message.content
Совпадение строк check_ocr.py all_lines_exact: PASS
Совпадение чисел check_ocr.py numbers_in_order_exact: PASS
Код завершения check_ocr.py 0

Полный код нативного запроса

Следующий полный скрипт run_ocr.py читает PNG, кодирует его в base64 и обращается к локальному /api/chat. Такой images-массив соответствует текущей документации Vision; stream:false нужен для одного JSON-ответа. Запрос Extract the text in the image. взят из официальной карточки OCR-модели, а сам вход и проверяющий код ниже — самостоятельный артефакт.

PYTHON10 строк
"""User-run native request; editorial author did not execute this request."""
import base64,json,sys
from pathlib import Path
from urllib.request import Request,urlopen
image=Path(sys.argv[1])
payload={'model':'deepseek-ocr:3b','messages':[{'role':'user','content':'Extract the text in the image.','images':[base64.b64encode(image.read_bytes()).decode('ascii')]}],'stream':False}
request=Request('http://localhost:11434/api/chat',data=json.dumps(payload).encode(),headers={'Content-Type':'application/json'})
with urlopen(request,timeout=300)as response:
 data=json.load(response)
print(data['message']['content'])

Пользовательский запуск:

ТЕРМИНАЛ
python3 run_ocr.py ocr_fixture_a.png > actual_a.txt
python3 run_ocr.py ocr_fixture_b.png > actual_b.txt

Это команды для будущего самостоятельного запуска. actual_a.txt и actual_b.txt в редакционном пакете отсутствуют: фактических ответов модели не было. Отсутствующие actual-файлы не заменены выдуманным ответом модели — их создаёт сам читатель своим запуском.

Разбор частой ошибки: адрес http://localhost:11434/api/chat совпадает с примером из документации Ollama по Chat endpoint. Если Ollama слушает другой порт или хост, запрос вернёт ошибку соединения — проверьте адрес в request=Request(...) до запуска. В нашем скрипте заголовок Content-Type: application/json явно указывает формат тела запроса.

Независимая сверка с эталоном

Полный проверяющий скрипт check_ocr.py:

PYTHON10 строк
"""Compare user OCR text with explicit synthetic ground truth."""
from pathlib import Path
import re,sys
expected=Path(sys.argv[1]).read_text(encoding='utf-8')
actual=Path(sys.argv[2]).read_text(encoding='utf-8')
def lines(text):return [line.strip()for line in text.splitlines()if line.strip()]
def numbers(text):return re.findall(r'\d+(?:[.,]\d+)?',text)
checks={'all_lines_exact':lines(actual)==lines(expected),'numbers_in_order_exact':numbers(actual)==numbers(expected)}
for name,passed in checks.items():print(name+': '+('PASS'if passed else'FAIL'))
if not all(checks.values()):raise SystemExit(1)

Проверки:

ТЕРМИНАЛ
python3 check_ocr.py expected_a.txt actual_a.txt
python3 check_ocr.py expected_b.txt actual_b.txt

Эталон успешной сверки каждого файла — две строки all_lines_exact: PASS и numbers_in_order_exact: PASS, код завершения 0. Эти строки описывают ожидаемый отчёт при успешной сверке. Фактический ответ модели читатель получает своим запуском. Сверка удаляет только пустые строки и пробелы по краям; знаки, цифры и остальные символы остаются значимыми. Поэтому изменение форматирования может дать строгий FAIL: разбирайте разницу.

Первая конкретная проверка: в A должны остаться все пять строк, номер R-027, дата и сумма 810 без добавленных реквизитов. Вторая: B обязан вернуть доставку 120 и итог 840; старый итог 810 отвергается. Если текст неполный или придуманы числа, результат не принимается.

Файл Ожидаемые строки Ключевые числа Итог
A 5 строк, номер R-027, дата 2026-11-05 3, 240, 720, 90, 810 810 RUB
B те же строки, изменены две 3, 240, 720, 120, 840 840 RUB

Два входа: доставка и итог

Два PNG различаются доставкой и итогом. Если ответ для B содержит старый итог 810, результат расходится с эталоном и отклоняется; причину ошибки нужно разбирать отдельно. Чекер сравнивает все числа в порядке появления, включая номер и дату: для B это 027, 2026, 11, 05, 3, 240, 720, 120, 840. Возврат старых значений доставки или итога даст numbers_in_order_exact: FAIL.

Практический порядок действий:

  1. Установите модель: ollama pull deepseek-ocr:3b.
  2. Положите run_ocr.py, check_ocr.py, оба PNG и оба expected_*.txt в одну папку.
  3. Запустите run_ocr.py для A и сохраните вывод в actual_a.txt.
  4. Запустите run_ocr.py для B и сохраните вывод в actual_b.txt.
  5. Прогоните check_ocr.py для каждой пары и посмотрите на две строки PASS/FAIL.
  6. При FAIL откройте diff между expected и actual: сначала ищите пропущенную строку, затем лишние символы, затем подменённое число.

Доставка и итог связаны арифметикой: 3×240+90=810 в A и 3×240+120=840 в B. Эта связь полезна тем, что подмена одного числа ломает сразу и строку, и итог. Проверка по числам в порядке появления ловит такую подмену без разбора всего текста.

Границы примера и альтернативная модель

Успех на этих двух простых картинках не доказывает пригодность для сканов, кириллицы или таблиц: для своей задачи добавьте собственные изображения и заранее выписанный эталон. Один простой пример не объявляется бенчмарком точности. Сравнение строк здесь даёт более строгую опору, чем просьба второй модели угадать, верно ли распознан документ.

Если нужен другой инструмент, в реестре Ollama есть карточка glm-ocr — мультимодальной OCR-модели для понимания сложных документов на архитектуре GLM-V encoder–decoder. В карточке указаны команды вида ollama run glm-ocr Text Recognition: ./image.png, размеры вариантов 2.7GB, 1.8GB и 2.7GB, контекст 128K и заявленный результат 94.62 на OmniDocBench V1.5. Это чужие заявления владельца карточки, они не переносятся на наш пример и не подтверждают качество на ваших файлах.

Критерий deepseek-ocr:3b glm-ocr
Запуск ollama pull deepseek-ocr:3b ollama run glm-ocr
Вход PNG в base64 через /api/chat PNG в команде карточки
Проверка свой check_ocr.py свой эталон
Заявленный бенчмарк нет в нашем пакете 94.62 на OmniDocBench V1.5

Выбор модели не отменяет главного шага: эталон выписывается до запуска, а результат сверяется построчно и по числам. Модель может вернуть правдоподобный текст с подменённой цифрой, и без эталона такую подмену заметить сложно.

Частые вопросы

Как запустить Ollama OCR для PNG без облачных сервисов?
Установите модель командой ollama pull deepseek-ocr:3b, сохраните run_ocr.py рядом с картинкой и запустите python3 run_ocr.py ocr_fixture_a.png > actual_a.txt. Скрипт кодирует PNG в base64 и отправляет его на локальный http://localhost:11434/api/chat с stream:false. Ответ приходит в поле message.content одного JSON-объекта. Облачная цепочка в этой схеме не участвует.

Почему проверка даёт FAIL, хотя текст распознан верно?
Чекер удаляет только пустые строки и пробелы по краям, остальные символы значимы. Лишний пробел внутри строки, изменённый регистр или другой разделитель дают строгий FAIL. Откройте diff между expected и actual и разберите конкретное расхождение. Автоматически называть такое расхождение галлюцинацией не стоит.

Чем отличаются два тестовых PNG?
ocr_fixture_a.png содержит доставку 90 RUB и итог 810 RUB, ocr_fixture_b.png — доставку 120 RUB и итог 840 RUB. Остальные строки совпадают: номер R-027, дата 2026-11-05, позиция Notebooks 3 x 240 = 720 RUB. Эти изменения помогают обнаружить возврат старых чисел, но сами по себе не устанавливают причину ошибки.

Можно ли использовать этот пример как бенчмарк точности?
Нет. Два синтетических чека с ASCII-текстом не покрывают сканы, кириллицу, таблицы и печати. Для своей задачи добавьте собственные изображения и выпишите эталон заранее. Заявленные чужие результаты на OmniDocBench относятся к карточке glm-ocr и не переносятся на ваш случай.

Что делать, если модель добавляет лишние реквизиты?
Дописанные строки, которых нет в эталоне, ломают all_lines_exact, и проверка завершается кодом 1. Сравните списки строк и уберите из промпта всё, что провоцирует домысливание. Формулировка запроса влияет на результат, поэтому фиксируйте её и меняйте по одному элементу за раз.

Читайте также

3 материала