Своими рукамипрактическая инструкция

Что увидит модель на фото листа? Первый запуск на Raspberry Pi

Редакция Фермозавра·25 июля 2026 г.·10 мин чтения

Возьмём готовую модель, обработаем фотографию и разберём три предложенных класса. Затем сравним ответы на своих снимках и начнём журнал наблюдений.

Что увидит модель на фото листа? Первый запуск на Raspberry Pi

Выберите фотографию листа и попробуйте спросить у модели, к какому из знакомых ей классов она её отнесёт. Начнём с готового комплекта, без обучения с нуля. Установим среду, загрузим конкретную версию модели и проследим, как снимок превращается в три строки результата.

Различия подходов разобраны в статье о CNN, RNN и LLM, основы — во вводном материале. Следующий этап — подготовка собственных данных, если проверка покажет в этом необходимость.

Наш первый лабораторный опыт

Скрипт принимает JPEG или PNG и показывает три класса с наибольшими оценками. Используем их для разбора снимка и планирования осмотра. Определение причины болезни остаётся отдельной задачей; здесь знакомимся с работой классификатора.

Начните с одного сохранённого файла, чтобы отделить запуск программы от настройки камеры. Когда получите ответ, поменяйте снимок и сравните классы. Скорость на своей Raspberry Pi и качество на своих фотографиях измерим в ходе опыта.

Готовим Raspberry Pi и место для снимков

  • Raspberry Pi 4B с охлаждением и подходящим корпусом. Объём памяти и скорость для своей нагрузки проверяют отдельно; фиксированное время ответа не обещается.
  • Блок питания, соответствующий плате. Для Pi 4 производитель рекомендует USB-C 15 Вт; официальный блок имеет выход 5,1 В и 3 А.
  • Носитель с запасом под ОС, окружение и снимки. Для настольного варианта Raspberry Pi OS документация рекомендует не менее 32 ГБ.
  • Доступ к устройству через монитор и клавиатуру либо настроенный SSH. Для первого запуска достаточно уже сохранённого снимка; камера подключается следующим этапом.

Требования к комплекту приведены в руководстве Raspberry Pi, характеристики питания — на странице блока USB-C. Смета включает также связь, хранение и настройку; цены уточните перед покупкой.

Камера снимает, Raspberry Pi считает

Выбранная модель занимает около 42 МБ и запускается в LiteRT с указанными ниже входом и выходом. Поэтому вычисление выполняет отдельный компьютер, а ESP32-CAM может стать источником кадров. Сначала освоим обработку уже сохранённых файлов.

Сбор снимков описан в материале об ESP32-CAM, передача кадров на вычислитель — в отдельной статье. В этом примере нет сетевого приёма фотографий или автоматического управления оборудованием.

Создаём отдельное окружение Python

Установите Raspberry Pi OS 64-bit. Пример использует Python 3.12 или 3.13; ветка ОС на Debian Trixie использует Python 3.13. Проверка в начале блока остановит установку при другой архитектуре или версии. Не заменяйте системный Python вручную ради примера: для старой системы сначала отдельно планируют обновление и резервную копию.

LiteRT устанавливается пакетом ai-edge-litert, а импортируется из ai_edge_litert.interpreter. Это соответствует официальной инструкции миграции. Для этого проекта используйте именно указанную среду выполнения. Выполните блок в Bash; библиотеки устанавливаются в отдельное окружение, системные сервисы не настраиваются.

#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
import platform, struct, sys
if platform.machine() != 'aarch64' or struct.calcsize('P') != 8:
    raise SystemExit('Нужны ARM64 и 64-битный Python; проверьте выбранный образ ОС.')
if not (3, 12) <= sys.version_info[:2] <= (3, 13):
    raise SystemExit('Этот набор версий рассчитан на Python 3.12 или 3.13.')
PY
sudo apt update
sudo apt install -y python3-venv python3-pip curl ca-certificates
mkdir -p ~/plant-ai
cd ~/plant-ai
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --only-binary=:all: \
  ai-edge-litert==2.2.0 numpy==2.5.3 pillow==12.3.0 \
  backports.strenum==1.2.8 flatbuffers==25.12.19 ml_dtypes==0.6.0 \
  protobuf==7.36.2 tqdm==4.70.1 typing_extensions==4.16.0
python -m pip check

