Привет, Хабр! На связи команда регионального научно-образовательного центра «Искусственный интеллект и анализ больших данных» при НГТУ им. Р. Е. Алексеева. При поддержке компании YADRO мы изучаем архитектуру RISC-V и компьютерное зрение, чтобы внедрить результаты в учебный процесс.
Предлагаем вместе с нами проверить, на что способен одноплатный компьютер Lichee Pi 4A в задачах обработки изображений, несмотря на его ограниченные ресурсы. А заодно — получить базовые навыки по разработке систем компьютерного зрения. Пройдем путь от настройки системы до отслеживания кликов по картинке и распознавания объектов с моделью YOLOX.
Кратко расскажем, что ждет вас в статье. Мы создадим виртуальное окружение Python и установим подходящую среду разработки — Visual Studio Code. Далее перейдем к работе с изображениями. Начнем с базовых операций, таких как получение информации о файле, изменение отдельных пикселей и работа с цветовыми каналами.
Затем освоим более продвинутые приемы: динамическую перекраску, фильтрацию, обрезку, вращение и комбинированные преобразования. Также коснемся интерактивной работы с изображениями — научимся отслеживать клики пользователя и добавлять графические элементы. И в завершении распознаем объекты на картинке.
Почему мы выбрали библиотеку Pillow и Lichee Pi 4A
Pillow — это мощная и удобная библиотека для работы с изображениями в Python. Она позволяет выполнять основные операции: изменять размер, фильтровать и поворачивать изображения, а также конвертировать в другие форматы. Мы решили узнать, насколько эффективно Lichee Pi 4A справится с этими задачами и можно ли его использовать для базовых операций компьютерного зрения.

