Лабораторна робота 9
Лабораторна робота №09. Python-модуль камери
Мета: навчитися проєктувати єдиний апаратно незалежний інтерфейс камери; реалізувати mock відкриття потоку, отримання кадру з часовою позначкою, контролю роздільної здатності й області інтересу, виявлення втрати/порушення послідовності та безпечного збереження діагностичного кадру без персональних метаданих.
Результати навчання та передумови
Після виконання студент уміє:
- визначати контракт
open/read/status/closeнезалежно від Picamera2, V4L2 або OpenCV; - розрізняти часову позначку захоплення, час обробки й номер послідовності;
- перевіряти геометрію кадру та область інтересу (ROI);
- виявляти кінець потоку, надмірний часовий розрив і немонотонний час;
- не повторно використовувати «останній добрий кадр» після втрати потоку;
- зберігати мінімальний діагностичний артефакт без EXIF, GPS, мережевих даних або ідентифікаторів людей;
- документувати параметри камери, які ще не підтверджені на MERC-I5.
Передумови: Python (Protocol, dataclass, bytes, pathlib, винятки), основи цифрового зображення та ЛР-04–08. OpenCV не потрібний для обов'язкової частини.
Середовище виконання
| Середовище | Статус | Примітка |
|---|---|---|
| Google Colab | дозволено | Лише синтетичні кадри; камера браузера не використовується |
| Локальний ПК | основне | Самодостатній Python mock зі стандартною бібліотекою |
| Raspberry Pi | дозволено | Тільки mock; не відкривати CSI/USB/V4L2 у цій редакції |
| Фізичний комплекс MERC-I5 | не потрібен | Камери не активуються; фото/відео людей не збираються |
Точні версії Python, Raspberry Pi OS, Picamera2, libcamera й OpenCV для курсу — потребують перевірки.
Необхідні знання та матеріали
- апаратний опис камер, програма ЛР-09 і перелік відсутніх параметрів;
- правила ПЗ і конфігурації, безпека та допуск студентів;
- Python зі стандартними модулями
dataclasses,typing,pathlib,tempfile,json; - текстовий редактор і термінал;
- синтетичні одноканальні кадри, які створює код нижче.
Ризики та правила безпеки
Ризики
| Небезпека | Наслідок | Захист у роботі |
|---|---|---|
| Камера втратила потік | застаріле зображення породжує неправильне рішення | помилка StreamLost; останній кадр не повертається повторно |
| Неправильний ROI/розмір | об'єкт обрізано або координати хибні | перевірка меж до crop |
| Людина в кадрі | порушення приватності | лише синтетичні дані; safe-smoke кодує PGM у пам'яті без metadata |
| Автоекспозиція змінила зображення | нестабільні ознаки | параметр фіксується у паспорті; у mock — явна конфігурація |
| Робота біля рухомої комірки | удар або захоплення | камеру фізично не налаштовувати під час активного обладнання |
Заборонені дії
- відкривати
/dev/video*, Picamera2, OpenCVVideoCaptureабо USB/CSI у цій редакції; - переставляти камеру, кабель чи освітлення на активному стенді;
- знімати людей, документи, екрани з обліковими даними або інші персональні/секретні дані;
- мовчки масштабувати чи обрізати кадр після невірної конфігурації;
- продовжувати автоматичний сценарій після втрати або застарівання кадру;
- видавати синтетичні часові метрики за вимірювання Raspberry Pi.
Умови негайної зупинки
Для майбутньої фізичної частини: людина в небезпечній зоні, рух механізму, пошкоджений/перетиснутий кабель, нестабільне живлення, перегрів, втрата кадрів або невідома орієнтація. Для mock: немонотонний час, надмірний розрив, неправильна довжина pixel buffer, ROI поза межами, кінець потоку або необроблений виняток.
Безпечний стан
Потік закритий, нові кадри не видаються, останній кадр позначений недійсним для керування, причина збережена, downstream отримує помилку й не продовжує рух. Діагностичний кадр зберігається лише після перевірки вмісту та політики даних.
Передпусковий чекліст
- Джерело кадрів —
MockCamera, не фізичний backend. - Код не імпортує
cv2,picamera2і не звертається до пристроїв. - Розмір buffer дорівнює
width × heightдля навчального grayscale. - Часові позначки монотонні, а допустимий розрив визначений у mock.
- ROI лежить у межах кадру.
- Тимчасовий кадр не містить персональних даних і видаляється автоматично.
Хід виконання роботи
- Визначити модель кадру, конфігурацію та контракт adapter-а.
- Реалізувати й запустити
MockCamera. - Перевірити послідовність, timestamp і ROI.
- Перевірити втрату, часовий розрив і пошкоджений кадр.
- Зберегти мінімальний діагностичний PGM без метаданих.
- Заповнити паспорт, метрики й звіт.
Методичні вказівки й теоретичні відомості
1. Кадр як перевірювана подія
Кадр — це не лише масив пікселів. Для відтворюваності потрібні геометрія, формат, номер послідовності та час захоплення. У цій роботі використано одноканальний 8-бітний mock, тому довжина buffer однозначно дорівнює width × height.
| Поле | Призначення | Перевірка |
|---|---|---|
sequence | порядок кадрів | строго зростає на 1 у тестовому потоці |
timestamp_ns | час захоплення mock | строго зростає; gap не перевищує межу mock |
width, height | геометрія | додатні; узгоджені з buffer |
pixels | дані grayscale | рівно один байт на піксель |
source | ідентифікатор backend | несекретний стабільний label, не шлях пристрою |
2. ROI
ROI задають як (x, y, width, height) від верхнього лівого кута. Гранична умова: x + width <= frame.width, y + height <= frame.height. Crop не повинен мовчки підрізати область за межами.
3. Втрата потоку
Повернення останнього кадру після втрати камери створює ілюзію свіжих даних. Правильний adapter припиняє видачу, позначає стан помилки й передає її споживачу. Пороговий gap конкретної MERC-I5 залежить від фактичної частоти та сценарію; число mock не переноситься на стенд.
4. Потік даних
Текстовий опис рисунка: після відкриття adapter видає новий Frame; геометрія, sequence і timestamp перевіряються до crop та передавання алгоритму, а будь-яка втрата потоку чи невалідність переводить джерело у FAULT, закриває потік і забороняє downstream продовжувати керівний сценарій.
flowchart LR
A["CameraAdapter.open"] --> B["Отримати Frame"]
B --> C{"Геометрія і час валідні?"}
C -->|"так"| D["Crop ROI"]
D --> E["Алгоритм споживача"]
E --> F["Метрики / мінімальний diagnostic"]
C -->|"ні або stream lost"| G["FAULT: кадр недійсний"]
G --> H["Закрити потік; рух не продовжувати"]
Рис. 1. Ілюстративний конвеєр валідації кадру без фізичної камери
5. Конфігурація mock поза кодом
Приклад camera.mock.json:
{
"backend": "mock",
"width": 4,
"height": 3,
"max_gap_ns": 1000,
"roi": [1, 1, 2, 2],
"diagnostic_format": "PGM"
}
Малі геометрія й gap у цьому файлі призначені тільки для швидкого test fixture й не описують камеру MERC-I5. Runtime читає JSON окремо, перевіряє whitelist ключів, додатність розмірів, межі ROI та backend == "mock"; фізичні device id, FPS, exposure й версії не додають до профілю до інвентаризації.
6. Першоджерела
- Raspberry Pi Camera Software — офіційний опис
rpicam-apps, Picamera2 та USB webcam; legacy camera stack не використовується як нова основа. - Офіційний Picamera2 manual — модель запитів, конфігурацій і metadata для майбутнього CSI backend.
- Linux V4L2 userspace API — первинна документація Linux для майбутнього USB backend.
- Python:
timeі Python:pathlib— часові позначки та безпечні шляхи. - Апаратна частина MERC-I5 і базова лінія ПЗ — локальні обмеження конкретного курсу.
Виконання лабораторної роботи
Крок 1. Визначити контракт і конфігурацію
Запишіть, що backend має повертати Frame, а не «будь-який масив». Не вказуйте /dev/video0: порядок device node не є паспортом камери.
open() -> потік OPEN або CameraError
read() -> новий валідний Frame або StreamLost
crop(frame, roi) -> новий Frame або ValueError
close() -> CLOSED; read заборонений
Очікуваний результат: контракт містить стан, timestamp, sequence, геометрію та помилки.
Критерій правильності: після StreamLost неможливо отримати старий кадр як новий.
Якщо результат не отримано: відокремте кеш/preview від керівного потоку й додайте явний стан недійсності.
Крок 2. Реалізувати повний mock
Збережіть як lab09_camera_mock.py.
# safe-smoke
from __future__ import annotations
import json
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Protocol
class CameraState(str, Enum):
CLOSED = "CLOSED"
OPEN = "OPEN"
FAULT = "FAULT"
class CameraError(RuntimeError):
pass
class StreamLost(CameraError):
pass
@dataclass(frozen=True)
class Frame:
width: int
height: int
pixels: bytes
timestamp_ns: int
sequence: int
source: str = "mock"
def __post_init__(self) -> None:
if self.width <= 0 or self.height <= 0:
raise ValueError("геометрія кадру має бути додатною")
if len(self.pixels) != self.width * self.height:
raise ValueError("buffer не відповідає grayscale-геометрії")
if self.timestamp_ns < 0 or self.sequence < 0:
raise ValueError("timestamp/sequence не можуть бути від'ємними")
@dataclass(frozen=True)
class CameraStatus:
state: CameraState
delivered: int
last_error: str | None
class CameraAdapter(Protocol):
def open(self) -> None: ...
def read(self) -> Frame: ...
def close(self) -> None: ...
def status(self) -> CameraStatus: ...
@dataclass(frozen=True)
class MockCameraConfig:
width: int
height: int
max_gap_ns: int
def __post_init__(self) -> None:
if self.width <= 0 or self.height <= 0 or self.max_gap_ns <= 0:
raise ValueError("невалідна конфігурація mock-камери")
class MockCamera:
def __init__(self, config: MockCameraConfig, frames: list[Frame]) -> None:
self.config = config
self.frames = list(frames)
self.index = 0
self.state = CameraState.CLOSED
self.last_error: str | None = None
self.last_frame: Frame | None = None
def open(self) -> None:
if self.state == CameraState.FAULT:
raise CameraError("потрібна ручна перевірка після FAULT")
self.state = CameraState.OPEN
def _fail(self, message: str) -> None:
self.state = CameraState.FAULT
self.last_error = message
raise StreamLost(message)
def read(self) -> Frame:
if self.state != CameraState.OPEN:
raise CameraError("потік не відкритий")
if self.index >= len(self.frames):
self._fail("потік завершився")
frame = self.frames[self.index]
self.index += 1
if (frame.width, frame.height) != (self.config.width, self.config.height):
self._fail("геометрія змінилася")
if self.last_frame is not None:
if frame.sequence != self.last_frame.sequence + 1:
self._fail("порушено послідовність кадрів")
gap = frame.timestamp_ns - self.last_frame.timestamp_ns
if gap <= 0 or gap > self.config.max_gap_ns:
self._fail("часовий розрив потоку недопустимий")
self.last_frame = frame
return frame
def close(self) -> None:
self.state = CameraState.CLOSED
def status(self) -> CameraStatus:
return CameraStatus(self.state, self.index, self.last_error)
def crop(frame: Frame, roi: tuple[int, int, int, int]) -> Frame:
x, y, width, height = roi
if x < 0 or y < 0 or width <= 0 or height <= 0:
raise ValueError("ROI має додатний розмір і невід'ємний початок")
if x + width > frame.width or y + height > frame.height:
raise ValueError("ROI виходить за межі кадру")
rows = []
for row in range(y, y + height):
start = row * frame.width + x
rows.append(frame.pixels[start:start + width])
return Frame(width, height, b"".join(rows), frame.timestamp_ns, frame.sequence, frame.source)
def encode_diagnostic_pgm(frame: Frame) -> bytes:
"""Повертає PGM у пам'яті без source/timestamp та інших metadata."""
header = f"P5\n{frame.width} {frame.height}\n255\n".encode("ascii")
return header + frame.pixels
def save_diagnostic_pgm(frame: Frame, path: Path) -> None:
"""Опційний запис у явно дозволений студентський workspace."""
path.write_bytes(encode_diagnostic_pgm(frame))
def make_frame(sequence: int, timestamp_ns: int, value: int) -> Frame:
return Frame(4, 3, bytes([value] * 12), timestamp_ns, sequence)
config = MockCameraConfig(width=4, height=3, max_gap_ns=1_000)
camera: CameraAdapter = MockCamera(config, [make_frame(0, 10_000, 7), make_frame(1, 10_500, 8)])
camera.open()
first = camera.read()
second = camera.read()
assert first.sequence == 0 and second.timestamp_ns > first.timestamp_ns
region = crop(second, (1, 1, 2, 2))
assert (region.width, region.height, region.pixels) == (2, 2, bytes([8] * 4))
try:
camera.read()
except StreamLost as exc:
assert "завершився" in str(exc)
else:
raise AssertionError("кінець потоку має бути виявлений")
assert camera.status().state == CameraState.FAULT
gap_camera = MockCamera(config, [make_frame(0, 1_000, 1), make_frame(1, 2_001, 2)])
gap_camera.open()
gap_camera.read()
try:
gap_camera.read()
except StreamLost as exc:
assert "розрив" in str(exc)
else:
raise AssertionError("надмірний gap має бути виявлений")
try:
crop(first, (3, 2, 2, 2))
except ValueError:
pass
else:
raise AssertionError("ROI поза кадром має бути відхилений")
data = encode_diagnostic_pgm(region)
assert data.startswith(b"P5\n2 2\n255\n") and data.endswith(bytes([8] * 4))
assert b"mock" not in data and b"timestamp" not in data
print(json.dumps({"checks": 7, "result": "PASS", "hardware": False}, ensure_ascii=False))
python -m py_compile lab09_camera_mock.py
python lab09_camera_mock.py
Очікуваний результат: JSON checks=7, result=PASS, hardware=false.
Критерій правильності: код повертає 0, не відкриває камеру/мережу й перевіряє PGM як bytes у пам'яті без файлового запису.
Якщо результат не отримано: не переходьте до фізичного backend; збережіть traceback, перевірте геометрію синтетичних кадрів і межі ROI.
Крок 3. Проаналізувати штатний потік і ROI
Простежте кадри sequence=0,1, gap 500 ns у синтетичному тесті та ROI (1,1,2,2). Ці часові числа навмисно навчальні й не моделюють частоту реальної камери.
sequence: 0 -> 1
timestamp_ns: 10000 -> 10500
frame: 4 x 3
ROI: x=1, y=1, width=2, height=2
Очікуваний результат: ROI має 4 байти зі значенням 8, початковий timestamp збережено.
Критерій правильності: crop не змінює sequence/timestamp і не виходить за межі.
Якщо результат не отримано: перевірте індекс рядка row * frame.width + x; не застосовуйте мовчазне clipping.
Крок 4. Перевірити втрату й часові аномалії
Зіставте три негативні випадки: кінець списку, gap понад max_gap_ns, ROI поза кадром. Додайте власний тест із повторним sequence.
python lab09_camera_mock.py
Очікуваний результат: потік переходить у FAULT, а не повертає попередній frame.
Критерій правильності: downstream отримує контрольований виняток до будь-якого керівного рішення.
Якщо результат не отримано: видаліть fallback «повернути last_frame»; зафіксуйте причину і закрийте потік.
Крок 5. Перевірити діагностичний артефакт
Safe-smoke кодує PGM лише в пам'яті. Переконайтеся, що bytes містять лише заголовок P5, розмір, максимум 255 і чотири пікселі; source та timestamp не записані. Функцію save_diagnostic_pgm студент може окремо перевірити лише у власному явно дозволеному workspace, після чого видалити тестовий файл.
Дозволено: мінімальний crop об'єкта, технічна геометрія, знеособлений ідентифікатор тесту
Заборонено: обличчя, ПІБ, екрани з токенами, EXIF/GPS, приватні IP, серійні номери без потреби
Очікуваний результат: автоматично перевірений in-memory артефакт; за окремої локальної файлової перевірки файл створюється лише в дозволеному каталозі й видаляється після тесту.
Критерій правильності: байти не містять службових рядків, а звіт не публікує сам файл без потреби.
Якщо результат не отримано: не зберігайте кадр; спершу мінімізуйте ROI, метадані й строк зберігання.
Крок 6. Заповнити паспорт камери
| Поле | Результат цієї редакції |
|---|---|
| API | CameraAdapter.open/read/status/close |
| Формат mock | 8-bit grayscale, один байт на піксель |
| Timestamp | синтетичний монотонний ns |
| ROI | (x, y, width, height), строгі межі |
| Реакція на втрату | FAULT, кадр недійсний |
| Diagnostic | автоматично перевірений PGM у пам'яті без metadata; файловий запис — окремий локальний тест |
| Фізичні backend-и | не підтверджено й не перевірено |
Очікуваний результат: паспорт не змішує mock із IMX708 або USB-камерою.
Критерій правильності: кожна апаратна характеристика має джерело або статус «не підтверджено».
Якщо результат не отримано: вилучіть припущені /dev/video*, FPS, exposure й версії; сформуйте перелік read-only даних для інвентаризації.
Сценарії перевірки
| Тип | Сценарій | Очікування |
|---|---|---|
| позитивний | два послідовні кадри | sequence і timestamp зростають |
| позитивний | ROI всередині 4×3 | результат 2×2, 4 байти |
| негативний | кадри закінчилися | StreamLost, FAULT |
| негативний | gap > max | StreamLost, старий кадр не повертається |
| негативний | buffer неправильної довжини | ValueError під час створення Frame |
| граничний | ROI торкається правої/нижньої межі | приймається, якщо дорівнює межі |
| граничний | ROI на 1 піксель поза кадром | відхиляється |
Таблиці вимірювань і метрики
| Кадр/тест | Sequence | Timestamp mock, ns | Gap, ns | Розмір | ROI | Стан | PASS/FAIL |
|---|---|---|---|---|---|---|---|
| 1 | — | ||||||
| 2 | |||||||
| lost | — | — | — | — | — | ||
| gap | — |
Метрики: 100% тестів PASS; 0 повторно виданих старих кадрів; 0 невалідних ROI, що пройшли; 0 файлових записів у safe-smoke; 0 апаратних відкриттів.
[!WARNING] [ПОТРЕБУЄ ДОПРАЦЮВАННЯ: АПАРАТНА АПРОБАЦІЯ КАМЕРИ ТА ПОЛІТИКА ДІАГНОСТИЧНИХ КАДРІВ] Не підтверджено: стабільність потоку, реальні FPS/latency/gap, експозиція, фокус, роздільна здатність, ROI, освітлення, вміст фону, права на зображення, строк зберігання й критерій втрати для конкретної камери MERC-I5; фізичний захват кадру не виконувався. Що потрібно додати: затверджений профіль камери й версій ПЗ, еталонну сцену без людей, правила приватності/публікації, набір часових журналів і діагностичних кадрів, критерії якості та відповідального оператора. Як завершити: після окремого дозволу виконати read-only захват нерухомої безпечної сцени, виміряти sequence/timestamp/gap серією кадрів, перевірити ROI та втрату потоку без руху механізмів, провести privacy-review; додати датовані результати й видалити блок лише після затвердження.
Вимоги до звіту
Див. загальні вимоги. Додатково подайте:
- контракт
CameraAdapterі модельFrame; - повний код mock та JSON smoke-результат;
- таблицю sequence/timestamp/ROI для всіх сценаріїв;
- аналіз небезпеки повторного використання старого кадру;
- опис формату PGM і перевірки відсутності metadata;
- паспорт із розділенням mock та майбутніх backend-ів;
- явну відмітку, що фізичні камери не відкривалися.
Критерії оцінювання
| Складова | Частка | Критерії |
|---|---|---|
| Підготовка | 10% | контракт, джерела, privacy/safety-чекліст, відсутність припущених пристроїв |
| Реалізація | 40% | Frame, CameraAdapter, mock, ROI, втрата потоку, diagnostic PGM |
| Перевірка | 25% | позитивні, негативні й граничні тести; код 0; жодного I/O камери |
| Аналіз | 15% | часові аномалії, stale frame, ROI, приватність і межі моделі |
| Звіт | 10% | таблиці, паспорт, джерела, висновки й чесний статус апробації |
| Разом | 100% |
Відкриття фізичної камери без дозволу або публікація персональних/секретних даних є критичною помилкою.
Контрольні питання
- Чому frame має містити timestamp і sequence?
- Чим час захоплення відрізняється від часу обробки?
- Чому не можна повертати останній кадр після втрати потоку?
- Які умови роблять ROI валідним?
- Навіщо adapter приховує Picamera2/V4L2/OpenCV?
- Чому
/dev/video0не є стабільним паспортним ідентифікатором? - Які дані можуть зробити діагностичний кадр чутливим?
- Які вимірювання потрібні для вибору реального
max_gap?
Висновки
У роботі реалізується детермінований CameraAdapter-мock із валідованими кадрами, ROI, часовою послідовністю, явною втратою потоку та мінімальним PGM, автоматично перевіреним у пам'яті. Код не залежить від камери й не збирає реальні зображення. Моделі пристроїв, режими, версії та метрики MERC-I5 залишаються адресно заблокованими до read-only інвентаризації й затвердженої апробації.