После установки рабочая папка — ~/plant-ai. В новом терминале снова перейдите в неё и активируйте .venv/bin/activate. Если для своей системы пакет не найден, остановитесь и проверьте версию Python и архитектуру; не убирайте проверку бинарных пакетов вслепую.

Скачиваем модель вместе с картой её ответов

Используется EfficientNetV2S автора Animesh Kumar, ревизия 5965b98c122f28b3154241e9d6d10b7a367cac0f. Карточка указывает лицензию MIT и 38 классов PlantVillage. Автор прямо отмечает: работа на полевых фотографиях не подтверждена, модель не предназначена для принятия коммерческих решений по выращиванию. Сохраните README вместе с файлами и учитывайте эти ограничения.

В model_float16_quant.tflite веса имеют пониженную точность, а вход остаётся float32 RGB размером 384×384. Подадим значения от 0 до 255: предварительная обработка встроена. Подписи прочитаем по числовому индексу class_indices.json. Эти детали важны для связи выходного числа с правильным названием.

Сохраните следующий блок как ~/plant-ai/download-model.sh. Он проверяет контрольные суммы, загружает отсутствующие или отличающиеся файлы во временные имена и заменяет их только после проверки. Затем выполните bash ~/plant-ai/download-model.sh.

#!/usr/bin/env bash
set -euo pipefail
cd -- "$(dirname -- "$0")"
mkdir -p model
MODEL_BASE='https://huggingface.co/animeshakr/plant-disease-efficientnetv2s/resolve/5965b98c122f28b3154241e9d6d10b7a367cac0f'
fetch_checked() {
  local model_name="$1" model_sha="$2" model_path="model/$1"
  if [ -f "$model_path" ] && printf '%s  %s\n' "$model_sha" "$model_path" | sha256sum --check --status; then
    printf 'Уже проверен: %s\n' "$model_name"
    return
  fi
  curl --fail --location --retry 2 --connect-timeout 20 --max-time 300 \
    --output "$model_path.part" "$MODEL_BASE/$model_name"
  printf '%s  %s\n' "$model_sha" "$model_path.part" | sha256sum --check
  mv -- "$model_path.part" "$model_path"
}
fetch_checked model_float16_quant.tflite c94c4b1dd60d8d5c3b2e621105cf790b95584b49b7820e127ef68bc942f0f6f6
fetch_checked class_indices.json 909728f7467cd426e756220cc772c6f11304e07528626e8950d6aec334aeabb0
fetch_checked README.md ac392613dfe2a70b1fd461e8522d36734ab2dccdc932a23c4dc7e3857c3304b8

Запускаем полный скрипт на своём файле

Сохраните predict.py рядом с папкой model в ~/plant-ai. Скрипт учитывает EXIF, переводит кадр в RGB и масштабирует BICUBIC. Для учебной загрузки выбраны пределы 20 МиБ и 16 миллионов пикселей. Если позже замените модель, заново сверьте подготовку изображения и её входы и выходы.

#!/usr/bin/env python3
"""One pinned educational classifier; scores are not a diagnosis."""
import argparse
import hashlib
import json
from pathlib import Path
import sys
import warnings
import numpy as np
from PIL import Image, ImageOps
from ai_edge_litert.interpreter import Interpreter

REVISION = '5965b98c122f28b3154241e9d6d10b7a367cac0f'
FILES = {
    'model_float16_quant.tflite': 'c94c4b1dd60d8d5c3b2e621105cf790b95584b49b7820e127ef68bc942f0f6f6',
    'class_indices.json': '909728f7467cd426e756220cc772c6f11304e07528626e8950d6aec334aeabb0',
}
MAX_BYTES = 20 * 1024 * 1024
MAX_PIXELS = 16_000_000
Image.MAX_IMAGE_PIXELS = MAX_PIXELS


def check_file(path, expected):
    digest = hashlib.sha256()
    with path.open('rb') as stream:
        for chunk in iter(lambda: stream.read(1024 * 1024), b''):
            digest.update(chunk)
    if digest.hexdigest() != expected:
        raise ValueError('Контрольная сумма не совпала: ' + path.name)


def read_labels(path):
    labels = json.loads(path.read_text(encoding='utf-8'))
    if not isinstance(labels, dict) or set(labels) != {str(i) for i in range(38)}:
        raise ValueError('Ожидается карта классов с индексами от 0 до 37')
    names = [labels[str(i)] for i in range(38)]
    if any(not isinstance(s, str) or not s.strip() for s in names) or len(set(names)) != 38:
        raise ValueError('Некорректные или повторяющиеся подписи классов')
    return names