Lichee Pi 4A — это одноплатный компьютер (single-board computer, SBC) на базе четырехъядерного процессора Alibaba T-Head TH1520 RISC-V Xuantie C910 2,0 ГГц и графического процессора Imagination. За ускорение ИИ-задач отвечает нейронный процессор (NPU), производительность которого составляет 4 TOPS. В целом Lichee Pi 4A по вычислительной мощности сравним с Raspberry Pi 4.
У Lichee Pi 4A есть широкий набор интерфейсов для подключения оборудования: USB 3.0, USB Type-C, HDMI 2.0, два интерфейса для камер (1×4-lane MIPI CSI и 1×2-lane MIPI CSI), гигабитный Ethernet, Wi-Fi и Bluetooth.
Основные преимущества Lichee Pi 4A:
Компактный размер и низкое энергопотребление позволяют использовать устройство для встраиваемых систем, мобильных решений и проектов в области интернета вещей.
Цена 12–14 тысяч рублей (на момент покупки) делает микрокомпьютер привлекательным вариантом для образовательных целей, проведения экспериментов и быстрого прототипирования.
Широкий набор интерфейсов обеспечивает гибкость при подключении периферийных устройств и внешних сенсоров.
Поддержка ОС Linux и Android значительно упрощает разработку, тестирование и портирование ПО.
Мы выбрали именно Lichee Pi 4A, а не Raspberry Pi 4 по трем причинам:
Платформа построена на перспективной архитектуре RISC-V, которая быстро набирает популярность в мире открытых вычислений.
Наличие NPU делает Lichee Pi 4A особенно интересным для экспериментов с компьютерным зрением.
Работа с менее распространенным оборудованием развивает навыки адаптации и оптимизации ПО под новые аппаратные решения — важную компетенцию для инженеров, которые специализируются на интернете вещей и локальном ИИ на периферийных устройствах (Edge AI).
Теперь перейдем к практическим шагам и займемся настройкой нужного ПО.
Устанавливаем и настраиваем инструменты
Для начала нам нужно подготовить базовую конфигурацию среды для работы с Python — нам поможет документация настройки окружения для Lichee Pi 4A.
Обновим список пакетов и установим актуальные версии библиотек и утилит:
sudo apt update
sudo apt upgrade -y
Ключ -y автоматически подтверждает установку всех обновлений без дополнительных вопросов от пакетного менеджера. Мы будем использовать его, чтобы ускорить процесс настройки.
Устанавливаем основные инструменты:sudo apt install wget git vim -y
wget — загрузка файлов через терминал,
git — система управления версиями для клонирования и работы с репозиториями,
vim — удобный текстовый редактор в командной строке
Проверяем версию Python:python3 --version
Обновить или установить Python на Linux поможет официальная документация.
Устанавливаем библиотеку SHL
Она нужна для работы с Lichee Pi 4A, порядок установки также описан в документации к устройству. Архив с библиотекой после скачивания нужно распаковать, а библиотечные файлы скопировать в системные директории.
Настраиваем менеджер пакетов и создаем виртуальное окружения
Чтобы упростить управление зависимостями Python-проектов, мы установили менеджер пакетов pip
и модуль venv
для создания виртуальных сред:
sudo apt install python3-pip python3-venv -y
После этого создаем и активируем виртуальную среду:
cd ~
python3 -m venv ort
source ~/ort/bin/activate
Так мы изолируем зависимости каждого проекта и обеспечиваем стабильность работы приложений на Lichee Pi 4A.
Теперь у нас есть полностью подготовленное рабочее окружение. Можно переходить к установке дополнительных библиотек Python и разработке наших проектов. Настроенное виртуальное окружение позволяет управлять установленными пакетами и минимизировать возможные конфликты между версиями библиотек.
Visual Studio Code для разработки
Для удобства разработки мы решили установить интегрированную среду разработки (IDE) Visual Studio Code, которая поддерживает работу с Python и другими языками программирования через расширения. Мы выбрали Visual Studio Code, потому что это кроссплатформенный инструмент с удобной интеграцией с Git, встроенным терминалом, отладчиком и поддержкой удаленной разработки. Кроме того, его активно развивает и поддерживает сообщество разработчиков.
Lichee Pi 4A основан на архитектуре RISC-V, поэтому официальная сборка Visual Studio Code для него недоступна. Мы установили VS Code на x86_64-машину разработчика и подключались к микрокомпьютеру по SSH. Код писали и редактировали локально, а запускали и отлаживали удаленно на Lichee Pi 4A.
Мы также протестировали VSCodium — open source-альтернативу VS Code. У этого редактора есть экспериментальные сборки для архитектуры RISC-V, которые можно собрать вручную, например с помощью QEMU или в chroot-среде. Нам удалось запустить VSCodium на Lichee Pi 4A, но его производительность оказалась низкой для полноценной работы, поэтому поэтому рекомендуем писать код удаленно, с основной х86_64-машины.
Операционная система Lichee Pi 4A создана на базе Debian/Ubuntu, поэтому на локальную машину мы установили Debian через .deb-пакет
. Его можно скачать с официального сайта.
Сначала скачиваем .deb-пакет для 64-разрядной версии:
wget https://update.code.visualstudio.com/latest/linux-deb-x64/stable -O vscode.deb
Устанавливаем пакет через apt
:sudo apt install ./vscode.deb
Если у вас старая версия дистрибутива, возможно, сначала придется установить пакет через dpkg
, а затем исправить зависимости:
sudo dpkg -i vscode.deb
sudo apt-get install -f
Установка .deb-пакета автоматически добавляет репозиторий Visual Studio Code в систему и настраивает ключ подписи. Так можно получать обновления через стандартный механизм обновления пакетов apt
:
sudo apt install apt-transport-https
sudo apt update
sudo apt install code
Теперь практически все нужные нам инструменты установлены и настроены, перейдем к работе с картинками.
Базовые приемы обработки изображений с Pillow
Начнем с основ — анализа картинки и базовых манипуляций, таких как изменение цвета пикселя и смена цветовой модели.
Получаем основную информацию об изображении
Первый шаг — загрузить изображение, определить его цветовую модель и получить информацию о его размере и границах.
from PIL import Image
from PIL import ImageFilter
img = Image.open(“flower.jpg”)
print(img.size, img.format, img.mode)

Эти базовые данные нам нужны для дальнейшей работы с изображением, анализ и визуализацию.
Меняем цвет пикселя
К отдельным пикселям можно обращаться с помощью метода load()
из библиотеки Pillow. Так мы сможем изменять цветовые значения точечно, а это основа для различных операций по обработке изображений.
Открываем white.jpg с помощью Pillow:
from PIL import Image
img = Image.open("white.jpg")
obj = img.load()
Выбираем пиксель с координатами (25, 45) и меняем его цвет:
obj[25, 45] = (0, 0, 0) # Новый цвет: черный (RGB: 0, 0, 0)
Сохраняем отредактированное изображение:
img.save("image3.jpg")
Визуально проверяем, что цвет пикселя изменился.
Использование метода load()
позволяет напрямую работать с массивом пикселей изображения: читать, модифицировать и анализировать отдельные элементы, не копируя данные в отдельные структуры. Это особенно важно для задач, которые требуют высокую производительности при обработке больших изображений.

