MERCI SPACE
Навчальні матеріали

Лабораторна робота 18

Лабораторна робота №18. Візуальне сортування деталей

Мета: реалізувати й перевірити в режимі dry-run послідовність «виявлення — підтверджена зупинка — класифікація — перевірка координат — вибір маршруту», виміряти частку успішних циклів і довести безпечну реакцію на невпевненість, тайм-аут та втрату підсистеми без керування фізичним обладнанням.

Результати навчання та передумови

Після роботи студент уміє:

  1. координувати сенсор, конвеєр, камеру, класифікатор і планувальник через явні стани;
  2. відрізнити піксельні, нормалізовані та роботні координати;
  3. не створювати план захоплення до підтвердженої зупинки й валідної трансформації;
  4. спрямувати UNKNOWN до символічної зони ручної перевірки;
  5. ін'єктувати відмови й перевірити тайм-аути;
  6. обчислити 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.
  • Підготовлено позитивний, негативний і граничний сценарії.
  • Фізичні модулі від'єднані від програмного процесу.

Хід виконання роботи

  1. Перевірити політику маршрутизації safe-smoke.
  2. Створити зовнішню конфігурацію й fixtures.
  3. Реалізувати dry-run автомат.
  4. Запустити пакет сценаріїв та зібрати журнал переходів.
  5. Розрахувати метрики й визначити межі фізичного перенесення.

Методичні вказівки й теоретичні відомості

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.

Mermaid
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

Python
# 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.

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.

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.

Python
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. Запуск і розрахунок метрик

Bash
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.

Сценарії перевірки

ТипСценарійОчікуваний станБезпечна реакція
позитивнийвідома деталь у центрі ROICOMPLETEлише символічний plan
негативнийUNKNOWN / низька confidenceREJECTMANUAL_REVIEW
негативнийкамера недоступнаFAULTусі виходи STOP
граничнийточка на правій межі ROIFAULTне екстраполювати
граничнийsensor tick = limit + 1FAULTtimeout
граничнийstop tick = limit + 1FAULTне знімати кадр

Таблиці вимірювань і метрики

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%

Контрольні питання

  1. Чому sensor event не є підтвердженням зупинки деталі?
  2. Чим нормалізована точка відрізняється від robot frame?
  3. Чому права й нижня межі ROI у коді виключені?
  4. Коли потрібен REJECT, а коли FAULT?
  5. Які метрики не можна отримати без фізичної апробації?
  6. Чому після перезапуску не можна автоматично продовжити pick?

Висновки

Опишіть підтверджену dry-run послідовність, результати ін'єкції відмов, політику UNKNOWN, отримані метрики та конкретні дані, потрібні для майбутньої фізичної валідації.

MERCI SPACE