def load_image(path):
    if not path.is_file() or not 0 < path.stat().st_size <= MAX_BYTES:
        raise ValueError('Нужен файл JPEG/PNG размером не более 20 МиБ')
    with warnings.catch_warnings():
        warnings.simplefilter('error', Image.DecompressionBombWarning)
        with Image.open(path) as original:
            if original.format not in {'JPEG', 'PNG'} or getattr(original, 'n_frames', 1) != 1:
                raise ValueError('Поддерживается один кадр JPEG или PNG')
            if original.width * original.height > MAX_PIXELS:
                raise ValueError('Изображение превышает 16 миллионов пикселей')
            original.load()
            image = ImageOps.exif_transpose(original).convert('RGB')
            image = image.resize((384, 384), Image.Resampling.BICUBIC)
            # This exact model includes preprocessing: do not divide by 255.
            return np.expand_dims(np.asarray(image, dtype=np.float32), 0)


def ranked_scores(values, labels):
    values = np.asarray(values)
    if values.shape != (1, 38) or not np.isfinite(values).all():
        raise ValueError('Некорректная форма или нечисловой выход модели')
    scores = values[0]
    if (scores < 0).any() or (scores > 1).any() or not np.isclose(scores.sum(), 1, atol=1e-3, rtol=0):
        raise ValueError('Выход не соответствует ожидаемому softmax')
    order = np.argsort(-scores, kind='stable')[:3]
    return [{'class': labels[int(i)], 'score': float(scores[i])} for i in order]


def predict(image_path, model_dir):
    for name, expected in FILES.items():
        check_file(model_dir / name, expected)
    labels = read_labels(model_dir / 'class_indices.json')
    image = load_image(image_path)
    engine = Interpreter(model_path=str(model_dir / 'model_float16_quant.tflite'), num_threads=1)
    engine.allocate_tensors()
    inputs, outputs = engine.get_input_details(), engine.get_output_details()
    if len(inputs) != 1 or len(outputs) != 1:
        raise ValueError('Ожидается один вход и один выход')
    inp, out = inputs[0], outputs[0]
    if list(inp['shape']) != [1, 384, 384, 3] or inp['dtype'] != np.float32:
        raise ValueError('Неожиданный вход: нужен float32 RGB [1,384,384,3]')
    if list(out['shape']) != [1, 38] or out['dtype'] != np.float32:
        raise ValueError('Неожиданный выход: нужен float32 [1,38]')
    engine.set_tensor(inp['index'], image)
    engine.invoke()
    return {'model_revision': REVISION, 'top': ranked_scores(engine.get_tensor(out['index']), labels),
            'note': 'Оценки модели не являются диагнозом или вероятностью болезни.'}


def main():
    parser = argparse.ArgumentParser(description='Учебная классификация одного снимка листа')
    parser.add_argument('image', type=Path)
    parser.add_argument('--model-dir', type=Path, default=Path(__file__).resolve().parent / 'model')
    parser.add_argument('--json', action='store_true')
    args = parser.parse_args()
    try:
        result = predict(args.image, args.model_dir)
    except (OSError, ValueError, RuntimeError, Image.DecompressionBombError, Image.DecompressionBombWarning) as exc:
        print('Ошибка: ' + str(exc), file=sys.stderr)
        return 2
    if args.json:
        print(json.dumps(result, ensure_ascii=False, allow_nan=False))
    else:
        print(result['note'])
        for item in result['top']:
            print(f"{item['score']:.4f}\t{item['class']}")
    return 0


if __name__ == '__main__':
    raise SystemExit(main())

Передайте путь к своему снимку. Названия останутся английскими, как в карте классов. Вариант --json поможет сохранить результат для сравнения; обработка выполняется локально.

cd ~/plant-ai
source .venv/bin/activate
python predict.py /полный/путь/leaf.jpg
python predict.py /полный/путь/leaf.jpg --json

Разбираем три строки ответа

Вывод содержит оценки softmax для классов, известных модели. Их сумма по всем классам близка к единице, однако это не означает, что модель умеет распознать любое состояние растения или надёжно оценить вероятность болезни. Даже высокий максимум может сопровождать ошибку на неподходящем снимке. Проблема калибровки оценок разобрана в исследовании Guo и соавторов.