Меняем цветовую модель
Чтобы лучше понять структуру цифровых изображений, научимся работать с цветовыми каналами с помощью методов split()
и merge()
из библиотеки Pillow.
Открываем исходное изображение:
from PIL import Image
img = Image.open("flower.jpg")
print(img.mode) # Проверили текущую цветовую модель: "RGB"
Разделяем изображение на три независимых канала: красный (R), зеленый (G) и синий (B):
R, G, B = img.split()
Создаем новый альфа-канал (mask), который заполнен полупрозрачными пикселями:
mask = Image.new("L", img.size, 128) # 128 из 255 — полупрозрачность
Здесь L — это режим изображения, который обозначает одноканальное изображение в градациях серого (8 бит на пиксель). Он часто используется для масок и альфа-каналов, где:
0 соответствует полной прозрачности,
255 — полной непрозрачности,
значения между ними — различным степеням полупрозрачности.
Подробнее о режимах в Pillow читайте в официальной документации.
Объединяем каналы в изображение RGBA-формата:
img2 = Image.merge("RGBA", (R, G, B, mask))
print(img2.mode) # Теперь "RGBA"
img2.show()
Зачем нам нужно уметь менять цветовую модель:
split()
позволяет независимо работать с отдельными цветовыми компонентами изображения.merge()
дает возможность собрать новое изображение, внося изменения в отдельные каналы или добавляя новые (например, управление прозрачностью через альфа-канал).Переход к модели RGBA открывает доступ к техникам наложения, маскирования, работы с прозрачными слоями — это нужно для создания сложной графики или анимации.

Новое изображение и более сложные манипуляции
Чтобы закрепить базовые навыки работы с Pillow, создадим простое изображение и будем постепенно усложнять задачи, добавляя элементы цветовой динамики и расчетов на основе координат.
Нарисуем черный квадрат 100×100 пикселей:
from PIL import Image
img = Image.new("RGB", (100, 100)) # Черный квадрат
img.save("qwerty.jpg")
Он будет нашим первым тестовым объектом для экспериментов.

Также нарисуем прямоугольник размером 320×240 пикселей и заполним его пурпурным цветом (RGB: 205, 100, 200):
from PIL import Image
img = Image.new("RGB", (320, 240), "rgb(205,100,200)")
img.save("qwerty2.jpg")

Эксперименты с различными значениями каналов RGB помогли нам добиться яркого и насыщенного оттенка. Этот навык пригодится для проектирования цветовых схем и тем оформления.
Теперь попробуем создать прямоугольник с более сложной, функциональной раскраской. Вместо того, чтобы использовать статичные значения цветовых каналов, мы решили вычислять их динамически, в зависимости от координат пикселей на изображении.
Создаем новое изображение в форме прямоугольника и приступаем к заполнению его пикселей по собственной формуле для вычисления значений красного, зеленого и синего каналов:
from PIL import Image
img = Image.new("RGB", (320, 240))
for x in range(320):
for y in range(240):
r = int(x / 3) # Зависимость красного от горизонтальной координаты
g = int((x + y) / 6) # Зависимость зеленого от суммы координат
b = int(y / 3) # Зависимость синего от вертикальной координаты
img.putpixel((x, y), (r, g, b))
img.save("okno2.png")
Мы намеренно используем метод putpixel()
вместо более производительного load()
, который обеспечивает быстрый доступ к пикселям и лучше подходит для задач массовой обработки изображений. putpixel()
позволяет наглядно показать принцип адресной записи цвета по координатам и не вводить дополнительных переменных, поэтому лучше подходит для обучения.
Наша формула учитывает горизонтальное и вертикальное положение каждого пикселя, мы смогли получить плавные переходы и динамичные визуальные эффекты.
Эксперименты с различными математическими выражениями позволили нам добиться красивых, плавно переходящих оттенков, которые создают эффект мерцания и глубины:

