SahilCodevally's picture
Init commit
e1c010e
Raw History Blame Contribute Delete
8.42 kB
"""
Object Mapper — Synonym normalization for bridging YOLO labels
and natural-language user commands.
YOLO may detect "cup" while the user says "mug". This module provides
a mapping layer and fuzzy-match helper to reconcile the two vocabularies.
"""
import logging
from difflib import SequenceMatcher
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Synonym Map: user-language → canonical YOLO label
# ---------------------------------------------------------------------------
SYNONYM_MAP: dict[str, str] = {
# ---------------------------------------------------------------
# COCO has 80 classes. This map bridges common user vocabulary
# to the actual YOLO/COCO label strings.
# ---------------------------------------------------------------
# person
"human": "person",
"worker": "person",
"operator": "person",
"man": "person",
"woman": "person",
"people": "person",
"employee": "person",
"technician": "person",
# bicycle
"bike": "bicycle",
"cycle": "bicycle",
# car
"automobile": "car",
"vehicle": "car",
"sedan": "car",
# motorcycle
"motorbike": "motorcycle",
"scooter": "motorcycle",
# truck
"van": "truck",
"lorry": "truck",
"pickup": "truck",
# boat
"ship": "boat",
"vessel": "boat",
"canoe": "boat",
# traffic light
"signal": "traffic light",
"stoplight": "traffic light",
# backpack
"bag": "backpack",
"rucksack": "backpack",
"knapsack": "backpack",
# umbrella
"parasol": "umbrella",
# handbag
"purse": "handbag",
"clutch": "handbag",
# suitcase (toolboxes, cases look like suitcases to YOLO)
"briefcase": "suitcase",
"toolbox": "suitcase",
"case": "suitcase",
"luggage": "suitcase",
"tool box": "suitcase",
# bottle
"water bottle": "bottle",
"flask": "bottle",
"jug": "bottle",
"canteen": "bottle",
"container": "bottle",
# wine glass
"goblet": "wine glass",
"chalice": "wine glass",
"glass": "wine glass",
# cup
"mug": "cup",
"teacup": "cup",
"coffee cup": "cup",
"tumbler": "cup",
# fork
"prong": "fork",
# knife (wrenches, tools often detected as knife-like)
"blade": "knife",
"wrench": "knife",
"spanner": "knife",
"screwdriver": "knife",
"tool": "knife",
# spoon
"ladle": "spoon",
"scoop": "spoon",
# bowl
"tray": "bowl",
"dish": "bowl",
"plate": "bowl",
"bin": "bowl",
"basket": "bowl",
# banana / apple / orange (fruits)
"fruit": "apple",
# sandwich
"sub": "sandwich",
"burger": "sandwich",
"wrap": "sandwich",
# pizza
"pie": "pizza",
# cake
"pastry": "cake",
"dessert": "cake",
# chair
"seat": "chair",
"stool": "chair",
"office chair": "chair",
# couch
"sofa": "couch",
"loveseat": "couch",
"settee": "couch",
# bed
"mattress": "bed",
"cot": "bed",
# dining table
"desk": "dining table",
"table": "dining table",
"workbench": "dining table",
"bench": "dining table",
"counter": "dining table",
# tv
"monitor": "tv",
"screen": "tv",
"display": "tv",
"television": "tv",
# laptop
"notebook": "laptop",
"computer": "laptop",
"macbook": "laptop",
# mouse
"trackpad": "mouse",
# remote
"controller": "remote",
"remote control": "remote",
# keyboard
"keypad": "keyboard",
# cell phone
"mobile": "cell phone",
"phone": "cell phone",
"smartphone": "cell phone",
"iphone": "cell phone",
"handset": "cell phone",
"scanner": "cell phone",
# microwave
"microwave oven": "microwave",
# oven
"stove": "oven",
"furnace": "oven",
# refrigerator
"fridge": "refrigerator",
"cooler": "refrigerator",
"freezer": "refrigerator",
# book
"notebook paper": "book",
"manual": "book",
"textbook": "book",
"binder": "book",
# clock
"watch": "clock",
"timer": "clock",
# vase
"pot": "vase",
"planter": "vase",
"jar": "vase",
# scissors
"cutter": "scissors",
"shears": "scissors",
"clipper": "scissors",
"pliers": "scissors",
"wire cutter": "scissors",
# teddy bear
"stuffed animal": "teddy bear",
"plush": "teddy bear",
"toy": "teddy bear",
# potted plant
"plant": "potted plant",
"houseplant": "potted plant",
"flower": "potted plant",
# -----------------------------------------------------------
# Domain-specific aliases for industrial / demo contexts
# -----------------------------------------------------------
"damaged part": "knife",
"defective part": "knife",
"component": "knife",
"metal part": "knife",
"part": "knife",
"piece": "knife",
"red bin": "bowl",
"blue bin": "bowl",
"sorting bin": "bowl",
"reject bin": "bowl",
"accept bin": "bowl",
"circuit board": "keyboard",
"pcb": "keyboard",
"board": "keyboard",
"hard hat": "sports ball",
"helmet": "sports ball",
"safety hat": "sports ball",
"gloves": "backpack",
"safety gloves": "backpack",
"pallet": "suitcase",
"crate": "suitcase",
"carton": "suitcase",
"box": "suitcase",
"hand truck": "bicycle",
"dolly": "bicycle",
"cart": "bicycle",
"forklift": "truck",
"clipboard": "book",
"blueprint": "book",
"stethoscope": "tie",
"syringe": "toothbrush",
"safety goggles": "wine glass",
"goggles": "wine glass",
"oil can": "bottle",
"spray can": "bottle",
"battery": "suitcase",
"car battery": "suitcase",
"multimeter": "cell phone",
"meter": "cell phone",
"soldering iron": "knife",
"magnifying lamp": "clock",
}
def normalize_label(label: str) -> str:
"""
Normalize a label using the synonym map.
Args:
label: Raw label string (from user command or YOLO).
Returns:
The canonical label if a synonym exists, otherwise the
original label lowercased and stripped.
"""
cleaned = label.strip().lower()
return SYNONYM_MAP.get(cleaned, cleaned)
def _similarity(a: str, b: str) -> float:
"""Return a 0-1 similarity score between two strings."""
return SequenceMatcher(None, a.lower(), b.lower()).ratio()
def find_best_match(
query: str,
detected_labels: list[str],
threshold: float = 0.4,
) -> str | None:
"""
Find the best matching detected label for a user query term.
Strategy:
1. Exact match after normalization.
2. Substring containment check.
3. Fuzzy similarity above *threshold*.
Args:
query: The term from the user command (e.g. "mug").
detected_labels: Labels returned by the vision detector.
threshold: Minimum similarity score for fuzzy matching.
Returns:
The best matching label, or ``None`` if no match is found.
"""
normalized_query = normalize_label(query)
normalized_detected = {normalize_label(lbl): lbl for lbl in detected_labels}
# 1. Exact match on normalized labels
if normalized_query in normalized_detected:
logger.info(
"Exact match: '%s' → '%s'",
query,
normalized_detected[normalized_query],
)
return normalized_detected[normalized_query]
# 2. Substring containment
for norm_lbl, orig_lbl in normalized_detected.items():
if normalized_query in norm_lbl or norm_lbl in normalized_query:
logger.info(
"Substring match: '%s' → '%s'", query, orig_lbl
)
return orig_lbl
# 3. Fuzzy matching
best_score = 0.0
best_label: str | None = None
for norm_lbl, orig_lbl in normalized_detected.items():
score = _similarity(normalized_query, norm_lbl)
if score > best_score:
best_score = score
best_label = orig_lbl
if best_label and best_score >= threshold:
logger.info(
"Fuzzy match: '%s' → '%s' (score=%.2f)",
query,
best_label,
best_score,
)
return best_label
logger.warning("No match found for query '%s'", query)
return None