Wizja komputerowa w kontroli jakości żywności za pomocą PyTorch i YOLO
Każdego roku niewykryte wady żywności podczas produkcji kosztują światowy przemysł w przybliżeniu 7 miliardów dolarów: wycofanie produktu, wycofanie z rynku, uszkodzenie reputację marki, sankcje regulacyjne, a w najpoważniejszych przypadkach ryzyko dla zdrowia konsumenta. Posiniaczony pomidor, który ląduje w słoiku, metaliczne ciało obce, które przekracza linię opakowania, partia owoców z ukrytą pleśnią: sytuacje wymagające ręcznej kontroli systemów nie są w stanie przechwytywać wystarczająco konsekwentnie, gdy linia wiruje z szybkością 10-20 sztuk na sekundę.
Ludzka inspekcja wzrokowa jest skuteczna, ale z natury ograniczona: zdrowy inspektor, dobrze wytrenowany i wypoczęty osiąga dokładność rzędu 60-70% na liniach dużych prędkości, ze znaczną zmiennością związaną ze zmęczeniem, oświetleniem i subiektywnością. Systemy jednak działa wizja komputerowa oparta na głębokim uczeniu się dokładność większa niż 98%, 24 godziny na dobę, bez spadków wydajności na nocnej zmianie i z absolutną spójnością w ocenie.
W tym kontekście rodzina Ultralytics YOLO (You Only Look Once) stała się standardem de facto do inspekcji przemysłowej w czasie rzeczywistym. YOLO11, wydany we wrześniu 2024 r., zapewnia wyjątkowa wydajność: 22% mniej parametrów niż YOLOv8 z wyższym mAP w benchmarku COCO, opóźnienia poniżej 2 ms na procesorach graficznych T4 i zdolność wykrywania defektów wielkości milimetra na szybkich przenośnikach taśmowych. Rynek AI w kontroli jakości żywności i osiągnięte bezpieczeństwo żywności 2,7 miliarda dolarów w 2025 roku i wzrośnie do 13,7 miliardów do 2030 r. przy CAGR na poziomie 30,9%.
Artykuł ten stanowi kompletny przewodnik techniczny: od architektury przemysłowych systemów wizyjnych, do konstrukcji i adnotacji konkretnych zbiorów danych dotyczących wad żywności, do rurociągu szkoleniowego z PyTorch i YOLO11, aż do wdrożenia na linii produkcyjnej ze sprzętem przemysłowym, integracją PLC i automatyczny system sortowania. Każda sekcja zawiera działający i przetestowany kod Pythona.
Czego dowiesz się w tym artykule
- Podstawy widzenia komputerowego stosowane w przemyśle spożywczym: specyficzne wyzwania i różnice w stosunku do tradycyjnego CV przemysłowego
- Ewolucja YOLO: od YOLOv5 do YOLO11, architektura, testy porównawcze i dlaczego YOLO wygrywa w Faster R-CNN w zakresie kontroli żywności
- Budowa zbioru danych dla wad żywności: adnotacja za pomocą CVAT/Roboflow, klasy, powiększanie danych specyficznych dla żywności
- Kompletny potok szkoleniowy z PyTorch i YOLO11: kod Python, strojenie hiperparametrów, walidacja
- Wykrywanie a klasyfikacja a segmentacja: kiedy zastosować które podejście do kontroli jakości
- Architektura sprzętowa linii inspekcyjnej: kamera GigE Vision, oświetlenie strukturalne, wyzwalacz, sterownik PLC
- Metryki jakości: mAP, precyzja, wycofanie, progi akceptowalne dla przemysłu spożywczego
- Automatyczny system sortowania: integracja siłowników pneumatycznych i robotów typu pick-and-place
- Kompletne studium przypadku: linia do sortowania owoców z YOLO11, 10 sztuk/sekundę, dokładność 98,3%
- Regulacje: IFS Food, BRC, HACCP dla systemów wizyjnych w środowiskach spożywczych
Seria FoodTech - Wszystkie artykuły
| # | Przedmiot | Poziom | Państwo |
|---|---|---|---|
| 1 | Rurociąg IoT dla rolnictwa precyzyjnego z Pythonem i MQTT | Zaawansowany | Dostępny |
| 2 | ML Edge do monitorowania upraw: wizja komputerowa na polach | Zaawansowany | Dostępny |
| 3 | Satelitarne API i wskaźniki roślinności: NDVI z Pythonem i Sentinel-2 | Mediator | Dostępny |
| 4 | Identyfikowalność Blockchain w żywności: od pola do supermarketu | Mediator | Dostępny |
| 5 | Wizja komputerowa w kontroli jakości za pomocą PyTorch YOLO (tutaj jesteś) | Zaawansowany | Aktualny |
| 6 | FSMA i zgodność cyfrowa: automatyzacja procesów regulacyjnych | Mediator | Już wkrótce |
| 7 | Rolnictwo pionowe: kontrola środowiska za pomocą IoT i ML | Zaawansowany | Już wkrótce |
| 8 | Prognozowanie popytu na sprzedaż detaliczną żywności za pomocą Prophet i LightGBM | Mediator | Już wkrótce |
| 9 | Pulpit nawigacyjny Farm Intelligence: analityka w czasie rzeczywistym za pomocą Grafany | Mediator | Już wkrótce |
| 10 | Optymalizacja żywności w łańcuchu dostaw: ML na rzecz redukcji odpadów | Mediator | Już wkrótce |
Wizja komputerowa w przemyśle spożywczym: wyzwania i możliwości
Wizja komputerowa w kontroli jakości żywności nie jest po prostu „stosowanym CV przemysłowym”. do jedzenia”. Ma unikalne cechy, które czynią ją bardziej złożoną, a jednocześnie bardziej interesującą w porównaniu do kontroli elementów mechanicznych lub płytek drukowanych. Zrozum te specyfiki i pierwszy krok w budowaniu solidnego i niezawodnego systemu.
Naturalna zmienność produktów spożywczych
Wadliwą śrubę od zgodnej można odróżnić po dokładnych i niezmiennych kryteriach geometrycznych: średnica, skok, długość. Jednak „idealny” pomidor istnieje w niemal nieskończonym zakresie kształtów, kolorów, faktur i rozmiarów, które różnią się nie tylko pomiędzy różnymi odmianami, ale także w obrębie tej samej uprawy. System wizyjny musi nauczyć się rozróżniać zmienność akceptowalny naturalny (nieco asymetryczny, ale całkowicie zdrowy pomidor) od wady rzeczywiste (oparzenie słoneczne, wgniecenie, atak grzyba).
To wyzwanie wymaga bardzo dużych i zrównoważonych zbiorów danych szkoleniowych z reprezentatywnymi próbami całej normalnej zmienności produktu i szczególną uwagę na kalibrację progów decyzyjnych, aby uniknąć zarówno wyników fałszywie negatywnych (niewykrytych defektów), jak i wyniki fałszywie pozytywne (odrzucenie zgodnych produktów, co ma bezpośredni wpływ na koszty operacyjne).
Wyzwania dotyczące oświetlenia i środowiska
Środowisko linii produkcyjnej żywności jest nieprzyjazne dla układów optycznych: para wodna z procesów prania, kondensacji na soczewkach w warunkach chłodniczych, różnic w oświetlenie wzdłuż przenośnika taśmowego, lustrzane odbicia od błyszczących powierzchni, takich jak woski lub glazury. Oświetlenie strukturalne (światło pierścieniowe, podświetlenie, oświetlenie współosiowe, światło spolaryzowane) ma fundamentalne znaczenie i musi być zaprojektowane razem z systemem wizyjnym, nie dodano po namyśle.
Do produktów przezroczystych lub półprzezroczystych (galaretki, napoje, opakowania PET), zastosowano podświetlenie, dzięki któremu widoczne są wtrącenia i pęcherzyki powietrza. W przypadku nieprzezroczystych produktów, takich jak owoce i warzywa, połączenie rozproszonego oświetlenia pod kątem 45 stopni przy świetle współosiowym dobrze uwidacznia oparzenia, wgniecenia i powierzchowne zmiany chorobowe. Do wykrywania metalowych ciał obcych obsługiwany jest system wizyjny indukcyjny lub rentgenowski wykrywacz metali.
Szybkość linii i przetwarzanie w czasie rzeczywistym
Linia do pakowania owoców zazwyczaj pracuje z szybkością 8–15 sztuk na sekundę na kanał. Linia do produkcji ciastek może osiągnąć 200-400 sztuk na minutę. System wizyjny musi pozyskać obraz, przetworzyć go i zakomunikować decyzję do siłownika odrzutu w czasie potrzebnym produktowi na przebycie danej odległości pomiędzy stacją inspekcji a stacją sortowania: typowo 200-500 ms.
To ograniczenie czasowe wyklucza podejścia wymagające obliczeń, takie jak modele segmentacji w wysokiej rozdzielczości bez optymalizacji i nagradza architektury one-shoty, takie jak YOLO, które wykonują wykrywanie w jednym przejściu do przodu na GPU.
Ewolucja YOLO dla Inspekcji Żywności: od YOLOv5 do YOLO11
Rodzina YOLO zdominowała przemysłowe wykrywanie w czasie rzeczywistym przez prawie dekadę. Prześledzenie jego ewolucji pomaga zrozumieć, dlaczego YOLO11 jest optymalnym wyborem na nowy system kontroli żywności w 2025 roku.
YOLOv5 (2020) - Punkt zwrotny
YOLOv5 firmy Ultralytics zrewolucjonizowało dostępność detekcji przemysłowej: pierwsza natywna modułowa implementacja PyTorch, uproszczony eksport do ONNX, TensorRT i CoreML, udokumentowany i powtarzalny proces szkoleniowy. Zrobił wykrywanie niestandardowe dostępne dla zespołów bez głębokiej wiedzy w CV i zostało skolonizowane globalnych liniach produkcyjnych dzięki prostocie wdrożenia. Wiele udogodnień zainstalowane w latach 2021–2023 nadal działają na YOLOv5.
YOLOv8 (2023) - Architektura nowoczesna
YOLOv8 wprowadził architekturę bez kotwic, która eliminuje potrzebę zdefiniuj predefiniowane pola kontrolne, upraszczając szkolenie na temat zbiorów danych dotyczących żywności gdzie wymiary obiektów są bardzo zmienne (z punktu widzenia formy milimetr do całego jabłka). Szkielet C2f (Częściowy etap przekrojowy z 2 wąskimi gardłami) poprawia przepływ gradientowy podczas treningu. Oddzielna głowica detekcyjna (oddzielona głowa) do klasyfikacji i regresji poprawia zbieżność. mAP@50 na COCO: 50,2% dla YOLOv8m przy parametrach 25,9M.
YOLO11 (wrzesień 2024) - Stan techniki
YOLO11 stanowi najbardziej znaczący skok pod względem wydajności obliczeniowej: model YOLO11m osiąga mAP@50 51,5% na COCO z samym Parametry 20,1 mln, czyli o 22% mniej niż YOLOv8m przy tych samych zadaniach. Ulepszony szkielet z architekturą C3k2 zapewnia bogatszą ekstrakcję funkcji. Szyjka SPPF (Spatial Pyramid Pooling - Fast) lepiej radzi sobie z łuskowatymi obiektami różne, istotne przy wykrywaniu zarówno defektów milimetrowych, jak i całych obiektów. Opóźnienie na procesorach graficznych NVIDIA T4 dla modelu Nano i 1,5 ms, umożliwiając przetwarzanie z prędkością 600+ FPS.
Porównanie YOLO i alternatywnych architektur do kontroli żywności
| Architektura | mAP@50 COCO | Opóźnienie (ms) | Parametry | Kwalifikuje się do kontroli jakości żywności |
|---|---|---|---|---|
| YOLO11n | 39,5% | 1,5 ms | 2,6 mln | Doskonały (wdrożenie brzegowe) |
| YOLO11m | 51,5% | 4,7 ms | 20,1 mln | Doskonały (budżet) |
| YOLO11x | 54,7% | 11,3 ms | 56,9 mln | Dobra (wysoka dokładność) |
| YOLOv8m | 50,2% | 5,1 ms | 25,9 mln | Dobry (starsze systemy) |
| Szybszy R-CNN | 55,0% | 120-200 ms | 41,8 mln | Słabo (zbyt wolno) |
| Dyski SSD MobileNet | 23,2% | 1,1 ms | 6,8 mln | Marginalny (niski poziom) |
| RT-DETR | 53,1% | 8,9 ms | 42,0 mln | Dobry (bez NMS) |
Szybszy R-CNN, pomimo dużej dokładności w statycznych testach porównawczych, i praktycznie nie nadaje się do kontroli w czasie rzeczywistym na przenośnikach taśmowych: opóźnienie 120-200 ms oznaczają, że przy 10 sztukach/sekundę system widzi tylko 1 sztukę z 12-20. YOLO11m z czasem 4,7 ms z łatwością przetwarza 200 klatek na sekundę, pozostawiając mnóstwo miejsca na grę rurociąg komunikacyjny ze sterownikiem PLC i siłownikiem odpadowym.
Zbiory danych dotyczące wad żywności: konstrukcja i adnotacja
Jakość zbioru danych szkoleniowych jest czynnikiem najbardziej determinującym wydajność systemu wizyjnego. Doskonały model YOLO11 wytrenowany na słabym zbiorze danych da mierne rezultaty. I odwrotnie, trenowano nawet na prostszym modelu bogaty, zrównoważony i dobrze opatrzony komentarzami zbiór danych daje znacznie lepsze wyniki.
Klasy typowych wad w przemyśle spożywczym
Klasy, które należy uwzględnić w zbiorze danych, zależą od produktu i procesu, ale istnieją kategorie wspólne dla przemysłu spożywczego w ogóle:
Klasy defektów systemu wizyjnego dotyczącego owoców i warzyw
| Klasa | Opis | Typowa przyczyna | Próg krytyczności |
|---|---|---|---|
mold | Powierzchowna lub ukryta pleśń | Wilgoć, rany skóry | Wysoki (obowiązkowe odrzucenie) |
bruise | Wgniecenie uderzeniowe | Kolekcja, transport | Średni (w zależności od nasilenia) |
burn | Oparzenie słoneczne lub oparzenie zimnem | Bezpośrednie narażenie na promieniowanie UV, mróz | Średnio-wysoki |
crack | Pęknięcie lub szczelina | Szybki wzrost, susza | Wysoki (wektor patogenu) |
foreign_object | Ciało obce (liście, kamienie, plastik) | Zbiór mechaniczny | Krytyka |
rot | Zaawansowana zgnilizna | Bakterie, grzyby | Wysoki (obowiązkowe odrzucenie) |
insect_damage | Obrażenia owadów | Ataki entomologiczne | Średnio-wysoki |
size_defect | Kaliber poza specyfikacją | Zmienność odmianowa | Niski (przekierowanie) |
color_defect | Nietypowy kolor (przesadnie/niedojrzały) | Termin zbioru | Przeciętny |
ok | Produkt zgodny | - | - |
Narzędzia do adnotacji: CVAT, Label Studio i Roboflow
Każde z trzech głównych narzędzi do opisywania przemysłowych zbiorów danych o konkretnych mocach:
CVAT (narzędzie do adnotacji widzenia komputerowego) Intela i narzędzia Solidne, hostowane samodzielnie oprogramowanie typu open source, idealne dla zespołów mających wymagania dotyczące prywatności danych lub w środowiskach pozbawionych powietrza. Obsługuje obwiednię, wielokąt, polilinię i adnotacje punktowe i śledzenie wideo. Integracja z Roboflow pozwala na korzystanie z wcześniej wytrenowanych modeli jako asystenci adnotacji, redukując czas pracy ręcznej o 60-70%.
Studio Etykiet i bardziej elastyczne dla multimodalnych zbiorów danych (obrazy, tekst, dźwięk) i łatwo integruje się z potokami MLOps. Obsługuje wspólne adnotacje z wielokrotne recenzje i głosowanie konsensusowe, przydatne, gdy wielu komentatorów pracuje nad tymi samymi obrazami.
Roboflow oferuje najbardziej zintegrowany potok: adnotacja, przetwarzanie wstępne, powiększanie i eksportowanie w formacie YOLO w ramach jednej chmury. Dla zbiorów danych dotyczących żywności publiczny, Roboflow Universe obsługuje setki zbiorów danych domeny publicznej (owoce, rośliny, wady powierzchni), które można wykorzystać jako punkt wyjścia w procesie uczenia się transferowego.
Rozmiar zbioru danych
W przypadku systemu wykrywania obejmującego 8–10 klas defektów w jednym produkcie, minimalny zalecany rozmiar oraz:
- Zestawy treningowe: minimum 500 zdjęć na zajęcia, najlepiej 1000+
- Zestaw walidacyjny: 15-20% szkolenia, podzielone na zajęcia
- Zestawy testowe: 10-15%, całkowicie odseparowane, uzyskane w rzeczywistych warunkach liniowych
- Rzadkie klasy: na zajęcia takie jak
foreign_objectktóre są rzadko potrzebne w produkcji, należy stosować nadmierne próbkowanie i agresywne wzmacnianie
Rozszerzanie danych specyficzne dla żywności
Augmentacja produktów spożywczych musi symulować rzeczywiste różnice, jakie występują system spotka się w produkcji. Standardowe techniki (odwrócenie w poziomie, obrót, uprawa) musi być zintegrowana ze wspomaganiem specyficznym dla żywności:
# augmentation_food.py
# Configurazione augmentation specifica per food quality inspection
import albumentations as A
import cv2
import numpy as np
def build_food_augmentation_pipeline(image_size: int = 640) -> A.Compose:
"""
Pipeline di augmentation per dataset di difetti alimentari.
Simula variazioni reali di illuminazione, orientamento e condizioni di linea.
"""
return A.Compose([
# Geometria: prodotti alimentari arrivano in orientamenti casuali
A.RandomRotate90(p=0.5),
A.HorizontalFlip(p=0.5),
A.VerticalFlip(p=0.3),
A.ShiftScaleRotate(
shift_limit=0.1,
scale_limit=0.2,
rotate_limit=45,
border_mode=cv2.BORDER_REFLECT,
p=0.7
),
# Illuminazione: variazioni reali in linea (vapore, sporco sulle lenti)
A.OneOf([
A.RandomBrightnessContrast(
brightness_limit=0.3,
contrast_limit=0.3,
p=1.0
),
A.RandomGamma(gamma_limit=(70, 130), p=1.0),
A.CLAHE(clip_limit=4.0, tile_grid_size=(8, 8), p=1.0),
], p=0.8),
# Colore: variazioni stagionali e di maturazione
A.HueSaturationValue(
hue_shift_limit=15, # Piccolo: non cambiare il colore del prodotto
sat_shift_limit=30,
val_shift_limit=20,
p=0.5
),
# Rumore: sensore camera industriale, interferenze EMI
A.OneOf([
A.GaussNoise(var_limit=(10.0, 50.0), p=1.0),
A.ISONoise(color_shift=(0.01, 0.05), intensity=(0.1, 0.5), p=1.0),
A.MultiplicativeNoise(multiplier=(0.9, 1.1), p=1.0),
], p=0.4),
# Blur: motion blur da velocità nastro, defocus per DOF limitata
A.OneOf([
A.MotionBlur(blur_limit=7, p=1.0), # Più realistico per nastro
A.GaussianBlur(blur_limit=(3, 7), p=1.0),
], p=0.3),
# Riflessioni: superfici cerate, glazze, film di acqua
A.RandomSunFlare(
flare_roi=(0.0, 0.0, 1.0, 0.5),
num_flare_circles_lower=1,
num_flare_circles_upper=3,
src_radius=100,
p=0.1
),
# Ritaglio e padding finale
A.PadIfNeeded(
min_height=image_size,
min_width=image_size,
border_mode=cv2.BORDER_REFLECT
),
A.Resize(image_size, image_size),
# Normalizzazione finale
A.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
),
], bbox_params=A.BboxParams(
format='yolo',
label_fields=['class_labels'],
min_area=100, # Ignora bbox troppo piccole dopo crop
min_visibility=0.3 # Ignora bbox con meno del 30% visibile
))
def augment_rare_class(
image: np.ndarray,
bboxes: list,
class_labels: list,
n_augmentations: int = 10
) -> list:
"""
Over-sampling per classi rare come foreign_object.
Genera n varianti augmentate di un singolo campione.
"""
pipeline = build_food_augmentation_pipeline()
augmented_samples = []
for _ in range(n_augmentations):
result = pipeline(
image=image,
bboxes=bboxes,
class_labels=class_labels
)
augmented_samples.append({
'image': result['image'],
'bboxes': result['bboxes'],
'class_labels': result['class_labels']
})
return augmented_samples
Kompletny potok szkoleniowy za pomocą PyTorch i YOLO11
Szkolenie dostosowanego modelu YOLO11 do kontroli jakości żywności działa według zorganizowanego potoku: przygotowanie środowiska, przygotowanie zbioru danych, szkolenia z transferem uczenia się, walidacją i optymalizacją pod kątem wdrożenia.
Konfiguracja środowiska szkoleniowego
# Requisiti: Python 3.11+, CUDA 12.1+, NVIDIA GPU con 8GB+ VRAM
# Consigliato: RTX 3080/4080 per training locale, A100 per training veloce
# 1. Installazione dipendenze
# pip install ultralytics==8.3.0 albumentations roboflow torch torchvision
# 2. Struttura directory dataset (formato YOLO)
# dataset/
# ├── images/
# │ ├── train/ (70% campioni)
# │ ├── val/ (20% campioni)
# │ └── test/ (10% campioni)
# ├── labels/
# │ ├── train/ (file .txt corrispondenti)
# │ ├── val/
# │ └── test/
# └── data.yaml (configurazione dataset)
# data.yaml - Configurazione dataset
DATA_YAML_CONTENT = """
path: /data/food_quality_dataset
train: images/train
val: images/val
test: images/test
nc: 10 # Numero di classi
names:
0: ok
1: mold
2: bruise
3: burn
4: crack
5: foreign_object
6: rot
7: insect_damage
8: size_defect
9: color_defect
"""
import yaml
with open('/data/food_quality_dataset/data.yaml', 'w') as f:
f.write(DATA_YAML_CONTENT)
Pobieranie i przygotowanie zestawu danych za pomocą Roboflow
# dataset_preparation.py
# Scarica dataset da Roboflow Universe o usa dataset locale
from roboflow import Roboflow
import os
def download_food_dataset(api_key: str, workspace: str, project: str, version: int) -> str:
"""
Scarica dataset da Roboflow e prepara per training YOLO11.
"""
rf = Roboflow(api_key=api_key)
proj = rf.workspace(workspace).project(project)
dataset = proj.version(version).download("yolov8") # Formato YOLO11 compatibile
dataset_path = dataset.location
print(f"Dataset scaricato in: {dataset_path}")
print(f"Training images: {len(os.listdir(os.path.join(dataset_path, 'train', 'images')))}")
print(f"Validation images: {len(os.listdir(os.path.join(dataset_path, 'valid', 'images')))}")
return dataset_path
def analyze_class_distribution(dataset_path: str) -> dict:
"""
Analizza la distribuzione delle classi nel dataset.
Identifica classi sbilanciate che richiedono over-sampling.
"""
import glob
from collections import Counter
class_counts = Counter()
label_files = glob.glob(os.path.join(dataset_path, 'train', 'labels', '*.txt'))
for label_file in label_files:
with open(label_file, 'r') as f:
for line in f:
class_id = int(line.split()[0])
class_counts[class_id] += 1
total = sum(class_counts.values())
distribution = {
class_id: {
'count': count,
'percentage': round(count / total * 100, 2),
'needs_oversampling': count < total / len(class_counts) * 0.3 # < 30% della media
}
for class_id, count in sorted(class_counts.items())
}
return distribution
# Analisi distribuzione per identificare classi rare
if __name__ == "__main__":
dataset_path = "/data/food_quality_dataset"
distribution = analyze_class_distribution(dataset_path)
print("\nDistribuzione classi nel dataset:")
class_names = ['ok', 'mold', 'bruise', 'burn', 'crack',
'foreign_object', 'rot', 'insect_damage', 'size_defect', 'color_defect']
for class_id, info in distribution.items():
name = class_names[class_id] if class_id < len(class_names) else f"class_{class_id}"
warning = " *** RICHIEDE OVERSAMPLING ***" if info['needs_oversampling'] else ""
print(f" {name}: {info['count']} campioni ({info['percentage']}%){warning}")
Szkolenie YOLO11 z nauką transferową
# train_yolo11_food.py
# Training pipeline completa per food quality inspection
from ultralytics import YOLO
import torch
import yaml
import os
from pathlib import Path
def train_food_quality_model(
dataset_yaml: str,
output_dir: str = "./runs/food_quality",
epochs: int = 150,
batch_size: int = 16,
image_size: int = 640,
model_variant: str = "yolo11m.pt" # n/s/m/l/x
) -> dict:
"""
Training YOLO11 per food quality inspection con ottimizzazioni specifiche.
Args:
dataset_yaml: Path al file data.yaml del dataset
output_dir: Directory per salvare i risultati
epochs: Numero di epoche (150 e un buon punto di partenza)
batch_size: Dimensione batch (16 per 8GB VRAM, 32 per 16GB+)
image_size: Dimensione immagine (640 standard, 1280 per difetti piccoli)
model_variant: Variante YOLO11 da usare come punto di partenza
Returns:
dict con metriche finali del training
"""
# Verifica GPU disponibile
device = 'cuda' if torch.cuda.is_available() else 'cpu'
if device == 'cpu':
print("ATTENZIONE: Training su CPU, sarà molto lento. Usa una GPU.")
print(f"Device: {device}")
if device == 'cuda':
print(f"GPU: {torch.cuda.get_device_name(0)}")
print(f"VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB")
# Carica modello pre-addestrato su COCO (transfer learning)
model = YOLO(model_variant)
# Configurazione training
training_args = {
'data': dataset_yaml,
'epochs': epochs,
'batch': batch_size,
'imgsz': image_size,
'device': device,
'project': output_dir,
'name': 'food_quality_v1',
# Ottimizzatore: AdamW e ottimo per fine-tuning
'optimizer': 'AdamW',
'lr0': 0.001, # Learning rate iniziale
'lrf': 0.01, # Learning rate finale = lr0 * lrf
'momentum': 0.937,
'weight_decay': 0.0005,
# Learning rate scheduling: cosine annealing
'cos_lr': True,
'warmup_epochs': 5, # 5 epoche di warmup per stabilizzare
# Augmentation integrata (albumentations)
'hsv_h': 0.015, # Hue shift (piccolo per preservare colore prodotto)
'hsv_s': 0.7, # Saturation
'hsv_v': 0.4, # Value (luminosita)
'degrees': 30.0, # Rotazione
'translate': 0.1, # Traslazione
'scale': 0.5, # Scaling
'shear': 5.0,
'perspective': 0.0001,
'flipud': 0.3, # Flip verticale (utile per frutta)
'fliplr': 0.5, # Flip orizzontale
'mosaic': 1.0, # Mosaic augmentation (4 immagini)
'mixup': 0.1, # MixUp augmentation
'copy_paste': 0.1, # Copy-paste per classi rare
# Loss weights: aumenta peso classificazione per difetti rari
'cls': 1.5, # Classification loss weight (default 0.5)
'box': 7.5, # Box regression loss weight
'dfl': 1.5, # Distribution Focal Loss weight
# Valutazione e salvataggio
'val': True,
'save': True,
'save_period': 10, # Salva checkpoint ogni 10 epoche
'patience': 30, # Early stopping se no miglioramento per 30 epoche
# Performances
'workers': 8, # DataLoader workers
'cache': True, # Cache dataset in RAM per velocità
'amp': True, # Automatic Mixed Precision (FP16)
# Metriche
'plots': True, # Genera grafici di training
'verbose': True,
}
# Avvia training
print(f"\nAvvio training YOLO11 per food quality inspection")
print(f"Epoche: {epochs}, Batch: {batch_size}, Immagini: {image_size}x{image_size}")
results = model.train(**training_args)
# Estrai metriche finali
metrics = {
'mAP50': float(results.results_dict.get('metrics/mAP50(B)', 0)),
'mAP50_95': float(results.results_dict.get('metrics/mAP50-95(B)', 0)),
'precision': float(results.results_dict.get('metrics/precision(B)', 0)),
'recall': float(results.results_dict.get('metrics/recall(B)', 0)),
'best_model_path': str(Path(output_dir) / 'food_quality_v1' / 'weights' / 'best.pt')
}
print(f"\nTraining completato!")
print(f"mAP@50: {metrics['mAP50']:.4f}")
print(f"mAP@50-95: {metrics['mAP50_95']:.4f}")
print(f"Precision: {metrics['precision']:.4f}")
print(f"Recall: {metrics['recall']:.4f}")
print(f"Modello salvato in: {metrics['best_model_path']}")
return metrics
if __name__ == "__main__":
metrics = train_food_quality_model(
dataset_yaml="/data/food_quality_dataset/data.yaml",
output_dir="./runs/food_quality",
epochs=150,
batch_size=16,
image_size=640,
model_variant="yolo11m.pt"
)
Strojenie hiperparametrów za pomocą Ray Tune
# hyperparameter_tuning.py
# Ricerca automatica degli hyperparametri ottimali con Ray Tune
from ultralytics import YOLO
from ray import tune
from ray.tune.schedulers import ASHAScheduler
import torch
def objective(config: dict) -> dict:
"""Funzione obiettivo per Ray Tune."""
model = YOLO("yolo11m.pt")
results = model.train(
data="/data/food_quality_dataset/data.yaml",
epochs=50, # Epoche ridotte per tuning rapido
batch=config['batch'],
lr0=config['lr0'],
lrf=config['lrf'],
momentum=config['momentum'],
weight_decay=config['weight_decay'],
cls=config['cls'],
hsv_s=config['hsv_s'],
mixup=config['mixup'],
amp=True,
verbose=False,
plots=False,
)
mAP50 = results.results_dict.get('metrics/mAP50(B)', 0)
return {"mAP50": mAP50}
def run_hyperparameter_search(n_trials: int = 20) -> dict:
"""Esegue ricerca hyperparametri con ASHA scheduler."""
search_space = {
'batch': tune.choice([8, 16, 32]),
'lr0': tune.loguniform(1e-4, 1e-2),
'lrf': tune.uniform(0.001, 0.1),
'momentum': tune.uniform(0.85, 0.98),
'weight_decay': tune.loguniform(1e-5, 1e-3),
'cls': tune.uniform(0.5, 2.0),
'hsv_s': tune.uniform(0.4, 0.9),
'mixup': tune.uniform(0.0, 0.3),
}
scheduler = ASHAScheduler(
max_t=50,
grace_period=10,
reduction_factor=2
)
analysis = tune.run(
objective,
config=search_space,
num_samples=n_trials,
scheduler=scheduler,
metric="mAP50",
mode="max",
resources_per_trial={"cpu": 4, "gpu": 1},
)
best_config = analysis.best_config
print(f"Migliori hyperparametri trovati:")
for key, value in best_config.items():
print(f" {key}: {value}")
return best_config
Wykrywanie a klasyfikacja a segmentacja w przypadku kontroli jakości żywności
YOLO11 obsługuje trzy główne paradygmaty: wykrywanie obiektów (obwiednia), klasyfikacja (cała klasa obrazu) i segmentacja instancji (maska na poziomie pikseli). Wybór zależy od rodzaju wady, wymaganej prędkości i dostępnego sprzętu.
Kiedy stosować które podejście
| Zbliżać się | Wyjścia | Typowe opóźnienie | Użyj Case Food QC | Plusy/minusy |
|---|---|---|---|---|
| Klasyfikacja | Klasa + pewność siebie | 0,5-1,5 ms | Klasyfikacja owoców według kategorii/jakości; dojrzewanie | Plusy: bardzo szybko. Wady: brak lokalizacji defektów |
| Wykrywanie obiektów | Bbox + zajęcia + paczka | 1,5-5 ms | Wykrywanie wielu defektów na tym samym elemencie; ciała obce | Plusy: optymalna równowaga prędkości i informacji. Wady: brak precyzyjnego kształtu |
| Segmentacja | Maska pikseli + klasa | 3-15 ms | Zmierz obszar defektu; precyzyjna kontrola rozmiaru; cieniowanie | Plusy: szczegółowe informacje. Wady: wolniejsze i bardziej złożone |
| Ocena pozycji | Kluczowe punkty | 2-6 ms | Orientacja części dla robotów typu pick-and-place; liczyć | Zalety: Precyzyjna orientacja. Wady: Wymaga zestawu danych punktów kluczowych |
W przypadku większości systemów kontroli jakości żywności tzwwykrywanie obiektów z YOLO11 to optymalny wybór: wykrywa i lokalizuje wiele defektów w tym samym produkcie, umożliwia ilościowe określenie dotkliwości na podstawie rozmiaru ramki ograniczającej, i utrzymuje opóźnienia zgodne z liniami dużej prędkości. Segmentacja jest uzasadniona gdy potrzebny jest precyzyjny pomiar obszaru defektu w celu jego klasyfikacji (np. sklasyfikować oparzenie jako „łagodne” < 5% powierzchni, „poważne” > 15% powierzchni).
Metryki jakości: interpretacja dla przemysłu spożywczego
Standardowe wskaźniki CV (mAP, precyzja, przypominanie) mają określone implikacje w kontekście żywności, które należy zrozumieć i jasno zakomunikować osobom odpowiedzialnym produkcji przed uruchomieniem.
mAP (średnia średnia precyzja)
mAP podsumowuje krzywą precyzji i przypomnienia dla wszystkich klas. mAP@50 wykorzystuje m.in Próg IoU (Przecięcie nad Unią) wynoszący 0,5, aby uwzględnić wykrycie poprawne. mAP@50-95 ma średni próg od 0,5 do 0,95 i jest bardziej dotkliwy. W przypadku kontroli jakości żywności mAP@50 jest zazwyczaj głównym miernikiem ze względu na dokładność dokładnej lokalizacji (IoU 0,95) i mniej krytyczny niż dokładność klasyfikacja wad.
Precyzja i pamięć: krytyczny kompromis
W kontroli jakości żywności precyzja i wycofanie mają fundamentalne asymetrie kosztów:
- Niska pamięć (fałszywie negatywne): wadliwy produkt, który przekracza kontroli i dociera do konsumenta. Koszt: wycofanie produktu, uszkodzenie reputacja marki, potencjalne zagrożenie dla zdrowia. Gorszący do bardzo krytycznych wad (pleśń, ciała obce).
- Niska precyzja (fałszywie pozytywne): przychodzi zgodny produkt odrzucony. Koszt: strata produktu, zmniejszenie wydajności linii. Dopuszczalne w granicach uzależnione od wartości produktu.
Kalibracja progu ufności jest głównym mechanizmem zarządzania nim
ten kompromis: obniżenie progu zwiększa zapamiętywanie (mniej wyników fałszywie negatywnych) a
ze szkodą dla precyzji (więcej wyników fałszywie dodatnich). W przypadku wad krytycznych, takich jak ciała obce,
ustawiony jest bardzo niski próg (0,3-0,4) akceptowający więcej wyników fałszywie dodatnich.
Za wady takie jak size_defect, próg może być wyższy (0,6-0,7).
# evaluate_model.py
# Valutazione completa del modello con metriche per food QC
from ultralytics import YOLO
import numpy as np
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt
import seaborn as sns
from pathlib import Path
def evaluate_food_model(
model_path: str,
test_dataset_yaml: str,
class_names: list,
confidence_thresholds: dict # Soglie per classe
) -> dict:
"""
Valutazione completa con metriche specifiche per food quality inspection.
Args:
model_path: Path al modello addestrato (best.pt)
test_dataset_yaml: Path al data.yaml del test set
class_names: Nomi delle classi
confidence_thresholds: Soglie confidence per classe {"mold": 0.35, "ok": 0.6}
"""
model = YOLO(model_path)
# Validazione standard YOLO
results = model.val(
data=test_dataset_yaml,
split='test',
imgsz=640,
conf=0.001, # Basso per calcolare la curva completa
iou=0.6,
plots=True,
verbose=False,
)
# Metriche per classe
class_metrics = {}
for i, name in enumerate(class_names):
class_metrics[name] = {
'precision': float(results.box.p[i]) if i < len(results.box.p) else 0,
'recall': float(results.box.r[i]) if i < len(results.box.r) else 0,
'mAP50': float(results.box.ap50[i]) if i < len(results.box.ap50) else 0,
'mAP50_95': float(results.box.ap[i]) if i < len(results.box.ap) else 0,
}
# Soglie di accettabilita per l'industria alimentare
acceptance_thresholds = {
'mold': {'recall_min': 0.98, 'precision_min': 0.85},
'foreign_object': {'recall_min': 0.995, 'precision_min': 0.80},
'rot': {'recall_min': 0.97, 'precision_min': 0.87},
'bruise': {'recall_min': 0.90, 'precision_min': 0.85},
'burn': {'recall_min': 0.88, 'precision_min': 0.83},
'crack': {'recall_min': 0.93, 'precision_min': 0.85},
'insect_damage': {'recall_min': 0.92, 'precision_min': 0.84},
'size_defect': {'recall_min': 0.85, 'precision_min': 0.88},
'color_defect': {'recall_min': 0.85, 'precision_min': 0.88},
'ok': {'recall_min': 0.95, 'precision_min': 0.92},
}
# Verifica accettabilita
go_nogo_results = {}
for class_name, metrics in class_metrics.items():
thresholds = acceptance_thresholds.get(class_name, {})
recall_ok = metrics['recall'] >= thresholds.get('recall_min', 0.0)
precision_ok = metrics['precision'] >= thresholds.get('precision_min', 0.0)
go_nogo_results[class_name] = {
'go': recall_ok and precision_ok,
'recall_ok': recall_ok,
'precision_ok': precision_ok,
'recall': metrics['recall'],
'precision': metrics['precision'],
}
# Report finale
print("\n=== VALUTAZIONE FOOD QUALITY MODEL ===\n")
print(f"mAP@50 globale: {float(results.box.map50):.4f}")
print(f"mAP@50-95 globale: {float(results.box.map):.4f}")
print("\nRisultati per classe:")
print(f"{'Classe':-20} {'Recall':>8} {'Precision':>10} {'mAP50':>8} {'GO/NO-GO':>10}")
print("-" * 60)
for class_name, gng in go_nogo_results.items():
status = "GO ✓" if gng['go'] else "NO-GO ✗"
print(f"{class_name:-20} {gng['recall']:8.4f} {gng['precision']:10.4f} "
f"{class_metrics[class_name]['mAP50']:8.4f} {status:>10}")
overall_go = all(gng['go'] for gng in go_nogo_results.values())
print(f"\nVALUTAZIONE FINALE: {'SISTEMA APPROVATO PER DEPLOY' if overall_go else 'NON APPROVATO - RICHIEDE MIGLIORAMENTI'}")
return {
'class_metrics': class_metrics,
'go_nogo': go_nogo_results,
'overall_go': overall_go,
'global_mAP50': float(results.box.map50),
}
# Esecuzione valutazione
if __name__ == "__main__":
class_names = ['ok', 'mold', 'bruise', 'burn', 'crack',
'foreign_object', 'rot', 'insect_damage', 'size_defect', 'color_defect']
confidence_thresholds = {
'mold': 0.35,
'foreign_object': 0.30,
'rot': 0.40,
'bruise': 0.50,
'burn': 0.50,
'crack': 0.45,
'insect_damage': 0.45,
'size_defect': 0.60,
'color_defect': 0.60,
'ok': 0.60,
}
results = evaluate_food_model(
model_path="./runs/food_quality/food_quality_v1/weights/best.pt",
test_dataset_yaml="/data/food_quality_dataset/data.yaml",
class_names=class_names,
confidence_thresholds=confidence_thresholds
)
Architektura sprzętowa dla linii kontroli przemysłowej
Przemysłowy system wizyjny do kontroli jakości żywności to nie tylko oprogramowanie: sprzęt jest tak samo fundamentalny jak model. Łańcuch optyczny (soczewka, czujnik, oświetlenie) decyduje o jakości obrazu, a obraz złej jakości nie da się zapisać w postprocessingu za pomocą najlepszego modelu AI.
Komponenty linii kontrolnej
Kompletna linia inspekcji obejmuje cztery odrębne strefy: Pozyskiwanie obraz, przetwarzanie AI, decyzja i sortowanie. Integracja tych obszarów odbywa się poprzez sygnały sprzętowe (enkoder, wyzwalacz, PLC) i komunikację przemysłowe (Ethernet/IP, PROFINET, OPC-UA).
Porównanie sprzętu do kontroli jakości żywności
| Część | Poziom wejścia | Profesjonalny | Wysoka wydajność |
|---|---|---|---|
| Pokój | USB3 Vision 5MP (asa Baslera) | GigE Vision 12MP (Basler ace2) | CoaXPress 25 MP (Allied Vision Goldeye) |
| Liczba klatek na sekundę | 30-60 klatek na sekundę | 100-200 klatek na sekundę | 300-500 klatek na sekundę |
| Ochrona | IP40 (biuro) | IP67 (rozpryski wody) | IP69K (strumień pod wysokim ciśnieniem) |
| Oświetlenie | Ogólny pierścień LED | Pasek LED ze sterownikiem | Programowalny stroboskop LED + IR |
| Wnioskowanie z GPU | NVIDIA Jetson Orin NX (16 GB) | Hailo-8 + procesor przemysłowy | RTX 4080 w komputerze przemysłowym |
| Opóźnienie wnioskowania | 8-15 ms (Jetson) | 2-5 ms (Hailo-8) | 1-3 ms (RTX 4080) |
| Koszt systemu | 5 000-15 000 EUR | 20 000-50 000 EUR | 60 000-150 000 EUR |
| Maksymalna przepustowość | 3-5 szt./sek | 10-20 szt./sek | 30-60 szt./sek |
Wizja GigE: standard komunikacji przemysłowej
GigE Vision (GenICam) to branżowy standard komunikacji między kamerami i system przetwarzania poprzez Gigabit Ethernet. Zalety w porównaniu z USB3: długość kabla do 100 metrów bez przedłużacza, obsługa PoE (Power over Ethernet), deterministyczne opóźnienie poprzez protokół Precision Time Protocol (PTP), obsługa wielu kamer pojedynczy przełącznik. Konfiguracja wymaga dedykowanej karty sieciowej z ramkami typu jumbo włączone (MTU 9000) i powinowactwo procesora dla procesu akwizycji.
System wyzwalania i synchronizacji
Synchronizacja między przenośnikiem taśmowym a pozyskiwaniem i krytyką obrazu aby uniknąć rozmycia ruchu i obrazów w połowie produktu. Zwykle używa się A enkoder obrotowy podłączony do paska, który generuje impuls co N mm przesunięcia. PLC (programowalny sterownik logiczny) odbiera impuls i generuje sygnał sprzętowy wyzwalacz kamery poprzez GPIO. Wyzwalacz sprzętowy jest niezbędny: wyzwalacz programowy wprowadza jitter o wartości 1-5 ms, który jest charakterystyczny dla szybkich taśm powoduje niedopuszczalne niewspółosiowości.
# camera_gige_acquisition.py
# Acquisizione immagini da camera GigE Vision con trigger hardware
import pypylon.pylon as pylon
import numpy as np
import cv2
import threading
import queue
import time
from dataclasses import dataclass
from typing import Optional
@dataclass
class AcquiredFrame:
"""Frame acquisito dalla camera con metadati."""
image: np.ndarray
timestamp_ns: int
frame_id: int
trigger_counter: int
class GigEVisionCamera:
"""
Wrapper per camera GigE Vision via Basler pylibpylon.
Gestisce trigger hardware, acquisizione e buffer.
"""
def __init__(
self,
device_index: int = 0,
trigger_mode: str = "Line1", # Trigger hardware su Line1
exposure_us: int = 500, # 500 microsec: congela il moto a 2m/s
gain_db: float = 6.0,
buffer_count: int = 10,
) -> None:
self._device_index = device_index
self._trigger_mode = trigger_mode
self._exposure_us = exposure_us
self._gain_db = gain_db
self._buffer_count = buffer_count
self._camera: Optional[pylon.InstantCamera] = None
self._frame_queue: queue.Queue = queue.Queue(maxsize=100)
self._acquiring = False
self._frame_counter = 0
def connect(self) -> None:
"""Connette e configura la camera GigE Vision."""
transport_factory = pylon.TlFactory.GetInstance()
devices = transport_factory.EnumerateDevices()
if len(devices) == 0:
raise RuntimeError("Nessuna camera GigE Vision trovata")
if self._device_index >= len(devices):
raise RuntimeError(f"Camera index {self._device_index} non disponibile")
self._camera = pylon.InstantCamera(
transport_factory.CreateDevice(devices[self._device_index])
)
self._camera.Open()
# Configurazione trigger hardware
self._camera.TriggerMode.SetValue("On")
self._camera.TriggerSource.SetValue(self._trigger_mode)
self._camera.TriggerActivation.SetValue("RisingEdge")
# Configurazione esposizione
self._camera.ExposureTime.SetValue(self._exposure_us)
self._camera.Gain.SetValue(self._gain_db)
# Pixel format: Mono8 per velocità massima, BayerRG8 per colore
self._camera.PixelFormat.SetValue("BayerRG8")
# Buffer pool
self._camera.MaxNumBuffer.SetValue(self._buffer_count)
print(f"Camera connessa: {self._camera.DeviceModelName.GetValue()}")
print(f"Risoluzione: {self._camera.Width.GetValue()}x{self._camera.Height.GetValue()}")
print(f"Frame rate max: {self._camera.AcquisitionFrameRate.GetValue():.1f} FPS")
def start_acquisition(self) -> None:
"""Avvia acquisizione continua in thread separato."""
if self._camera is None:
raise RuntimeError("Camera non connessa")
self._acquiring = True
self._camera.StartGrabbing(pylon.GrabStrategy_LatestImageOnly)
acquisition_thread = threading.Thread(
target=self._acquisition_loop,
daemon=True
)
acquisition_thread.start()
print("Acquisizione avviata in modalità trigger hardware")
def _acquisition_loop(self) -> None:
"""Loop di acquisizione continua."""
while self._acquiring and self._camera.IsGrabbing():
try:
grab_result = self._camera.RetrieveResult(
5000, # Timeout 5 secondi
pylon.TimeoutHandling_ThrowException
)
if grab_result.GrabSucceeded():
# Conversione immagine
converter = pylon.ImageFormatConverter()
converter.OutputPixelFormat = pylon.PixelType_BGR8packed
converted = converter.Convert(grab_result)
image = converted.GetArray()
frame = AcquiredFrame(
image=image.copy(),
timestamp_ns=grab_result.TimeStamp,
frame_id=grab_result.ImageNumber,
trigger_counter=self._frame_counter
)
# Non-blocking: scarta se coda piena (priorità ai frame più recenti)
try:
self._frame_queue.put_nowait(frame)
except queue.Full:
# Scarta frame vecchio, inserisci nuovo
try:
self._frame_queue.get_nowait()
except queue.Empty:
pass
self._frame_queue.put_nowait(frame)
self._frame_counter += 1
grab_result.Release()
except pylon.TimeoutException:
pass # Nessun trigger ricevuto nel timeout, normale
except Exception as e:
print(f"Errore acquisizione: {e}")
def get_frame(self, timeout: float = 1.0) -> Optional[AcquiredFrame]:
"""Recupera il prossimo frame dalla coda."""
try:
return self._frame_queue.get(timeout=timeout)
except queue.Empty:
return None
def stop(self) -> None:
"""Ferma acquisizione e disconnette la camera."""
self._acquiring = False
if self._camera and self._camera.IsGrabbing():
self._camera.StopGrabbing()
if self._camera:
self._camera.Close()
print("Camera disconnessa")
Automatyczny system sortowania: Integracja ze sterownikami PLC i siłownikami
Dane wyjściowe systemu wizyjnego muszą przełożyć się na działanie fizyczne: wydalenie wadliwego produktu z linii. Odbywa się to za pomocą siłowników pneumatycznych (dysze sprężonego powietrza) lub roboty typu pick-and-place, sterowane przez sterownik PLC na panelu podstawie decyzji systemu AI.
Czas działania systemu sortowania
Czas ma kluczowe znaczenie: system musi obliczać, zaczynając od momentu wyzwalacz (akwizycja obrazu), dokładny czas dotarcia produktu do stacji wyrzutu i sterować siłownikiem z dokładnością do milisekundy. Łańcuch czasowy obejmuje: czas akwizycji, czas wnioskowania AI, czas komunikacji ze sterownikiem PLC, czas reakcji siłownika pneumatycznego (typowo 30-80 ms łącznie z opóźnieniem otwarcia zaworu).
# sorting_system.py
# Sistema di sorting integrato con PLC via OPC-UA
import asyncio
import asyncua
from ultralytics import YOLO
import numpy as np
import time
from dataclasses import dataclass
from typing import Optional
from enum import IntEnum
class SortingDecision(IntEnum):
"""Decisioni di sorting basate sulla criticita del difetto."""
ACCEPT = 0 # Prodotto conforme: procede
REJECT_DEFECT = 1 # Difetto standard: canale scarto generico
REJECT_CRITICAL = 2 # Difetto critico (corpo estraneo, muffa): canale separato
REINSPECT = 3 # Bassa confidence: re-ispezione manuale
@dataclass
class InspectionResult:
"""Risultato ispezione con decisione di sorting."""
frame_id: int
timestamp_ms: float
decision: SortingDecision
primary_defect: Optional[str]
confidence: float
defect_count: int
inference_time_ms: float
class FoodInspectionEngine:
"""
Engine principale di ispezione: integra vision AI e logica di sorting.
"""
# Classificazione criticita difetti
CRITICAL_DEFECTS = {'foreign_object', 'mold', 'rot'}
STANDARD_DEFECTS = {'bruise', 'burn', 'crack', 'insect_damage'}
QUALITY_DEFECTS = {'size_defect', 'color_defect'}
# Soglie confidence per classe
CLASS_THRESHOLDS = {
'foreign_object': 0.30,
'mold': 0.35,
'rot': 0.38,
'crack': 0.45,
'bruise': 0.50,
'insect_damage': 0.48,
'burn': 0.50,
'size_defect': 0.60,
'color_defect': 0.60,
'ok': 0.60,
}
def __init__(self, model_path: str) -> None:
self._model = YOLO(model_path)
self._class_names = self._model.names
self._inspection_count = 0
self._defect_count = 0
self._reject_count = 0
def inspect(self, image: np.ndarray, frame_id: int) -> InspectionResult:
"""
Esegue l'ispezione AI su un frame e restituisce la decisione di sorting.
"""
start_time = time.perf_counter()
# Inference YOLO11
results = self._model(
image,
conf=0.25, # Soglia bassa: filtraggio per classe dopo
iou=0.45,
verbose=False,
half=True, # FP16 per velocità
)
inference_time_ms = (time.perf_counter() - start_time) * 1000
# Analisi detections
detections = []
for result in results:
if result.boxes is None:
continue
for box in result.boxes:
class_id = int(box.cls[0])
class_name = self._class_names[class_id]
confidence = float(box.conf[0])
# Applica soglia per classe
class_threshold = self.CLASS_THRESHOLDS.get(class_name, 0.5)
if confidence >= class_threshold and class_name != 'ok':
detections.append({
'class': class_name,
'confidence': confidence,
'bbox': box.xyxy[0].tolist(),
})
# Logica di decisione sorting
decision, primary_defect, max_confidence = self._make_sorting_decision(detections)
self._inspection_count += 1
if decision != SortingDecision.ACCEPT:
self._reject_count += 1
return InspectionResult(
frame_id=frame_id,
timestamp_ms=time.time() * 1000,
decision=decision,
primary_defect=primary_defect,
confidence=max_confidence,
defect_count=len(detections),
inference_time_ms=inference_time_ms,
)
def _make_sorting_decision(
self,
detections: list
) -> tuple[SortingDecision, Optional[str], float]:
"""
Logica di decisione sorting basata sui difetti rilevati.
Priorità: REJECT_CRITICAL > REJECT_DEFECT > REINSPECT > ACCEPT
"""
if not detections:
return SortingDecision.ACCEPT, None, 0.0
# Verifica presenza difetti critici (priorità assoluta)
critical_detections = [
d for d in detections
if d['class'] in self.CRITICAL_DEFECTS
]
if critical_detections:
primary = max(critical_detections, key=lambda x: x['confidence'])
return SortingDecision.REJECT_CRITICAL, primary['class'], primary['confidence']
# Verifica difetti standard
standard_detections = [
d for d in detections
if d['class'] in self.STANDARD_DEFECTS
]
if standard_detections:
primary = max(standard_detections, key=lambda x: x['confidence'])
# Se confidence bassa (0.45-0.55) su difetto standard: reinspect
if primary['confidence'] < 0.55:
return SortingDecision.REINSPECT, primary['class'], primary['confidence']
return SortingDecision.REJECT_DEFECT, primary['class'], primary['confidence']
# Solo difetti qualità
quality_detections = [
d for d in detections
if d['class'] in self.QUALITY_DEFECTS
]
if quality_detections:
primary = max(quality_detections, key=lambda x: x['confidence'])
return SortingDecision.REJECT_DEFECT, primary['class'], primary['confidence']
return SortingDecision.ACCEPT, None, 0.0
def get_statistics(self) -> dict:
"""Statistiche di ispezione in tempo reale."""
reject_rate = self._reject_count / max(self._inspection_count, 1) * 100
return {
'total_inspected': self._inspection_count,
'total_rejected': self._reject_count,
'reject_rate_pct': round(reject_rate, 2),
'throughput': self._inspection_count, # Da normalizzare nel tempo
}
class PLCInterface:
"""
Interfaccia OPC-UA verso PLC Siemens/Beckhoff per controllo attuatori.
"""
# Indirizzi OPC-UA nodi PLC (configurati nel TIA Portal o TwinCAT)
SORT_COMMAND_NODE = "ns=2;s=FoodLine.Sort.Command"
SORT_DELAY_MS_NODE = "ns=2;s=FoodLine.Sort.DelayMs"
CONVEYOR_SPEED_NODE = "ns=2;s=FoodLine.Conveyor.SpeedMps"
INSPECTION_TO_EJECTOR_MM = 450.0 # Distanza fisica stazione ispezione -> espulsore
def __init__(self, plc_url: str = "opc.tcp://192.168.1.100:4840") -> None:
self._plc_url = plc_url
self._client: Optional[asyncua.Client] = None
async def connect(self) -> None:
"""Connette al PLC via OPC-UA."""
self._client = asyncua.Client(url=self._plc_url)
await self._client.connect()
print(f"Connesso al PLC: {self._plc_url}")
async def get_conveyor_speed(self) -> float:
"""Legge velocità nastro in m/s dal PLC."""
node = self._client.get_node(self.CONVEYOR_SPEED_NODE)
return await node.read_value()
async def send_sort_command(
self,
decision: SortingDecision,
conveyor_speed_mps: float
) -> None:
"""
Invia comando di sorting al PLC con delay calcolato.
Il delay compensa il ritardo di trasporto dalla stazione di ispezione
all'attuatore di espulsione.
"""
if decision == SortingDecision.ACCEPT:
return # Nessuna azione necessaria
# Calcola delay: distanza / velocità - anticipo attuatore
transport_delay_ms = (self.INSPECTION_TO_EJECTOR_MM / 1000) / conveyor_speed_mps * 1000
actuator_lead_ms = 60 # Il valvola pneumatica richiede ~60ms per aprirsi
total_delay_ms = max(0, int(transport_delay_ms - actuator_lead_ms))
# Codice comando per PLC
plc_command = 1 if decision in [SortingDecision.REJECT_DEFECT, SortingDecision.REJECT_CRITICAL] else 0
# Scrivi delay e comando
delay_node = self._client.get_node(self.SORT_DELAY_MS_NODE)
command_node = self._client.get_node(self.SORT_COMMAND_NODE)
await delay_node.write_value(total_delay_ms)
await command_node.write_value(plc_command)
async def disconnect(self) -> None:
"""Disconnette dal PLC."""
if self._client:
await self._client.disconnect()
Wdróż na krawędzi: optymalizacja pod kątem sprzętu przemysłowego
Wyszkolony model musi zostać zoptymalizowany pod kątem wdrożenia na sprzęcie docelowym. YOLO11 obsługuje wiele formatów eksportu, co może zmniejszyć opóźnienia o 30-70% w porównaniu do natywnego modelu PyTorch.
# deploy_optimization.py
# Export e ottimizzazione modello per deploy industriale
from ultralytics import YOLO
import torch
import time
import numpy as np
def export_optimized_model(
model_path: str,
target_hardware: str = "tensorrt", # tensorrt, openvino, onnx, hailo
image_size: int = 640,
batch_size: int = 1,
) -> str:
"""
Esporta il modello YOLO11 nel formato ottimizzato per l'hardware target.
Formati supportati per applicazioni industriali:
- TensorRT (NVIDIA GPU): massima velocità su CUDA hardware
- OpenVINO (Intel CPU/iGPU): ottimale per sistemi embedded Intel
- ONNX (universale): compatibile con ONNX Runtime su qualsiasi hardware
- Hailo: formato proprietario per chip Hailo-8/Hailo-15
"""
model = YOLO(model_path)
export_args = {
'format': target_hardware,
'imgsz': image_size,
'batch': batch_size,
'half': True, # FP16: dimezza memoria, +30% velocità
'int8': False, # INT8 richiede calibration dataset
'simplify': True, # Semplifica grafo ONNX
'dynamic': False, # Batch size fisso per latenza deterministica
'verbose': False,
}
if target_hardware == "tensorrt":
export_args.update({
'workspace': 4, # GB di workspace TensorRT
'device': '0', # GPU 0
})
exported_path = model.export(**export_args)
print(f"Modello esportato: {exported_path}")
return str(exported_path)
def benchmark_inference(model_path: str, n_iterations: int = 1000) -> dict:
"""
Benchmark di latenza per confronto tra formati export.
"""
model = YOLO(model_path)
# Immagine di test casuale (simula frame camera)
dummy_image = np.random.randint(0, 255, (640, 640, 3), dtype=np.uint8)
# Warmup
for _ in range(50):
model(dummy_image, verbose=False)
# Benchmark
latencies = []
for _ in range(n_iterations):
start = time.perf_counter()
model(dummy_image, verbose=False, half=True)
latencies.append((time.perf_counter() - start) * 1000)
latencies = np.array(latencies)
results = {
'mean_ms': float(np.mean(latencies)),
'median_ms': float(np.median(latencies)),
'p95_ms': float(np.percentile(latencies, 95)),
'p99_ms': float(np.percentile(latencies, 99)),
'max_fps': round(1000 / np.mean(latencies), 1),
}
print(f"\nBenchmark {n_iterations} iterazioni:")
print(f" Latenza media: {results['mean_ms']:.2f} ms")
print(f" Latenza P95: {results['p95_ms']:.2f} ms")
print(f" Latenza P99: {results['p99_ms']:.2f} ms")
print(f" FPS massimo: {results['max_fps']:.1f} FPS")
return results
# Esempio: confronto latenze per hardware diversi
# Risultati tipici su linea produttiva reale:
# PyTorch FP32: 12.4 ms -> 80 FPS
# PyTorch FP16: 7.1 ms -> 140 FPS
# TensorRT FP16: 2.8 ms -> 357 FPS
# TensorRT INT8: 1.9 ms -> 526 FPS
# ONNX Runtime: 5.2 ms -> 192 FPS
# OpenVINO: 3.8 ms -> 263 FPS
Studium przypadku: Linia do sortowania owoców z YOLO11
Przedstawiamy rzeczywiste wdrożenie systemu wizyjnego dla spółdzielni gospodarstwo owocowe z południowych Włoch, linia do pakowania jabłek i pomarańczy o wydajności z 10 sztuk na sekundę na kanał, 3 kanały równoległe, sprawne W okresie żniw (październik-styczeń) całodobowo.
Specyfikacje systemu
- Produkty: Jabłka (Fuji, Gala, Golden), pomarańcze (Tarocco, Navel)
- Linia: 3 kanały po 10 szt./sek. = łącznie 30 szt./sek
- Pokój: Basler ace2 GigE, 12MP, 200 FPS, IP67, współosiowe oświetlenie LED
- Wnioskowanie sprzętowe: NVIDIA Jetson AGX Orin 64 GB (1 na kanał)
- Siłowniki: 3 dysze pneumatyczne pod ciśnieniem 6 barów na kanał (krytyka/usterka/ponowna inspekcja)
- Sterownik PLC: Siemens S7-1500 z komunikacją OPC-UA
- Wykryte klasy: ok, pleśń, siniak, oparzenie, wada_rozmiaru, obiekt_obcy
Zbiory danych i szkolenia
- Zbiór danych: łącznie 28 400 zdjęć zebranych w 3 sezonach kolekcji (2022–2024)
- Adnotacja: CVAT z 4 adnotatorami, głosowanie konsensusowe dla klas niejednoznacznych
- Augmentacja: niestandardowy potok albuminacji z symulacją warunków pogodowych
- Model: YOLO11m trenowany przez 120 epok na AWS p3.2xlarge (Tesla V100)
- Czas szkolenia: 4,7 godziny
Wyniki w produkcji
Wskaźniki systemowe w produkcji (średni sezon 2024–2025)
| Metryczny | Cel | Wynik | Ocena |
|---|---|---|---|
| mAP@50 globalnie | > 90% | 93,7% | Doskonały |
| Przypomnij sobie pleśń | > 98% | 98,4% | Zatwierdzony |
| Przypomnij sobie obcy_obiekt | > 99,5% | 99,6% | Zatwierdzony |
| Przypomnij sobie siniak | > 90% | 91,8% | Zatwierdzony |
| Precyzja w porządku | > 92% | 94,2% | Doskonały |
| Opóźnienie wnioskowania | < 8 ms | 6,3 ms (Jetson AGX) | Zatwierdzony |
| Całkowite opóźnienie systemu | < 80 ms | 71 ms | Zatwierdzony |
| Przepustowość na kanał | 10 szt./sek | 10,0 szt./sek | Osiągnięty |
| Wskaźnik fałszywie pozytywnych odrzuceń | < 3% | 2,1% | Doskonały |
| Czas pracy systemu | > 99% | 99,7% | Doskonały |
Zwrot z inwestycji i wpływ ekonomiczny
System zastąpił 6 ręcznych inspektorów (2 na zmianę x 3 zmiany), co było kosztowne instalacji 145 000 EUR (sprzęt, oprogramowanie, integracja, szkolenie, uruchomienie). Obliczone korzyści ekonomiczne:
- Redukcja personelu inspekcyjnego: 180 000 EUR/rok (brutto z opłatami)
- Redukcja przeoczonych niezgodności: 45 000 EUR/rok (uniknięte wycofania, sankcje)
- Redukcja fałszywych alarmów w porównaniu z inspekcją ręczną: 28 000 EUR/rok (produkt odzyskany)
- Zwrot inwestycji: 8,5 miesiąca
- 3-letni zwrot z inwestycji: 478%
Przepisy i certyfikaty dotyczące systemów wizyjnych w środowiskach spożywczych
Przemysłowy system wizyjny zainstalowany na certyfikowanej linii spożywczej muszą spełniać szczegółowe wymogi regulacyjne, które wykraczają poza samą wydajność sztucznej inteligencji. Niezastosowanie się do tych wymagań może podważyć certyfikaty zakładu.
Wymagania fizyczne: IP69K i materiały dopuszczone do kontaktu z żywnością
Komory przemysłowe do środowisk spożywczych muszą posiadać odpowiednią ocenę IP69K: pełna ochrona przed wnikaniem kurzu (6) i strumieniami wody wysokie ciśnienie (9K) - czyszczenie myjkami ciśnieniowymi w temperaturze 80 stopni, 100 bar standard w wielu placówkach. Materiały mające kontakt z produktem spożywczym (osłony, mocowania, wsporniki) muszą być włożone Stal AISI 316L lub materiały posiadające certyfikat FDA/EC 10/2011 dla polimerów.
Kable i złącza muszą mieć certyfikat do stosowania w środowisku spożywczym (powłoka TPU odporny na kwasy detergentowe, złącza M12 IP67+). Komputery przemysłowe obróbka odbywa się w szafach do ocynowania IP65 z dala od linii, z wyjściem wideo/sygnału do kamery poprzez ekranowany kabel GigE.
IFS Food i BRC: Wymagania dotyczące systemów automatycznej kontroli
Standard IFS Żywność 8 (Międzynarodowe standardy wyróżnione) e Globalny standard BRC dotyczący bezpieczeństwa żywności, wydanie 9 wymagają takich systemów kontroli automatycznej to:
- Udokumentowane specyfikacjami wykrywania (klasy, progi, produkty objęte)
- Okresowa kalibracja ze znanymi próbkami referencyjnymi (test obciążeniowy)
- Poddano wstępnej walidacji z udokumentowanym protokołem (OQ/PQ)
- Zintegrowany z planem HACCP jako CCP lub OPRP, w zależności od ryzyka
- Wyposażony w system alarmowy w przypadku nieprawidłowego działania (jeśli system AI jest w trybie offline, linia zatrzymuje się)
- Z zastrzeżeniem udokumentowanej konserwacji zapobiegawczej (czyszczenie obiektywu, kalibracja aparatu)
HACCP: System wizyjny jako CCP
Do wykrywania ciał obcych (metal, plastik, szkło, kamień), system wizyjny można zakwalifikować jako Krytyczny punkt kontroli (CCP) w planie HACCP, zastępując lub uzupełniając tradycyjny wykrywacz metali. Wymaga to walidacji naukowej, która wykaże możliwości systemu wykryć rodzaj ciała obcego o minimalnej wielkości, stabilny jako krytyczny (zwykle 2-3 mm dla metali, 5-10 mm dla tworzyw sztucznych).
Przestroga: Ograniczenia systemu wizyjnego opartego na ciałach obcych
System wizyjny wykrywa jedynie ciała obce widoczne na powierzchni. Ciała obce znajdujące się wewnątrz produktu (np. metal wewnątrz pomidora) nie są widoczne dla kamery optycznej. Do wykrywania wewnętrznych ciał obcych, indukcyjny wykrywacz metali lub system rentgenowski pozostają obowiązkowe oraz uzupełnienie systemu wizyjnego.
Walidacja systemu: OQ i PQ
# validation_protocol.py
# Protocollo di validazione OQ/PQ per sistema vision alimentare
import json
import datetime
from dataclasses import dataclass, field, asdict
from typing import Optional
from ultralytics import YOLO
import numpy as np
@dataclass
class ChallengeTestResult:
"""Risultato di un singolo challenge test."""
defect_class: str
defect_severity: str # 'mild', 'moderate', 'severe'
n_samples: int
detected_correctly: int
false_positives_on_ok: int
recall: float
precision: float
pass_fail: str
@dataclass
class ValidationReport:
"""Report di validazione OQ/PQ del sistema vision."""
system_id: str
product_type: str
validation_date: str
model_version: str
hardware_config: dict
challenge_results: list[ChallengeTestResult] = field(default_factory=list)
overall_result: str = "PENDING"
validated_by: str = ""
notes: str = ""
def to_json(self, output_path: str) -> None:
"""Esporta il report in formato JSON per documentazione normativa."""
report_dict = asdict(self)
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(report_dict, f, indent=2, ensure_ascii=False)
print(f"Report salvato: {output_path}")
def run_challenge_test(
model: YOLO,
challenge_images_dir: str,
defect_class: str,
severity: str,
n_ok_images: int = 50,
acceptance_recall: float = 0.95,
acceptance_precision: float = 0.85,
) -> ChallengeTestResult:
"""
Esegue un challenge test su campioni noti.
I challenge samples sono immagini acquisite dalla linea reale,
etichettate da ispettori esperti, con difetti di gravita nota.
Sono conservati fisicamente (campioni di riferimento) e fotografati
in condizioni di linea controllate.
"""
import glob
import os
# Immagini con difetto della classe specificata
defect_images = glob.glob(
os.path.join(challenge_images_dir, defect_class, severity, "*.jpg")
)
# Immagini ok di riferimento
ok_images = glob.glob(
os.path.join(challenge_images_dir, "ok", "*.jpg")
)[:n_ok_images]
detected = 0
total_defect = len(defect_images)
for img_path in defect_images:
result = model(img_path, verbose=False)
detections = [
r for r in result[0].boxes
if model.names[int(r.cls[0])] == defect_class
and float(r.conf[0]) >= 0.35 # Soglia del sistema
]
if len(detections) > 0:
detected += 1
# Falsi positivi su immagini ok
fp_count = 0
for img_path in ok_images:
result = model(img_path, verbose=False)
fp_detections = [
r for r in result[0].boxes
if model.names[int(r.cls[0])] == defect_class
and float(r.conf[0]) >= 0.35
]
if len(fp_detections) > 0:
fp_count += 1
recall = detected / max(total_defect, 1)
precision = detected / max(detected + fp_count, 1)
passed = recall >= acceptance_recall and precision >= acceptance_precision
return ChallengeTestResult(
defect_class=defect_class,
defect_severity=severity,
n_samples=total_defect,
detected_correctly=detected,
false_positives_on_ok=fp_count,
recall=round(recall, 4),
precision=round(precision, 4),
pass_fail="PASS" if passed else "FAIL"
)
Monitorowanie i pętla sprzężenia zwrotnego w produkcji
System wizyjny AI w produkcji nie jest statycznym artefaktem: warunkami zmiany linii (sezonowość produktu, zużycie oświetlenia, zabrudzenia na soczewkach), a działanie modelu należy stale monitorować aby wykryć dryf modelu, zanim wpłynie to na jakość.
# production_monitor.py
# Monitoring continuo del sistema vision in produzione
import sqlite3
import time
import json
from collections import deque
from typing import Optional
from dataclasses import dataclass
@dataclass
class ProductionStats:
"""Statistiche di produzione per monitoraggio."""
timestamp: str
window_minutes: int
total_inspected: int
reject_rate_pct: float
defect_distribution: dict
avg_inference_ms: float
p99_inference_ms: float
alert_triggered: bool
alert_reason: Optional[str]
class ProductionMonitor:
"""
Monitor continuo per sistema vision alimentare.
Rileva anomalie statistiche che indicano degradazione del modello.
"""
# Soglie di alert
MAX_REJECT_RATE_PCT = 15.0 # >15% scarto e anomalo
MIN_REJECT_RATE_PCT = 0.1 # <0.1% potrebbe indicare modello non funzionante
MAX_INFERENCE_P99_MS = 15.0 # Latenza P99 non deve superare 15ms
MAX_CONSECUTIVE_ACCEPTS = 500 # 500 ok di fila = modello probabilmente bloccato
def __init__(self, db_path: str = "/data/production_log.db") -> None:
self._db_path = db_path
self._inspection_buffer: deque = deque(maxlen=10000)
self._consecutive_accepts = 0
self._init_db()
def _init_db(self) -> None:
"""Inizializza database SQLite per log produzioni."""
with sqlite3.connect(self._db_path) as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS inspections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp REAL,
frame_id INTEGER,
decision INTEGER,
primary_defect TEXT,
confidence REAL,
inference_ms REAL,
line_speed_mps REAL
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS alerts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp REAL,
alert_type TEXT,
details TEXT,
acknowledged INTEGER DEFAULT 0
)
""")
def log_inspection(self, result: dict) -> Optional[str]:
"""
Logga un'ispezione e verifica anomalie.
Ritorna il motivo dell'alert se presente, None altrimenti.
"""
self._inspection_buffer.append(result)
# Tracking accept consecutivi
if result['decision'] == 0: # ACCEPT
self._consecutive_accepts += 1
else:
self._consecutive_accepts = 0
# Alert: troppi accept consecutivi (modello potrebbe essere bloccato)
if self._consecutive_accepts >= self.MAX_CONSECUTIVE_ACCEPTS:
alert_msg = (
f"Alert: {self._consecutive_accepts} accept consecutivi. "
f"Verificare funzionamento sistema vision."
)
self._log_alert("CONSECUTIVE_ACCEPTS", alert_msg)
self._consecutive_accepts = 0 # Reset dopo alert
return alert_msg
# Controlla reject rate su finestra recente (ultimi 100 pezzi)
if len(self._inspection_buffer) >= 100:
recent = list(self._inspection_buffer)[-100:]
reject_count = sum(1 for r in recent if r['decision'] != 0)
reject_rate = reject_count / len(recent) * 100
if reject_rate > self.MAX_REJECT_RATE_PCT:
alert_msg = f"Alert: tasso scarto {reject_rate:.1f}% (soglia: {self.MAX_REJECT_RATE_PCT}%)"
self._log_alert("HIGH_REJECT_RATE", alert_msg)
return alert_msg
# Controlla latenza
if result.get('inference_ms', 0) > self.MAX_INFERENCE_P99_MS:
alert_msg = f"Alert: latenza inference {result['inference_ms']:.1f}ms supera soglia"
self._log_alert("HIGH_LATENCY", alert_msg)
return alert_msg
return None
def _log_alert(self, alert_type: str, details: str) -> None:
"""Logga alert nel database."""
with sqlite3.connect(self._db_path) as conn:
conn.execute(
"INSERT INTO alerts (timestamp, alert_type, details) VALUES (?, ?, ?)",
(time.time(), alert_type, details)
)
print(f"[ALERT] {alert_type}: {details}")
def get_hourly_report(self) -> dict:
"""Genera report orario delle prestazioni di produzione."""
recent = list(self._inspection_buffer)
if not recent:
return {}
total = len(recent)
rejects = sum(1 for r in recent if r['decision'] != 0)
latencies = [r.get('inference_ms', 0) for r in recent]
defect_dist = {}
for r in recent:
defect = r.get('primary_defect') or 'ok'
defect_dist[defect] = defect_dist.get(defect, 0) + 1
return {
'total_inspected': total,
'reject_rate_pct': round(rejects / total * 100, 2),
'defect_distribution': defect_dist,
'avg_inference_ms': round(sum(latencies) / len(latencies), 2),
'p99_inference_ms': round(sorted(latencies)[int(len(latencies) * 0.99)], 2),
}
Najlepsze praktyki i anty-wzorce
Najlepsze praktyki dotyczące systemów wizyjnych do żywności
- Oświetlenie przed modelem: Zainwestuj 30% swojego budżetu sprzęt wysokiej jakości oświetlenia strukturalnego. Zbiór danych uzyskany za pomocą stabilne i spójne oświetlenie zmniejsza liczbę próbek o 40%. niezbędne do osiągnięcia tej samej wydajności.
- Liczne próbki negatywne: Zbiór danych musi zawierać co najmniej 3 razy więcej obrazów „ok” niż defektów, a obrazy „ok” muszą obejmować całą naturalną zmienność produktu (różny wiek, wielkość, odmiany).
- Comiesięczny test prowokacyjny: Wykonaj formalny test prowokacyjny co miesiąc z próbkami fizycznymi, o których wiadomo, że wykrywają dryf wzorca z powodu sezonowe zmiany produktów.
- Limity czasu i awarie: Jeśli system AI zajmuje więcej niż 2x nominalnego opóźnienia, uznaj obraz za nieważny i wyślij produkt do kanału ręcznej ponownej kontroli.
- Rejestruj każdą klatkę: Zapisz obraz i wynik każdego z nich kontrolę przez co najmniej 72 godziny. Jest to niezbędne do debugowania po incydencie oraz do zbierania nowych próbek szkoleniowych.
- Redundancja sprzętu: W przypadku linii krytycznych zainstaluj jedną pomieszczenie zapasowe skonfigurowane identycznie jak pomieszczenie podstawowe z automatycznym wyłącznikiem w przypadku niepowodzenia.
Anty-wzorce, których należy unikać
- Trening ze zdjęciami ze smartfona: Przechwycone obrazy ze smartfonem w laboratorium nie odzwierciedlają rzeczywistych warunków linii. Do gromadzenia danych zawsze używaj ostatniej komory przemysłowej.
- Pojedynczy próg ufności dla wszystkich klas: Próg mundur faworyzuje najliczniej reprezentowane klasy. Stosuj progi według klas, kalibrowany w oparciu o krytyczność wady.
- Wdróż bez okresu trybu cienia: Przed sprawdzeniem na sztuczną inteligencję, należy uruchomić system równolegle z inspekcją ręczną na co najmniej 2 tygodnie, porównywanie decyzji. Napraw nadmierne fałszywe alarmy przed uruchomieniem.
- Ignorowanie dryfu modelu: Wydajność modelu spada z biegiem czasu ze względu na różnice w produkcie, zużycie oświetlenia, brud na obiektywie. Bez aktywnego monitorowania degradacja jest cicha i niebezpieczna.
- Brak planu odzyskiwania po awarii: Jeśli system AI zawiedzie, musi istnieć plan awaryjny (inspekcja ręczna, spowolnienie linii) udokumentowane i okresowo testowane.
Wnioski i dalsze kroki
Wizja komputerowa z YOLO11 to obecnie najbardziej dojrzała i dostępna technologia do automatycznej kontroli jakości w przemyśle spożywczym. To już nie jest technologia laboratorium: działają setki systemów takich jak ten opisany w studium przypadku światowych fabryk, posiadające udokumentowane wyniki z dokładnością przekraczającą 98%, Zwrot z inwestycji poniżej 12 miesięcy i czas sprawności wynoszący ponad 99%.
Rynek sztucznej inteligencji w zakresie bezpieczeństwa i jakości żywności wzrośnie z 2,7 do 13,7 miliarda dolarów do 2030 r. (CAGR 30,9%), napędzany coraz bardziej rygorystycznymi wymogami regulacyjnymi, niedobór wyspecjalizowanej siły roboczej i wzrost kosztów wycofania. Firmy spożywcze którzy dziś inwestują w systemy wizyjne AI, zdobywają przewagę konkurencyjną strukturalne trudne do późniejszego wypełnienia.
Ścieżka techniczna opisana w tym artykule, od gromadzenia zbioru danych do adnotacji z CVAT/Roboflow, od szkolenia YOLO11, poprzez walidację z testami prowokacyjnymi, aż do wdrożenia z integracją PLC i ciągłym monitorowaniem, i ma zastosowanie do każdej linii produkcyjnej paszę z niezbędnymi dostosowaniami do konkretnego produktu.
Lista kontrolna do rozpoczęcia projektu dotyczącego wizji żywności
- Zdefiniuj priorytetowe klasy wad dla swojego produktu (początkowo maksymalnie 10)
- Zdobądź co najmniej 200 próbek na klasę za pomocą ostatecznej kamery w rzeczywistych warunkach liniowych
- Wybierz narzędzie do adnotacji (własny CVAT zapewniający prywatność, Roboflow zapewniający szybkość)
- Zacznij od YOLO11m na ograniczonym zestawie danych, aby zweryfikować podejście (2-3 dni pracy)
- Zdefiniuj progi akceptowalności (wycofanie/precyzja) wspólnie z menedżerem ds. jakości
- Zaplanuj okres trybu cienia przed uruchomieniem
- Dokumentuj od początku wszystko pod kątem wymagań IFS/BRC/HACCP
Kontynuuj w serii FoodTech
Ten artykuł jest częścią serii Technologia żywności na fedicocalo.dev. Następny artykuł dotyczy zgodności z przepisami cyfrowymi: FSMA i zgodność cyfrowa: automatyzacja procesów regulacyjnych, gdzie zobaczymy jak zautomatyzować obieg dokumentów HACCP, zarządzać planami kontrolować za pomocą narzędzi cyfrowych i przygotowywać się do audytów FDA/IFS/BRC za pomocą systemów zintegrowanej identyfikowalności.
Powiązane artykuły z innych serii:
- Seria MLOps: Jak wprowadzić modele YOLO do produkcji i monitorować za pomocą MLflow i Evidently
- Seria widzenia komputerowego: Zaawansowane sieci CNN, segmentacja semantyczna i wdrażanie brzegowe za pomocą TensorRT
- Seria inżynierii AI: Skalowalne potoki wnioskowania z serwerem wnioskowania Triton
- Zaawansowane serie głębokiego uczenia się: Techniki transferu uczenia się i adaptacji domen dla małych zbiorów danych