Меняем цветовые каналы
Посмотрим, как будет выглядеть привычная картинка, если поменять местами красный, зеленый и синий компоненты:
from PIL import Image
img = Image.open("flower.jpg")
for x in range(img.size[0]):
for y in range(img.size[1]):
r, g, b = img.getpixel((x, y))
img.putpixel((x, y), (b, r, g))
img.save(“okne2.jpg”)
Мы перебрали все пиксели исходного изображения и извлекли для каждого значения трех цветовых каналов: R (красный), G (зеленый) и B (синий). Эти числовые значения хранят информацию об интенсивности соответствующего цвета в данном пикселе.
Продолжим экспериментировать и сделаем перестановку каналов RGB: R станет B, G — R, а B — G. Так мы получим новые значения для каждого пикселя, которые отражают смещение цветовых компонент.

Наконец, попробуем перемешать каналы:
from PIL import Image
img = Image.open("picture.jpg")
for x in range(img.size[0]):
for y in range(img.size[1]):
r, g, b = img.getpixel((x, y))
img.putpixel((x, y), (b, r, g))
img.save(“okne.jpg”)
Мы визуализировали картинку с новыми значениями R, G и B для каждого пикселя. В результате получился довольно необычный эффект:

Учимся работать с отдельными цветовыми каналами
Разделяя изображение на отдельные цветовые каналы и собирая их обратно, мы можем по-разному влиять на его внешний вид и проводить эксперименты с цветом.
Методом split()
выделим три отдельных канала RGB: красный, зеленый и синий в виде кортежа. Каждый канал — это отдельное черно-белое изображение, которое хранит информацию об интенсивности своего цвета в каждом пикселе.
Теперь соберем изображение обратно из трех каналов с помощью конструкции Image.merge()
. Для этого передадим ей режим RGB и три выделенных ранее канала, чтобы объединить черно-белые изображения в одно цветное:
from PIL import Image
img = Image.open("picture.jpg")
r,g,b = img.split()
img2 = Image.merge(“RGB”, (r,g,b))
img.save(“o.jpg”)
Мы восстановили исходное изображение:

Строим гистограммы
Этот наглядный инструмент показывает, как распределены пиксели по значениям цветовых компонент. Гистограмма позволяет быстро понять особенности цветового состава изображения, что нам пригодится для дальнейшего анализа и обработки изображений.
Загружаем изображение в переменную im
и определяем его размеры с помощью im.size
, чтобы узнать общее количество пикселей, которые нужно обработать. Затем создаем пустой список, в котором будут храниться данные гистограммы.
Теперь нам нужно перебрать все пиксели изображения и извлечь значения каналов R, G и B для каждого пикселя. Добавляем полученное количество пикселей и их значения в список гистограммы.
import matplotlib.pyplot as plt
from PIL import Image
import numpy as np
import matplotlib
matplotlib.use(‘Tkagg’)
im = Image.open(“picture.jpg”)
plt.figure(figsize = (8,6))
plt.hist(nр.array(im).flatten(), bins = 256, color = ‘gray’)
plt.title(‘Histogram of the Image’)
plt.xlabel(‘Pixel Value’)
plt.ylabel(‘Frequency’)
plt.show()
Наконец, с помощью значений из списка строим три графика гистограммы, которые показывают распределение интенсивности для красного, синего и зеленого каналов:

Копируем и меняем размер
Сначала мы открываем исходное изображение и создаем его копию, чтобы работать с дубликатом и не вносить изменений в оригинал.
Затем снова открываем изображение и получаем его исходные размеры. После этого изменяем размер изображения пропорционально с помощью метода thumbnail()
. Он изменяет изображение и не возвращает новую копию, поэтому в результате оригинал заменяется уменьшенной версией. Если нужно, то полученное изображение можно сохранить отдельно.
Далее мы снова открываем исходное изображение, определяем его размеры и создаем версию с новыми параметрами методом resize()
. В отличие от thumbnail()
этот метод не затрагивает исходный файл, а возвращает отдельное изображение с заданными размерами. Его также можно сохранить отдельно.
Таким образом, thumbnail()
подходит для быстрого уменьшения размера, когда изменения оригинала допустимы, а resize позволяет получить изображение нужного размера без потери исходного файла.
from PIL import Image
img = Image.open("picture.jpg")
img2 = img.copy()
print("Размер копии:", img2.size)
img2.thumbnail((400, 300))
print("Новый размер после thumbnail:", img2.size)
imgrn = img.resize((300, 300)) # Создаем новое изображение размером 300x300
imgrn.save("be_like6.jpg")

