Лабораторна робота 18
Лабораторна робота №18. Візуальне сортування деталей
Мета: реалізувати й перевірити в режимі dry-run послідовність «виявлення — підтверджена зупинка — класифікація — перевірка координат — вибір маршруту», виміряти частку успішних циклів і довести безпечну реакцію на невпевненість, тайм-аут та втрату підсистеми без керування фізичним обладнанням.
Результати навчання та передумови
Після роботи студент уміє:
- координувати сенсор, конвеєр, камеру, класифікатор і планувальник через явні стани;
- відрізнити піксельні, нормалізовані та роботні координати;
- не створювати план захоплення до підтвердженої зупинки й валідної трансформації;
- спрямувати
UNKNOWNдо символічної зони ручної перевірки; - ін'єктувати відмови й перевірити тайм-аути;
- обчислити success rate, reject rate, fault rate та latency dry-run.
Передумови: ЛР-05, ЛР-06, ЛР-09, ЛР-16 та концепції ЛР-17. У цій редакції координатне перетворення завершується нормалізованою точкою, а не командою робота.
Середовище виконання
| Середовище | Статус | Примітка |
|---|---|---|
| Google Colab | дозволено | Стандартна бібліотека Python, JSON-fixtures |
| Локальний ПК | основне | Повний dry-run без мережі й апаратних бібліотек |
| Raspberry Pi | дозволено | Лише mock, без GPIO, камер і мережі MG400 |
| Симуляція | основне | Логічний час, символічні маршрути й нормалізовані координати |
| Фізичний комплекс MERC-I5 | не використовується | Рух і I/O не дозволені в цьому запуску |
Необхідні знання та матеріали
- Python зі стандартною бібліотекою;
- два JSON-файли з цього документа;
- політика безпеки студентів;
- результати класифікатора ЛР-16 та концепція перевірки області ЛР-17;
- жодні IP-адреси, GPIO, координати стенда чи секрети не потрібні.
Ризики та правила безпеки
Ризики
Непідтверджена зупинка може дати розмитий кадр і хибні координати; невірна калібровка — неправильну ціль; низька впевненість — хибний маршрут; повторна подія сенсора — подвійний цикл. Dry-run перевіряє логіку, але не фізичні межі й гальмування.
Заборонені дії
- не викликати MG400, грипер, конвеєр, подавач або GPIO;
- не підставляти нормалізовані координати як міліметри;
- не планувати захоплення без
stop_ack,camera_ok, класу й перевірки ROI; - не продовжувати автоматично після
FAULT, перезапуску чи суперечливого стану; - не «виправляти»
UNKNOWNвибором найближчого класу.
Умови негайної зупинки
Тайм-аут сенсора або зупинки, втрата камери, координата поза ROI, дублікат активного part_id, нечислове значення, відсутній маршрут чи невідомий стан переводять dry-run у FAULT або REJECT без подальших дій.
Безпечний стан
Усі mock-виходи мають значення STOP; план містить лише символічні записи, а не рухові команди. Після FAULT потрібні аналіз причини та ручний acknowledge, якого базовий автомат цього завдання навмисно не виконує.
Передпусковий чекліст
- Скрипт імпортує лише стандартну бібліотеку.
- У конфігурації немає IP, портів, GPIO і фізичних координат.
- ROI і трансформація позначені як синтетичні.
- Усі адаптери є
DryRunAdapter. - Підготовлено позитивний, негативний і граничний сценарії.
- Фізичні модулі від'єднані від програмного процесу.
Хід виконання роботи
- Перевірити політику маршрутизації safe-smoke.
- Створити зовнішню конфігурацію й fixtures.
- Реалізувати dry-run автомат.
- Запустити пакет сценаріїв та зібрати журнал переходів.
- Розрахувати метрики й визначити межі фізичного перенесення.
Методичні вказівки й теоретичні відомості
1. Послідовність і блокування
Сенсорна подія лише починає цикл. Вона не доводить, що деталь нерухома або розташована в зоні камери. Наступний gate — підтвердження зупинки. Потім камера й класифікатор дають спостереження; лише після перевірки confidence та ROI дозволено обчислити абстрактну ціль.
2. Координатні системи
(u, v) — піксельна точка кадру. У dry-run вона переходить у (x_norm, y_norm) в межах 0…1. Це дає перевірювану логіку «в області / поза областю», але не є калібруванням robot frame. Реальне перетворення має походити з ЛР-17 і незалежно перевірятися за похибкою.
3. Рішення та невизначеність
COMPLETE означає лише сформований і перевірений символічний план. REJECT — деталь ідентифікована недостатньо надійно й лишається у визначеній контрольній позиції. FAULT — підсистема або протокол не дають безпечного продовження. У жодному з цих станів dry-run не рухає механізми.
Текстовий опис рисунка: автомат починає з очікування деталі, вимагає підтвердженої зупинки, інспекції, валідної координати й символічного маршруту; невпевненість веде у REJECT, а тайм-аут, втрата камери чи вихід за ROI — у FAULT.
stateDiagram-v2
[*] --> WAIT_PART
WAIT_PART --> STOPPING: sensor_detected
STOPPING --> INSPECTING: stop_ack
INSPECTING --> TRANSFORMING: class accepted
INSPECTING --> REJECT: UNKNOWN / low confidence
TRANSFORMING --> ROUTING: point inside ROI
TRANSFORMING --> FAULT: outside / invalid
ROUTING --> COMPLETE: symbolic plan recorded
WAIT_PART --> FAULT: sensor timeout
STOPPING --> FAULT: stop timeout
INSPECTING --> FAULT: camera lost
COMPLETE --> [*]
REJECT --> [*]
FAULT --> [*]
Рис. 1. Автомат dry-run візуального сортування
4. Джерела
Виконання лабораторної роботи
Крок 1. Safe-smoke маршруту й UNKNOWN
# safe-smoke
ROUTES = {"red-cylinder-candidate": "BOX_RED", "blue-cube-candidate": "BOX_BLUE"}
def route(label: str, confidence: float, threshold: float = 0.75) -> str:
if confidence < threshold or label not in ROUTES:
return "MANUAL_REVIEW"
return ROUTES[label]
assert route("red-cylinder-candidate", 0.90) == "BOX_RED"
assert route("blue-cube-candidate", 0.75) == "BOX_BLUE"
assert route("blue-cube-candidate", 0.749) == "MANUAL_REVIEW"
assert route("UNKNOWN", 1.0) == "MANUAL_REVIEW"
print("safe-smoke: routing gates PASS")
Очікуваний результат: safe-smoke: routing gates PASS.
Критерій правильності: рівність порогу приймається, нижче порогу та невідомий клас відхиляються.
Якщо результат не отримано: не запускати повний сценарій; усунути неоднозначність оператора порівняння.
Крок 2. Зовнішня dry-run конфігурація
Збережіть як lab18-config.json.
{
"confidence_min": 0.75,
"roi_px": [0, 0, 640, 480],
"timeouts_ticks": {"sensor": 3, "stop": 2},
"routes": {
"red-cylinder-candidate": "BOX_RED",
"blue-cube-candidate": "BOX_BLUE"
},
"unknown_route": "MANUAL_REVIEW",
"mode": "dry-run"
}
Очікуваний результат: конфігурація містить тільки символічні маршрути та піксельний ROI.
Критерій правильності: mode дорівнює dry-run, усі тайм-аути додатні.
Якщо результат не отримано: виправити JSON; не додавати координати або адресу стенда.
Крок 3. Набір сценаріїв
Збережіть як lab18-cases.json.
[
{"id":"P-01","sensor_tick":1,"stop_ack_tick":1,"camera_ok":true,"u":320,"v":240,"label":"red-cylinder-candidate","confidence":0.92},
{"id":"N-UNKNOWN","sensor_tick":1,"stop_ack_tick":1,"camera_ok":true,"u":300,"v":200,"label":"UNKNOWN","confidence":0.40},
{"id":"N-CAMERA","sensor_tick":1,"stop_ack_tick":1,"camera_ok":false,"u":320,"v":240,"label":"red-cylinder-candidate","confidence":0.95},
{"id":"B-ROI","sensor_tick":1,"stop_ack_tick":1,"camera_ok":true,"u":640,"v":240,"label":"blue-cube-candidate","confidence":0.90},
{"id":"B-SENSOR","sensor_tick":4,"stop_ack_tick":1,"camera_ok":true,"u":320,"v":240,"label":"red-cylinder-candidate","confidence":0.90},
{"id":"B-STOP","sensor_tick":1,"stop_ack_tick":3,"camera_ok":true,"u":320,"v":240,"label":"red-cylinder-candidate","confidence":0.90}
]
Очікуваний результат: шість fixtures без апаратних команд.
Критерій правильності: є успіх, UNKNOWN, втрата камери, межа ROI і два тайм-аути.
Якщо результат не отримано: перевірити JSON через python -m json.tool; не пропускати негативні cases.
Крок 4. Реалізація dry-run автомата
Збережіть як lab18_dry_run.py.
from __future__ import annotations
import argparse
import json
import math
from dataclasses import asdict, dataclass, field
from enum import Enum
from pathlib import Path
class State(str, Enum):
WAIT_PART = "WAIT_PART"
STOPPING = "STOPPING"
INSPECTING = "INSPECTING"
TRANSFORMING = "TRANSFORMING"
ROUTING = "ROUTING"
COMPLETE = "COMPLETE"
REJECT = "REJECT"
FAULT = "FAULT"
@dataclass
class Outcome:
case_id: str
state: str
route: str | None
x_norm: float | None
y_norm: float | None
reason: str
transitions: list[str] = field(default_factory=list)
def transition(log: list[str], old: State, new: State, reason: str) -> State:
log.append(f"{old.value}->{new.value}:{reason}")
return new
def run_case(case: dict, cfg: dict) -> Outcome:
if cfg.get("mode") != "dry-run":
raise ValueError("only dry-run mode is accepted")
state = State.WAIT_PART
log: list[str] = []
limits = cfg["timeouts_ticks"]
sensor_tick = int(case["sensor_tick"])
if sensor_tick > int(limits["sensor"]):
state = transition(log, state, State.FAULT, "sensor-timeout")
return Outcome(case["id"], state.value, None, None, None, "sensor-timeout", log)
state = transition(log, state, State.STOPPING, "part-detected")
stop_tick = int(case["stop_ack_tick"])
if stop_tick > int(limits["stop"]):
state = transition(log, state, State.FAULT, "stop-timeout")
return Outcome(case["id"], state.value, None, None, None, "stop-timeout", log)
state = transition(log, state, State.INSPECTING, "stop-confirmed")
if not bool(case["camera_ok"]):
state = transition(log, state, State.FAULT, "camera-lost")
return Outcome(case["id"], state.value, None, None, None, "camera-lost", log)
confidence = float(case["confidence"])
label = str(case["label"])
if not math.isfinite(confidence) or confidence < float(cfg["confidence_min"]):
state = transition(log, state, State.REJECT, "low-confidence")
return Outcome(case["id"], state.value, cfg["unknown_route"], None, None, "low-confidence", log)
if label not in cfg["routes"]:
state = transition(log, state, State.REJECT, "unknown-class")
return Outcome(case["id"], state.value, cfg["unknown_route"], None, None, "unknown-class", log)
state = transition(log, state, State.TRANSFORMING, "class-accepted")
left, top, right, bottom = map(float, cfg["roi_px"])
u, v = float(case["u"]), float(case["v"])
if not all(math.isfinite(value) for value in (left, top, right, bottom, u, v)):
state = transition(log, state, State.FAULT, "non-finite-coordinate")
return Outcome(case["id"], state.value, None, None, None, "non-finite-coordinate", log)
if not (left <= u < right and top <= v < bottom and right > left and bottom > top):
state = transition(log, state, State.FAULT, "outside-roi")
return Outcome(case["id"], state.value, None, None, None, "outside-roi", log)
x_norm = (u - left) / (right - left)
y_norm = (v - top) / (bottom - top)
state = transition(log, state, State.ROUTING, "normalized-point-valid")
route = str(cfg["routes"][label])
state = transition(log, state, State.COMPLETE, f"plan-only:{route}")
return Outcome(case["id"], state.value, route, x_norm, y_norm, "plan-recorded", log)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--config", type=Path, required=True)
parser.add_argument("--cases", type=Path, required=True)
parser.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
cfg = json.loads(args.config.read_text(encoding="utf-8"))
cases = json.loads(args.cases.read_text(encoding="utf-8"))
outcomes = [run_case(case, cfg) for case in cases]
by_id = {outcome.case_id: outcome for outcome in outcomes}
assert by_id["P-01"].state == "COMPLETE"
assert by_id["N-UNKNOWN"].state == "REJECT"
for case_id in ("N-CAMERA", "B-ROI", "B-SENSOR", "B-STOP"):
assert by_id[case_id].state == "FAULT"
args.out.write_text(
json.dumps([asdict(item) for item in outcomes], ensure_ascii=False, indent=2),
encoding="utf-8",
)
print("PASS:", {state: sum(item.state == state for item in outcomes) for state in State._value2member_map_})
return 0
if __name__ == "__main__":
raise SystemExit(main())
Очікуваний результат: один COMPLETE, один REJECT, чотири FAULT; у журналі немає апаратної команди.
Критерій правильності: P-01 має нормалізовану точку (0.5, 0.5); u=640 відхиляється через напіввідкриту межу ROI.
Якщо результат не отримано: зупинити dry-run, звірити fixtures і журнал переходів; не змінювати expected задля проходження тесту.
Крок 5. Запуск і розрахунок метрик
python lab18_dry_run.py --config lab18-config.json --cases lab18-cases.json --out lab18-results.json
Обчисліть:
success_rate = COMPLETE / all;reject_rate = REJECT / all;fault_rate = FAULT / all;- кількість переходів і логічний tick до кінцевого стану.
Очікуваний результат: JSON з усіма переходами та підсумкова таблиця.
Критерій правильності: знаменник охоплює всі шість cases; невизначеність не зарахована як успіх.
Якщо результат не отримано: перевірити унікальність id та повноту вихідного JSON; не видаляти невдалі cases.
Фізичне перенесення: еталон і адресні прогалини
Еталон потребує підтверджених адаптерів ЛР-05/06/09/13, калібрування ЛР-17, безпечної висоти й меж, синхронізації часу, локального оператора, окремого плану валідації та першого покрокового запуску. Dry-run лишається обов'язковим gate.
Сценарії перевірки
| Тип | Сценарій | Очікуваний стан | Безпечна реакція |
|---|---|---|---|
| позитивний | відома деталь у центрі ROI | COMPLETE | лише символічний plan |
| негативний | UNKNOWN / низька confidence | REJECT | MANUAL_REVIEW |
| негативний | камера недоступна | FAULT | усі виходи STOP |
| граничний | точка на правій межі ROI | FAULT | не екстраполювати |
| граничний | sensor tick = limit + 1 | FAULT | timeout |
| граничний | stop tick = limit + 1 | FAULT | не знімати кадр |
Таблиці вимірювань і метрики
| Case | Клас/confidence | (u,v) | Нормалізована точка | Маршрут | Кінцевий стан | Переходів | Причина |
|---|---|---|---|---|---|---|---|
| P-01 | |||||||
| N-UNKNOWN | — | ||||||
| N-CAMERA | — | — | |||||
| B-ROI | — | — | |||||
| B-SENSOR | — | — | |||||
| B-STOP | — | — |
Вимоги до звіту
Див. загальні вимоги. Додайте конфігурацію, fixtures, код, журнал переходів, таблицю метрик, пояснення напіввідкритої ROI, матрицю сценаріїв і твердження, що координати нормалізовані та жоден механізм не запускався.
Критерії оцінювання
| Складова | Частка | Що оцінюється |
|---|---|---|
| Підготовка | 10% | залежності, конфігурація, safety-чекліст |
| Реалізація | 40% | автомат, gates, трансформація, маршрути й журнали |
| Перевірка | 25% | позитивний, негативні, граничні й timeout cases |
| Аналіз | 15% | метрики, невизначеність, координатні межі |
| Звіт | 10% | відтворювані артефакти й чесна межа перевірки |
| Разом | 100% |
Контрольні питання
- Чому sensor event не є підтвердженням зупинки деталі?
- Чим нормалізована точка відрізняється від robot frame?
- Чому права й нижня межі ROI у коді виключені?
- Коли потрібен
REJECT, а колиFAULT? - Які метрики не можна отримати без фізичної апробації?
- Чому після перезапуску не можна автоматично продовжити pick?
Висновки
Опишіть підтверджену dry-run послідовність, результати ін'єкції відмов, політику UNKNOWN, отримані метрики та конкретні дані, потрібні для майбутньої фізичної валідації.