Сравните первые три класса со снимком и результатом осмотра. Для рабочего отбора понадобится свой порог по проверенным данным, пропускам и ложным тревогам. Пока оставьте все оценки в журнале. Ошибку обработки пометьте отдельно, чтобы отличать её от полученного результата.

Меняем свет, фон и ракурс

Модель училась на контролируемых фотографиях PlantVillage. Теперь посмотрите, что происходит с кадрами вашего хозяйства: другим фоном, освещением, сортом и повреждениями. Сохраните примеры, где ответ заметно меняется или выглядит сомнительно.

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

Добавляем камеру после первого успешного запуска

Откройте исходный кадр в полном размере: виден ли лист, резки ли детали, нет ли блика? Подберите расстояние под объектив и объект. Снимайте обычные и подозрительные состояния, отмечайте растение, дату и место.

Для поддерживаемой камеры Raspberry Pi используется rpicam-still. Команды и параметры описаны в официальной документации камеры. Проверьте обнаружение камеры, затем сделайте кадр. По SSH параметр --nopreview отключает окно предпросмотра. Для USB-вебкамеры сохраните JPEG средствами её драйвера или приложения.

rpicam-hello --list-cameras
rpicam-still --nopreview --output leaf.jpg --width 1280 --height 960
python ~/plant-ai/predict.py leaf.jpg

Если rpicam-still отсутствует, установите rpicam-apps из репозитория своей Raspberry Pi OS по документации. Если камера не найдена, проверьте совместимость и подключение. После получения читаемого кадра вернитесь к обработке.

Если программа остановилась с ошибкой

  • Не та версия модели или карта классов: скрипт остановится по контрольной сумме. Не заменяйте хеш, пока не проверили новый комплект и обработку данных.
  • Неправильная нормализация или порядок каналов: для выбранного файла нужны RGB и исходный диапазон значений float32.
  • Повреждённый, многокадровый или слишком большой файл: исправьте источник снимка, а не трактуйте отсутствие результата как отсутствие проблемы.
  • Перегрев, недостаточное питание или нехватка памяти: проверяйте устройство под своей нагрузкой. Время работы ARM64-сервера не является замером на Pi.

Ведём небольшой журнал ответов

Рядом со снимком сохраняйте дату, место, ревизию модели, первые классы и результат осмотра. Когда причина подтверждена, добавьте её отдельно. Для спорных случаев оставьте понятную отметку — они особенно полезны для обсуждения следующих шагов.

Пополняйте журнал разными условиями, которые важны для вашей задачи. Примеры, по которым меняли настройки, оставьте в группе разработки; для независимой оценки используйте отложенную подборку.

Сравниваем первую подборку

  1. Проверьте запуск на сохранённом снимке и обработку заведомо повреждённого файла: при ошибке не должно быть предсказания.
  2. Составьте отдельный набор снимков с подтверждёнными состояниями и отмеченными условиями съёмки.
  3. Не смешивайте близкие кадры одного растения между обучением и проверкой, если оцениваете перенос на новые растения.
  4. Считайте ошибки по классам, пропуски и ложные тревоги, а также время проверки сообщений человеком.
  5. Сравните результат с обычным осмотром и решите, какую дополнительную проверку нужно провести.

Небольшая подборка может выявить явные неполадки, но не доказывает пригодность системы. Критерий «попала в тему в семи случаях» слишком неопределёнен для приёмки. Принципы разделения связанных данных описаны в документации scikit-learn.

От результата программы к осмотру растения

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

Готовим следующий опыт

Сохраняйте исходные фотографии и проверенные метки с самого начала. При необходимости переходите к подготовке собственных данных. Для постоянной съёмки понадобится отдельная схема доставки кадров с камеры и обработкой сбоев; она рассматривается в связанной статье.

Возвращаемся к своему вопросу

Теперь у вас есть путь от файла до ответа. Посмотрите, какие снимки модель разбирает полезно, где ошибается и сколько времени занимает обработка. Эти наблюдения помогут выбрать следующий шаг. Другие материалы доступны в плане серии.

💬 Комментарии

Чтобы оставить комментарий, войдите или зарегистрируйтесь

Загрузка комментариев...

Похожие статьи