Вырезаем фрагмент и меняем его размер
Открываем исходное изображение и с помощью метода crop()
выделяем прямоугольную область, задав ее координаты в виде кортежа box = (300, 300, 500, 500). Значения указывают левую верхнюю и правую нижнюю точки прямоугольника, который нам нужен.
Мы получили новое изображение, которое содержит только выделенный фрагмент. Теперь мы можем с ним работать, не меняя остальную часть картинки.
Методом resize()
изменим размеры фрагмента до 200x200 пикселей и получаем новое изображение. В отличие от crop()
он сохраняет оригинал без изменений.
from PIL import Image
img = Image.open("picture.jpg")
box = (300, 300, 500, 500) # (левая, верхняя, правая, нижняя границы)
img_crop = img.crop(box)
new_size = (200, 200)
img_resized = img_crop.resize(new_size)
img_resized.save("bee.jpg")
В результате мы получили уменьшенную версию выбранной области, при этом исходное изображение осталось без изменений:

Такой подход полезен, когда необходимо работать с отдельными фрагментами — например, для создания миниатюр, акцентирования на деталях или подготовки данных для задач машинного обучения.
Вращение и отражение изображения
Изменение ориентации изображения — полезная операция в обработке графических данных, особенно для задач дополнения данных (data augmentation) в машинном обучении и при создании эффектов в графических приложениях. Разберемся с основными способами вращения изображения с помощью библиотеки Pillow.
Открываем исходное изображение и получаем его размеры:
from PIL import Image
img = Image.open("picture.jpg")
print("Исходный размер изображения:", img.size)
Поворачиваем изображение на 90° без изменения размеров холста:
img_rotated_90 = img.rotate(90)
print("Размер после поворота на 90°:", img_rotated_90.size)
Поворачиваем изображение на 45°:
img_rotated_45 = img.rotate(45)
img_rotated_45.save("rotated_45.jpg")
Часть изображения обрезается, поэтому воспользуемся параметром (expand=True)
, чтобы автоматически увеличить размер холста:
img_rotated_45_expand = img.rotate(45, expand=True)
img_rotated_45_expand.save("rotated_45_expand.jpg")
Поворачиваем изображение на 180° с параметром (expand=True)
:
img_rotated_180_expand = img.rotate(180, expand=True)
img_rotated_180_expand.save("rotated_180_expand.jpg")

Запомним, что без expand=True
библиотека Pillow старается сохранить исходные размеры, поэтому изображение может быть обрезано. Если использовать этот параметр, то библиотека автоматически увеличит размеры холста, чтобы вместить повернутую картинку без обрезки.
А теперь перейдем к зеркальным отражениям. Они могут пригодится для подготовки дополнительных вариантов изображений в системах компьютерного зрения. Сделаем зеркальные отражения картинки по горизонтали и вертикали с помощью метода transpose()
:
img_flipped_horizontal = img.transpose(Image.FLIP_LEFT_RIGHT)
img_flipped_vertical = img.transpose(Image.FLIP_TOP_BOTTOM)
img_flipped_horizontal.save("flipped_horizontal.jpg")
img_flipped_vertical.save("flipped_vertical.jpg")
Закрашиваем изображения нужным цветом
Заливка частей или всей картинки выбранным цветом может понадобится, если нужно выделить какую-то область, подготовить изображение к обработке или сделать фон.
Закрасим прямоугольную область в верхнем левом углу с помощью метода paste()
. Для этого передадим ему кортеж RGB для красного цвета, которым хотим залить область, и координаты этой области:
from PIL import Image
img = Image.open("picture.jpg")
img.paste((255, 0, 0), (0, 0, 500, 500)) # Координаты: (левая, верхняя, правая, нижняя границы)
img.save("6lab1.jpg")
Теперь снова открываем наше изображение и полностью заливаем его зеленым цветом с помощью paste()
. Для чего передаем кортеж RGB для зеленого цвета и координаты всего изображения, которые мы получили методом getbbox()
:
img.paste((0, 128, 0), img.getbbox()) # getbbox() возвращает весь прямоугольник изображения
img.save("6lab2.jpg")

