Архитектура системы оптического контроля: калибровка масштаба и модель инспекции
Архитектура системы оптического контроля: калибровка масштаба и модель инспекции
На высокоскоростной сборочной линии погрешность в пикселей при измерении диаметра прецизионного подшипника может означать разницу между исправным узлом и заклиниванием механизма. Камера машинного зрения оперирует исключительно безразмерными дискретными массивами яркостей, где нет понятий «миллиметр», «шероховатость» или «технологический допуск». Превращение матрицы чисел в метрологически достоверный вердикт — задачу систем автоматической оптической инспекции (Automated Optical Inspection, AOI) — обеспечивает математически выверенная архитектура калибровки и формальная модель инспекции.
Любая промышленная AOI-система решает две фундаментальные задачи: переводит пиксельные координаты в физические единицы измерений рабочего поля и сопоставляет извлеченные геометрические характеристики с эталонной технологической картой.
Архитектура промышленного пайплайна инспекции
В отличие от исследовательских скриптов обработки изображений, промышленная система оптического контроля работает в жестких временных рамках конвейера. Каждый этап пайплайна выполняет изолированную функцию преобразования данных: от захвата фотонов до отправки сигнала в промышленный контроллер (PLC).
Поток данных внутри инспекционного модуля организован в виде последовательного детерминированного графа:
[Захват кадра]
│
▼
[Предобработка] ──────────► Шумоподавление, нормализация освещения
│
▼
[Калибровка и масштабирование] ──► Перевод координат px -> мм
│
▼
[Локализация и сегментация] ───► Выделение ROI и контуров детали
│
▼
[Измерительный блок] ────► Расчет метрик (габариты, моменты, дефекты)
│
▼
[Модель валидации (Decision Engine)] ──► Проверка допусков (PASS / FAIL)
│
▼
[Телеметрия и визуализация] ──► Вывод оверлея, логирование брака
Качественная реализация требует отделения этапа калибровки пространственного масштаба от самого процесса контроля: коэффициент пересчета должен быть вычислен заранее по стабильному калибровочному эталону.
Калибровка масштаба: переход от пикселей к миллиметрам
Матрица камеры фиксирует проекцию трехмерного объекта на двухмерную плоскость светочувствительных ячеек. Если оптическая ось камеры строго перпендикулярна рабочей плоскости конвейера (телецентрическая схема или съемка с фиксированной дистанции без перспективного скоса), соотношение между размерами в пикселях и реальными миллиметрами описывается линейным коэффициентом масштабирования.
Коэффициент пространственного разрешения (Scale Factor, ) — физическая величина, показывающая, сколько миллиметров реального пространства приходится на один пиксель дискретной матрицы в плоскости фокусировки:
где — известный физический размер калибровочного маркера в миллиметрах, а — измеренный размер маркера на матрице в пикселях.
Обратная величина называется плотностью пикселей на единицу длины (Pixels Per Metric, ):
Практический пример калибровки
Представим калибровочную мишень — прецизионный керамический диск с сертифицированным диаметром . При съемке на инспекционной позиции контур диска после бинаризации и поиска минимальной описанной окружности дает диаметр .
- Масштабный коэффициент:
- Плотность пикселей:
Если на проверяемой детали алгоритм измерил длину стороны , ее физический размер рассчитывается простым произведением:
Для двумерных характеристик (площадь пятна дефекта, общая площадь детали) коэффициент масштабируется квадратично: .
Ниже представлен модуль автоматической калибровки по круглому эталону:
from dataclasses import dataclass
import cv2
import numpy as np
@dataclass(frozen=True)
class CalibrationResult:
scale_factor_mm_per_px: float
pixels_per_mm: float
reference_center_px: tuple[float, float]
reference_radius_px: float
class ScaleCalibrator:
"""Модуль вычисления пространственного разрешения по круглому калибровочному маркеру."""
def __init__(self, target_diameter_mm: float):
if target_diameter_mm <= 0:
raise ValueError("Диаметр эталона должен быть положительным числом.")
self.target_diameter_mm = target_diameter_mm
def calibrate(self, calibration_frame: np.ndarray) -> CalibrationResult:
"""Находит эталонный круг на кадре и рассчитывает масштабный коэффициент."""
if calibration_frame is None or calibration_frame.size == 0:
raise ValueError("Получен пустой кадр калибровки.")
# Приведение к полутоновому виду и подавление шумов
if len(calibration_frame.shape) == 3:
gray = cv2.cvtColor(calibration_frame, cv2.COLOR_BGR2GRAY)
else:
gray = calibration_frame
blurred = cv2.GaussianBlur(gray, (7, 7), 1.5)
# Бинаризация методом Оцу для уверенного отделения маркера от фона
_, binary = cv2.threshold(
blurred, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU
)
# Морфологическая очистка мелких артефактов
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
cleaned = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
# Поиск внешних контуров
contours, _ = cv2.findContours(
cleaned, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
if not contours:
raise RuntimeError("Калибровочный маркер не обнаружен на изображении.")
# Выбираем наибольший контур как целевую мишень
target_cnt = max(contours, key=cv2.contourArea)
# Оценка круглости мишени для исключения бликов и частичных перекрытий
area = cv2.contourArea(target_cnt)
perimeter = cv2.arcLength(target_cnt, True)
if perimeter == 0:
raise RuntimeError("Контур эталона имеет нулевой периметр.")
circularity = 4 * np.pi * (area / (perimeter * perimeter))
if circularity < 0.85:
raise RuntimeError(
f"Маркер деформирован или перекрыт (circularity = {circularity:.2f} < 0.85)"
)
# Определение параметров минимальной описанной окружности
(cx, cy), radius_px = cv2.minEnclosingCircle(target_cnt)
diameter_px = radius_px * 2.0
if diameter_px <= 0:
raise RuntimeError("Некорректный диаметр описанной окружности.")
scale_factor = self.target_diameter_mm / diameter_px
ppm = diameter_px / self.target_diameter_mm
return CalibrationResult(
scale_factor_mm_per_px=scale_factor,
pixels_per_mm=ppm,
reference_center_px=(float(cx), float(cy)),
reference_radius_px=float(radius_px),
)
Модель инспекции и допусковый контроль
Получение физических размеров детали — только половина процесса. Система контроля качества должна сопоставить измеренные величины с допусками конструкторской документации.
Поля допусков: номинал, USL и LSL
Каждый контролируемый параметр (длина, ширина, диаметр отверстия, округлость) характеризуется технологическим коридором допустимых значений:
| Параметр допуска | Обозначение | Описание |
|---|---|---|
| Номинальное значение | Идеальный расчетный размер по чертежу | |
| Нижний предел допуска | (Lower Spec Limit) | Минимально допустимый размер детали () |
| Верхний предел допуска | (Upper Spec Limit) | Максимально допустимый размер детали () |
Решение о статусе параметра формируется по строгому условию:
Если или , изделие признается браком () с фиксацией конкретного нарушения.
Структурирование спецификации изделия
Чтобы инспекционный конвейер оставался гибким и мог переключаться между разными типами деталей без правки кода алгоритмов, параметры контроля инкапсулируются в декларативные структуры данных.
from dataclasses import dataclass
from enum import Enum
from typing import Optional
class InspectionStatus(Enum):
PASS = "PASS"
FAIL = "FAIL"
UNCERTAIN = "UNCERTAIN"
@dataclass(frozen=True)
class ToleranceSpec:
"""Числовой интервал допуска для одного геометрического параметра."""
name: str
nominal: float
lower_tolerance: float # delta отрицательная или нулевая (величина смещения вниз)
upper_tolerance: float # delta положительная или нулевая (величина смещения вверх)
unit: str = "мм"
@property
def lsl(self) -> float:
return self.nominal - abs(self.lower_tolerance)
@property
def usl(self) -> float:
return self.nominal + abs(self.upper_tolerance)
def validate(self, value: float) -> tuple[bool, float]:
"""Возвращает флаг соответствия допуску и величину отклонения от номинала."""
deviation = value - self.nominal
is_valid = self.lsl <= value <= self.usl
return is_valid, deviation
@dataclass
class ParameterEvaluation:
spec: ToleranceSpec
measured_value: float
deviation: float
is_valid: bool
@dataclass
class InspectionReport:
status: InspectionStatus
details: list[ParameterEvaluation]
summary_message: str
class InspectionEngine:
"""Исполнительный модуль сопоставления измерений с технологической картой."""
def __init__(self, specs: list[ToleranceSpec]):
self.specs = {spec.name: spec for spec in specs}
def evaluate(self, measurements: dict[str, float]) -> InspectionReport:
evaluations: list[ParameterEvaluation] = []
overall_pass = True
failed_params: list[str] = []
for param_name, spec in self.specs.items():
if param_name not in measurements:
overall_pass = False
failed_params.append(f"{param_name} (не измерен)")
continue
val = measurements[param_name]
is_valid, deviation = spec.validate(val)
evaluations.append(
ParameterEvaluation(
spec=spec,
measured_value=val,
deviation=deviation,
is_valid=is_valid,
)
)
if not is_valid:
overall_pass = False
failed_params.append(
f"{param_name}: {val:.3f}{spec.unit} вне [{spec.lsl:.3f}, {spec.usl:.3f}]"
)
if overall_pass:
status = InspectionStatus.PASS
summary = "Все параметры в пределах технологического допуска."
else:
status = InspectionStatus.FAIL
summary = "Брак: " + "; ".join(failed_params)
return InspectionReport(
status=status, details=evaluations, summary_message=summary
)
Сквозной пример: интеграция калибратора и модели валидации
Объединим калибровку масштаба и допусковый контроль в единый метрологический модуль. Он принимает сырые измерения в пикселях, преобразует их в физические величины с использованием масштабного коэффициента и формирует структурированный отчет.
# 1. Задаем чертежные требования к детали (например, стальной фланец)
flange_specs = [
ToleranceSpec(
name="outer_diameter",
nominal=80.0,
lower_tolerance=0.2,
upper_tolerance=0.2,
),
ToleranceSpec(
name="inner_hole_diameter",
nominal=25.0,
lower_tolerance=0.1,
upper_tolerance=0.1,
),
ToleranceSpec(
name="wall_thickness",
nominal=27.5,
lower_tolerance=0.25,
upper_tolerance=0.25,
),
]
engine = InspectionEngine(flange_specs)
# 2. Результат предварительной калибровки по эталону
# Допустим, масштабный коэффициент k_scale = 0.08 мм/px
scale_factor = 0.08
# 3. Необработанные измерения с контурного детектора в пикселях
raw_measurements_px = {
"outer_diameter": 1003.5, # 1003.5 * 0.08 = 80.28 мм (номинал 80.0 +- 0.2 -> FAIL)
"inner_hole_diameter": 312.0, # 312.0 * 0.08 = 24.96 мм (номинал 25.0 +- 0.1 -> PASS)
"wall_thickness": 344.0, # 344.0 * 0.08 = 27.52 мм (номинал 27.5 +- 0.25 -> PASS)
}
# 4. Пересчет в физические величины
physical_measurements = {
name: px_val * scale_factor for name, px_val in raw_measurements_px.items()
}
# 5. Вынесение метрологического вердикта
report = engine.evaluate(physical_measurements)
print(f"Итоговый вердикт: {report.status.value}")
print(f"Отчет: {report.summary_message}")
for item in report.details:
status_str = "OK" if item.is_valid else "FAIL"
print(
f" - {item.spec.name}: факт={item.measured_value:.3f} {item.spec.unit} "
f"(отклонение {item.deviation:+.3f} {item.spec.unit}) -> {status_str}"
)
Вывод программы наглядно демонстрирует причину отбраковки:
Итоговый вердикт: FAIL
Отчет: Брак: outer_diameter: 80.280мм вне [79.800, 80.200]
- outer_diameter: факт=80.280 мм (отклонение +0.280 мм) -> FAIL
- inner_hole_diameter: факт=24.960 мм (отклонение -0.040 мм) -> OK
- wall_thickness: факт=27.520 мм (отклонение +0.020 мм) -> OK
Такая структура изолирует логику принятия решений от алгоритмов обработки изображений. В следующих главах мы наполним этот каркас конкретными методами компьютерного зрения: алгоритмами локализации детали на конвейере, субпиксельным измерением контуров и выявлением микродефектов поверхности.