Проводим комбинированные манипуляции
Такие операции позволяют накладывать графические элементы, чтобы создавать новые изображения.
Разместим уменьшенную копию изображения в левый угол исходного и нарисуем красную рамку вокруг фрагмента, чтобы его выделить:
from PIL import Image
img = Image.open("picture.jpg")
img_small = img.resize((200, 150))
img.paste((255, 0, 0), (9, 9, 211, 161)) # Прямоугольник под рамку
img.paste(img_small, (10, 10))
Теперь добавим новый графический элемент. Нарисуем белую полупрозрачную горизонтальную полосу и наложим ее на изображение:
white_bar = Image.new("RGB", (img.size[0], 100), (255, 255, 255)) # Белая полоса
mask = Image.new("L", (img.size[0], 100), 160) # Маска с прозрачностью
img.paste(white_bar, (0, 0), mask)
Наконец, вставим уменьшенное изображение в центр исходного:
box = (200, 200, 400, 400)
img_crop = img.crop(box)
img_resized = img_crop.resize((300, 300))
img_rotated = img_resized.rotate(90)
img.paste(img_rotated, (230, 80))
img.save("picturik.jpg")

Выводим координаты клика по точке на картинке
Теперь напишем программу, которая умеет отслеживать и сохранять координаты кликов пользователя по изображению. Инструмент может пригодиться, например, для анализа взаимодействия пользователя с картинкой, выявления популярных областей или сбора данных для машинного обучения.
from PIL import Image
import matplotlib.pyplot as plt
import numpy as np
img = np.array(Image.open("picture.jpg"))
plt.imshow(img)
plt.axis('off')
print("Кликните на 3 точки на изображении")
points = plt.ginput(3) # Ожидаем 3 клика мышью
print("Координаты кликов:", points)
plt.show()
Пользователь сделал три клика, и программа вывела координаты этих точек в консоль:
Координаты кликов: [(123.5, 245.2), (300.7, 150.4), (450.3, 75.8)]
Рисуем графические элементы
Для начала нам нужно импортировать две библиотеки:
from PIL import Image
— чтобы работать с изображениями,import matplotlib.pyplot as plt
— чтобы использовать функций Matplotlib
Выведем изображение на экран с помощью функции imshow(img)
из Matplotlib и временно отключим отображение координатных осей с помощью axis('off')
. Важно понимать, что при этом рисование происходит не на самом изображении, а на холсте визуализации Matplotlib. То есть оригинальный файл не изменяется — мы работаем с его копией в виде массива.
Теперь определим координаты точек, которые хотим визуализировать, — для этого создадим списки x и y, которые содержат значения координат.
Нанесем две серии точек с помощью функции plot()
из Matplotlib. Первая серия точек — это зеленые кружочки, которые соединены линией (plot(x,y,'go-'))
, а вторая — точки без линии, которые построены по первым двум элементам списков x и y (plot(x[:2], y[:2]))
.
Наконец, добавим заголовок plotting: 'picture.jpg' с помощью функции title()
, чтобы подписать получившуюся визуализацию.
from PIL import Image
import matplotlib.pyplot as plt
import numpy as np
img = np.array(Image.open("picture.jpg"))
plt.imshow(img)
plt.axis('off')
x = [100, 100, 400, 400]
y = [200, 500, 200, 500]
plt.plot(x, y, 'go-') # 'g' — зеленый цвет, 'o' — кружки, '-' — линии
plt.plot(x[:2], y[:2], 'bo-') # Дополнительно синим цветом
plt.title('Plotting: "picture.jpg"')
plt.show()

Обратите внимание: если необходимо не просто визуализировать, а сохранить изображение с нанесенными элементами, лучше использовать библиотеку Pillow и модуль ImageDraw
, которые позволяют вносить изменения непосредственно в изображение.
Обрабатываем картинки с помощью фильтров
С помощью фильтрации можно выделять детали или создавать художественные эффекты на изображениях. Разберемся, как работают три встроенных фильтра из модуля ImageFilter
библиотеки Pillow.
Импортируем нужные модули:
from PIL import Image, ImageFilter
Загружаем исходное изображение:
img = Image.open("picture.jpg")
Теперь применим три разных фильтра и посмотрим, как они работают и для чего нужны.
EMBOSS — фильтр, создающий эффект тиснения (embossing), придает изображению вид трехмерной текстуры и ощущение рельефности. Подходит для художественной обработки и стилизации изображений:
img_emboss = img.filter(ImageFilter.EMBOSS)
img_emboss.save("first_filter.jpg")

FIND_EDGES — фильтр, который выделяет границы объектов (edge detection). Это полезно для анализа форм, сегментации, построения контурных моделей, создания технических схем и предобработки перед детектированием:
img_edges = img.filter(ImageFilter.FIND_EDGES)
img_edges.save("second_filter.jpg")

CONTOUR — подчеркивает основные границы объектов, создавая четкий и графический вид:
img_contour = img.filter(ImageFilter.CONTOUR)
img_contour.save("third_filter.jpg")

Строим контуры
Этот инструмент выделяет структурные особенности изображения. Функция Contour()
, как и одноименной фильтр, пригодится, если нужно создать техническую схему, иллюстрацию или визуализировать структуру изображения.
Загружаем исходную картинку и преобразуем ее в оттенки серого — с черно-белым изображением работать будет проще.

Вызываем функцию plt.contour()
из Matplotlib, которая визуализирует линии равной интенсивности на изображении, чтобы выделить контуры объектов. Также отключим отображение осей координат с помощью plt.axis('off')
, чтобы сосредоточиться на самой картинке.
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
img = np.array(Image.open("picture.jpg").convert('L'))
plt.figure()
plt.gray() # Установить палитру серых оттенков
plt.contour(img, origin='image')
plt.axis('equal')
plt.axis('off')
plt.show()
Мы получили изображение с четко выделенными границами и контурами:

Мы рассмотрели базовые функции для работы с изображениями в библиотеки Pillow и познакомились с фильтрами Matplotlib. Теперь разберемся, как обнаруживать объекты на картинках.
Распознаем объекты с помощью модели YOLOX
YOLOX — это легковесная и быстрая модель обнаружения объектов, которая обеспечивает хороший баланс между точностью и скоростью. Она хорошо работает на одноплатных компьютерах, таких как Lichee Pi 4A, за счет оптимизированной архитектуры.
Для начала работы с YOLOX клонируем ее официальный репозиторий для архитектуры RISC-V, который содержит примеры. На вики Lichee Pi 4A есть подробная инструкция для этой модели.
Теперь настроим среду. Экосистема ПО для RISC-V активно развивается, поэтому загрузим и установим предварительно скомпилированные версии необходимых Python-библиотек. Обратите внимание, что нужно установить специальную версию ONNX Runtime, которая оптимизирована для процессоров Gentej. Документация к Lichee Pi 4A рекомендует модифицированную версию HHB-onnxruntime, которая оптимизирована для платформы SHL.
Подготовим скрипт для запуска: в demo/ONNXRuntime нужно адаптировать пути к библиотекам и зависимостям для корректной работы на одноплатном компьютере.
Для запуска инференса используем скрипт onnx_inference.py
и указываем:
путь к предобученной модели YOLOX-S в формате ONNX,
путь к изображению для обработки,
выходной каталог для сохранения результатов,
параметры обнаружения: порог вероятности и размер входного изображения.

В результате работы скрипта мы получили новое изображение, на котором:
обнаруженные объекты выделены прямоугольниками,
у каждого объекта есть метка класса и степень уверенности модели (confidence score),
модель корректно распознала на картинке два человека и мяч.
Заключение
Одноплатный компьютер Lichee Pi 4A на архитектуре RISC-V доказал, что его вычислительных мощностей достаточно для обучения основами компьютерного зрения — от базовой обработки изображений с Python-библиотекой Pillow до распознавания объектов с моделью YOLOX.
Уверены, что наш опыт будет полезен начинающим инженерам, которые хотят изучить основы компьютерного зрения и обработки изображений. Работа с Lichee Pi 4A поможет получить базовые навыки адаптации и оптимизации ПО под новые аппаратные решения, а это особенно важно, если вы планируете работать в сферах интернета вещей, встроенных систем или Edge AI.
Если остались вопросы или хотите поделиться своим опытом — пишите в комментариях, будем рады обсудить.
Комментарии (4)
rPman
27.05.2025 13:11Какие интересные предложения у sipeed, и мини кластер на этих платах, и ноутбук и fpga платы с девбордой (платы в sodim ddr3 слот)... или к примеру микрокомпьютер (на три сантиметра)
ponikrf
27.05.2025 13:11Сейчас в плане железа все ушло очень далеко. Risc-v набирает популярность. И даже появились интересные варианты по доступным ценам (привет эльбрус/байкал) например Milk-V Jupiter. Улучшается поддержка периферии. И если 3 года назад про risc-v говорили как о чем то космическом, то сегодня это уже работает и даже есть из чего выбрать. На ютубе explainingcomputers обычно разбирает самые актуальные в доступном сегменте подобные железки.
FenixFly
27.05.2025 13:11Раз уж это дело показывается на RISC-V процессоре, то было бы полезно в статье раскрыть больше информации про то, как и кем собрана pillow под RISC-V (и откуда она устанавливается), какая у нее производительность например в сравнении с Raspberry. Сборка Pillow с какими-нибудь оптимизациями под RISC-V и ее бенчмаркинг тянет на отдельную статью.
PuknulProgrammistamPodNos
Добрый день! Большое спасибо за вашу статью, было не очень интересно читать, но во многом из-за того, что я сам хорошо знаком с компьютерным зрением. И обучал машины "видеть" я ещё тогда, когда многие программисты не умели правильно нажимать на кнопки клавиатуры. Из-за этого, конечно же, в последствии я был вынужден уйти в преподавание и вести курсы по программированию (преподавал как раз-таки основы нажимания на клавиши). Сейчас работаю макакой в вэбе, и в целом всё устраивает: труд здесь обезьяний, но зарплаты - конские. Впрочем, как и на любой другой айти-вакансии (и вас, надеюсь, работодатель не обделил зарплатой в 1000000кк/нс). Но душой и сердцем я всё равно лелею научные айти-направления, в том числе и нейронные сети. До сих пор, бывает, достаю свой одноплатный компьютер BeagleBone Black и запускаю на нём самолично обученную нейронку. Моя гордость, она создана чтобы имитировать работу обычного среднестатистического программиста. Сперва я не совсем это понял, ведь когда запустил её, компьютер ушёл в мёртвое зависание. Я долго кусал локти и размышлял, что же пошло не так. Не хватило мощей? или где-то ошибка в коде? а может и вовсе какая-то утечка памяти. Но потом я наконец осознал - никаких ошибок нет. Всё работает верно. Ведь мы, айтишники, на самом деле только и занимаемся на работе тем, что ничем не занимаемся. Лежим в своих уютных креслах, плюём в потолок. Мозги не думают, разумом мы витаем где-то в облаках. Собственно, поковыряв свой одноплатный компьютер с запущенной на нём нейронкой, я как раз-таки и обнаружил, что всё это время компьютер находится в режиме простоя, хотя сам SoC при этом грелся неимоверно - настолько прекрасно нейросеть имитировала рабочую айти-деятельность. Но, думаю, что вы, будучи айтишником, и сами прекрасно понимаете о чём я. Не вижу в программистском безделье и балованьи библиотекой pillow ничего плохого (особенно если этого до сих пор не уловило ни моё, ни ваше руководство), однако хотелось бы почитать про какие-то более узко-направленные вещи. Скажем, дообучали ли вы как-то модель или это она прямо сама сходу смогла распознать людей и мяч; как выглядит код с "скармливанием" нейронной сети картинки; выдаёт ли нейронка сама на выход вот эту картинку с оранжевыми рамочками и опознанными объектами или это делали вы сторонними методами... и прочее. Или вы и впрямь только запустили демопроект и всё? В общем, хотелось бы почитать настоящий инженерный опыт работы с узкоспециализированной штукой, а не лицезреть в очередной раз обычный айти-программистский опыт. Ну и ещё, вы выбрали для этих целей Lichee Pi 4A просто потому что он у вас уже когда-то был куплен и лежал вот до поры до времени, пылился? Всё-таки сейчас на рынке присутствует огромное количество одноплатных компьютеров нового поколения, которые как раз-таки и предназначены для работы с нейронными сетями. Погуглите, особенно присмотритесь к компьютеру под названием iPhuck. Я думаю, что для решения многих ваших задач его технических характеристик хватит впроголодь.