1
0

3 کامیت‌ها 8192a3c84e ... 4bc1b9ab71

نویسنده SHA1 پیام تاریخ
  Dr-Swopt 4bc1b9ab71 include 3rd party model 3 روز پیش
  Dr-Swopt 3720bdc18a updated 3 روز پیش
  Dr-Swopt ec09741f95 cooked 3 روز پیش
6فایلهای تغییر یافته به همراه213 افزوده شده و 66 حذف شده
  1. BIN
      calibration_image_sample_data_20x128x128x3_float32.npy
  2. 135 57
      demo_app.py
  3. BIN
      palm_history.db
  4. BIN
      sawit_tbs.pt
  5. 46 9
      src/api/main.py
  6. 32 0
      test_benchmark.py

BIN
calibration_image_sample_data_20x128x128x3_float32.npy


+ 135 - 57
demo_app.py

@@ -16,32 +16,107 @@ from fpdf import FPDF
 
 @st.dialog("📘 AI Interpretation Guide")
 def show_tech_guide():
-    st.write("### 🎯 What does 'Confidence' mean?")
+    st.write("### 🧠 1. The 'Thinking' Phase: The Raw Tensor [1, 300, 6]")
     st.write("""
-    This is a probability score from **0.0 to 1.0**. 
-    - **0.90+**: The AI is nearly certain this is a bunch of this grade.
-    - **0.25 (Threshold)**: We ignore anything below this to filter out 'ghost' detections or background noise.
+    When the AI 'thinks' about an image, it doesn't see 'Ripe' or 'Unripe'. It populates a 
+    fixed-size memory buffer (Tensor) with **300 potential candidates**. Each candidate is 
+    represented by a row of 6 numbers.
+    """)
+    
+    
+    
+    st.table({
+        "Tensor Index": ["0, 1, 2, 3", "4", "5"],
+        "AI Output": ["Coordinates", "Confidence Score", "Class ID"],
+        "Programmer's Logic": ["`[x1, y1, x2, y2]`", "`float (0.0 - 1.0)`", "`int (0-5)`"]
+    })
+
+    st.write("#### 🎯 The Coordinate Paradox (Pixels vs. Ratios)")
+    st.write("""
+    Depending on the engine, the **Values at Index 0-3** speak different languages. 
+    This is why the raw numbers won't match if you swap engines:
+    """)
+    
+    col_a, col_b = st.columns(2)
+    with col_a:
+        st.info("**PyTorch Pathway (.pt)**")
+        st.write("- **Format**: Absolute Pixels")
+        st.write("- **Logic**: The AI outputs numbers mapped to the photo's resolution (e.g., `245.0`).")
+    with col_b:
+        st.success("**ONNX Pathway (.onnx)**")
+        st.write("- **Format**: Normalized Ratios")
+        st.write("- **Logic**: The AI outputs percentages (0.0 to 1.0) relative to its internal 640x640 grid (e.g., `0.38`).")
+
+    st.write("---")
+    st.write("### 🎯 2. What is 'Confidence'? (The Probability Filter)")
+    st.write("""
+    Confidence is the AI's **mathematical certainty** that an object exists in a specific box. 
+    It is the product of *Objectness* (Is something there?) and *Class Probability* (What is it?).
     """)
     
-    st.write("### 🛠️ The Raw Mathematical Tensor")
-    st.write("The AI returns a raw array of shape `[1, 300, 6]`. Here is the key:")
     st.table({
-        "Index": ["0-3", "4", "5"],
-        "Meaning": ["Coordinates (x1, y1, x2, y2)", "Confidence Score", "Class ID (0-5)"],
-        "Reality": ["The 'Box' in the image.", "The AI's certainty.", "The Ripeness Grade."]
+        "Confidence Value": ["> 0.90", "0.50 - 0.89", "< 0.25 (Threshold)"],
+        "Interpretation": ["**Certain**: Clear, unobstructed view.", "**Likely**: Valid, but possibly obscured by fronds.", "**Noise**: Discarded to prevent False Positives."]
     })
     
-    st.write("### ⚡ Inference vs. Processing Time")
+    
+
+    st.write("---")
+    st.write("### 🛠️ 3. The Custom Handler (The Translation Layer)")
     st.write("""
-    - **Inference Speed**: The time the AI model took to 'think' about the pixels.
-    - **Total Time**: Includes image uploading and database saving overhead.
+    Because ONNX returns raw ratios, we built a **Manual Scaling Handler**. It maps those 
+    `0.0 - 1.0` values back to your high-resolution photo pixels.
+    
+    This explains our two key metrics:
+    - **Inference Speed**: The time the AI spent populating the Raw Tensor.
+    - **Post-Processing**: The time our code spent 'translating' that Tensor into labels and pixels.
     """)
-    st.info("💡 **Engine Note**: ONNX is optimized for latency (~39ms), while PyTorch offers native indicator flexibility.")
 
+    st.write("---")
+    st.markdown("""
+    Your detection environment is powered by **YOLO26**, a custom architectural fork designed for zero-latency industrial sorting.
+    
+    ### ⚡ Performance Comparison
+    | Feature | YOLO26 (ONNX) | YOLO26 (Native) |
+    | :--- | :--- | :--- |
+    | **Coordinate System** | Normalized (0.0 - 1.0) | Absolute (Pixels) |
+    | **Primary Use Case** | Real-time Edge Sorting | High-Resolution Auditing |
+    | **Post-Processing** | None (NMS-Free) | Standard NMS |
+    """)
 
 # --- 1. Global Backend Check ---
 API_BASE_URL = "http://localhost:8000"
 
+# MPOB Color Map for Overlays (Global for consistency)
+overlay_colors = {
+    'Ripe': '#22c55e',       # Industrial Green
+    'Underripe': '#fbbf24',  # Industrial Orange
+    'Unripe': '#3b82f6',     # Industrial Blue
+    'Abnormal': '#dc2626',   # Critical Red
+    'Empty_Bunch': '#64748b',# Waste Gray
+    'Overripe': '#7c2d12'    # Dark Brown/Orange
+}
+
+# Helper to reset results when files change or engine switches
+def reset_single_results():
+    st.session_state.last_detection = None
+
+def reset_batch_results():
+    st.session_state.last_batch_results = None
+
+def reset_all_analysis():
+    """Global reset for all active analysis views."""
+    st.session_state.last_detection = None
+    st.session_state.last_batch_results = None
+    # Increment uploader keys to 'forget' current files (Clear Canvas)
+    if "single_uploader_key" not in st.session_state:
+        st.session_state.single_uploader_key = 0
+    st.session_state.single_uploader_key += 1
+    
+    if "batch_uploader_key" not in st.session_state:
+        st.session_state.batch_uploader_key = 0
+    st.session_state.batch_uploader_key += 1
+
 def check_backend():
     try:
         res = requests.get(f"{API_BASE_URL}/get_confidence", timeout=2)
@@ -93,43 +168,29 @@ st.sidebar.slider(
 )
 
 st.sidebar.markdown("---")
-st.sidebar.subheader("Inference Engine")
+# Inference Engine
 engine_choice = st.sidebar.selectbox(
-    "Select Model Engine",
-    ["YOLO26 (PyTorch - Native)", "YOLO26 (ONNX - High Speed)"],
+    "Select Model Engine:",
+    ["YOLO26 (ONNX - High Speed)", "YOLO26 (PyTorch - Native)", "Sawit-TBS (Benchmark)"],
     index=0,
-    help="ONNX is optimized for latency. PyTorch provides native object handling."
+    on_change=reset_all_analysis # Clear canvas on engine switch
 )
-st.sidebar.markdown("---")
-st.sidebar.subheader("🛠️ Technical Controls")
-show_trace = st.sidebar.toggle("🔬 Show Technical Trace", value=False, help="Enable to see raw mathematical tensor data alongside AI labels.")
-st.session_state.tech_trace = show_trace
-model_type = "onnx" if "ONNX" in engine_choice else "pytorch"
-if model_type == "pytorch":
-    st.sidebar.warning("PyTorch Engine: Higher Memory Usage")
-else:
-    st.sidebar.info("ONNX Engine: ~39ms Latency")
+
+# Map selection to internal labels
+engine_map = {
+    "YOLO26 (ONNX - High Speed)": "onnx",
+    "YOLO26 (PyTorch - Native)": "pytorch",
+    "Sawit-TBS (Benchmark)": "benchmark"
+}
 
 st.sidebar.markdown("---")
-if st.sidebar.button("❓ How to read results?", icon="📘", width='stretch'):
-    show_tech_guide()
+model_type = engine_map[engine_choice]
 
-# Helper to reset results when files change
-def reset_single_results():
-    st.session_state.last_detection = None
 
-def reset_batch_results():
-    st.session_state.last_batch_results = None
+if st.sidebar.button("❓ How to read results?", icon="📘", width='stretch'):
+    show_tech_guide()
 
-# MPOB Color Map for Overlays (Global for consistency)
-overlay_colors = {
-    'Ripe': '#22c55e',       # Industrial Green
-    'Underripe': '#fbbf24',  # Industrial Orange
-    'Unripe': '#3b82f6',     # Industrial Blue
-    'Abnormal': '#dc2626',   # Critical Red
-    'Empty_Bunch': '#64748b',# Waste Gray
-    'Overripe': '#7c2d12'    # Dark Brown/Orange
-}
+# Function definitions moved to top
 
 def display_interactive_results(image, detections, key=None):
     """Renders image with interactive hover-boxes using Plotly."""
@@ -151,7 +212,7 @@ def display_interactive_results(image, detections, key=None):
         x1, y1, x2, y2 = det['box']
         # Plotly y-axis is inverted relative to PIL, so we flip y
         y_top, y_bottom = img_height - y1, img_height - y2
-        color = overlay_colors.get(det['class'], "#ffeb3b")
+        color = overlay_colors.get(det['class'], "#9ca3af") # Fallback to neutral gray
 
         # The 'Hover' shape
         bunch_id = det.get('bunch_id', i+1)
@@ -169,7 +230,7 @@ def display_interactive_results(image, detections, key=None):
         ))
 
     fig.update_layout(width=800, height=600, margin=dict(l=0, r=0, b=0, t=0), showlegend=False)
-    st.plotly_chart(fig, use_container_width=True, key=key)
+    st.plotly_chart(fig, width='stretch', key=key)
 
 def annotate_image(image, detections):
     """Draws high-visibility 'Plated Labels' and boxes on the image."""
@@ -196,7 +257,7 @@ def annotate_image(image, detections):
         cls = det['class']
         conf = det['confidence']
         bunch_id = det.get('bunch_id', '?')
-        color = overlay_colors.get(cls, '#ffffff')
+        color = overlay_colors.get(cls, '#9ca3af') # Fallback to neutral gray
         
         # 2. Draw Heavy-Duty Bounding Box
         line_width = max(4, image.width // 150)
@@ -333,10 +394,14 @@ tab1, tab2, tab3, tab4 = st.tabs(["Single Analysis", "Batch Processing", "Simila
 # --- Tab 1: Single Analysis ---
 with tab1:
     st.subheader("Analyze Single Bunch")
+    # 1. Initialize Uploader Key
+    if "single_uploader_key" not in st.session_state:
+        st.session_state.single_uploader_key = 0
+
     uploaded_file = st.file_uploader(
         "Upload a bunch image...", 
         type=["jpg", "jpeg", "png"], 
-        key="single",
+        key=f"single_{st.session_state.single_uploader_key}",
         on_change=reset_single_results
     )
     
@@ -359,6 +424,11 @@ with tab1:
 
         # 2. Results Layout
         if st.session_state.last_detection:
+            # Redo Button at the top for easy access
+            if st.button("🔄 Re-analyze Image", width='stretch', type="primary", help="Force a fresh detection (useful if threshold changed)."):
+                st.session_state.last_detection = None
+                st.rerun()
+                
             data = st.session_state.last_detection
             st.divider()
             
@@ -367,7 +437,14 @@ with tab1:
             with m_col1:
                 st.metric("Total Bunches", data.get('total_count', 0))
             with m_col2:
-                st.metric("Healthy (Ripe)", data['industrial_summary'].get('Ripe', 0))
+                if model_type == "benchmark":
+                    # For benchmark model, show the top detected class instead of 'Healthy'
+                    top_class = "None"
+                    if data.get('industrial_summary'):
+                        top_class = max(data['industrial_summary'], key=data['industrial_summary'].get)
+                    st.metric("Top Detected Class", top_class)
+                else:
+                    st.metric("Healthy (Ripe)", data['industrial_summary'].get('Ripe', 0))
             with m_col3:
                 # Refined speed label based on engine
                 speed_label = "Raw Speed (Unlabeled)" if model_type == "onnx" else "Wrapped Speed (Auto-Labeled)"
@@ -397,15 +474,15 @@ with tab1:
             col1, col2 = st.columns([1.5, 1]) # Keep original col structure for summary below
             
             with col1:
-                col_tech_h1, col_tech_h2 = st.columns([4, 1])
+                col_tech_h1, col_tech_h2 = st.columns([1, 1])
                 with col_tech_h1:
                     st.write("#### 🛠️ Technical Evidence")
                 with col_tech_h2:
-                    if st.button("❓ Guide", key="guide_tab1"):
-                        show_tech_guide()
+                    st.session_state.tech_trace = st.toggle("🔬 Side-by-Side Trace", value=st.session_state.get('tech_trace', False))
                 
                 with st.expander("Raw Output Tensor (NMS-Free)", expanded=False):
-                    st.caption("See the Interpretation Guide for a breakdown of these numbers.")
+                    coord_type = "Absolute Pixels" if model_type == "pytorch" else "Normalized Ratios (0.0-1.0)"
+                    st.warning(f"Engine detected: {model_type.upper()} | Coordinate System: {coord_type}")
                     st.json(data.get('raw_array_sample', []))
                 with st.container(border=True):
                     st.write("### 🏷️ Detection Results")
@@ -717,13 +794,13 @@ with tab4:
                     # Prepare searchable dataframe
                     df_history = pd.DataFrame(history_data)
                     # Clean up for display
-                    display_df = df_history[['id', 'timestamp', 'filename', 'inference_ms']].copy()
-                    display_df.columns = ['ID', 'Date/Time', 'Filename', 'Inference (ms)']
+                    display_df = df_history[['id', 'timestamp', 'engine', 'filename', 'inference_ms']].copy()
+                    display_df.columns = ['ID', 'Date/Time', 'Engine', 'Filename', 'Inference (ms)']
                     
                     st.dataframe(
                         display_df, 
                         hide_index=True, 
-                        use_container_width=True,
+                        width='stretch',
                         column_config={
                             "ID": st.column_config.NumberColumn(width="small"),
                             "Inference (ms)": st.column_config.NumberColumn(format="%.1f ms")
@@ -740,7 +817,7 @@ with tab4:
                         )
                     with hist_col2:
                         st.write("##") # Alignment
-                        if st.button("🔬 Start Deep Dive", type="primary", use_container_width=True):
+                        if st.button("🔬 Start Deep Dive", type="primary", width='stretch'):
                             st.session_state.selected_history_id = target_id
                             st.rerun()
                 else:
@@ -756,7 +833,8 @@ with tab4:
                         
                         st.divider()
                         st.write(f"## 🔍 Deep Dive: Record #{record['id']}")
-                        st.caption(f"Original Filename: `{record['filename']}` | Processed: `{record['timestamp']}`")
+                        engine_val = record.get('engine', 'Unknown')
+                        st.caption(f"Original Filename: `{record['filename']}` | Processed: `{record['timestamp']}` | Engine: `{engine_val.upper()}`")
                         
                         detections = json.loads(record['detections'])
                         summary = json.loads(record['summary'])
@@ -783,7 +861,7 @@ with tab4:
                                 display_interactive_results(hist_img, detections, key=f"hist_plotly_{record['id']}")
                             with v_tab2:
                                 img_plate = annotate_image(hist_img.copy(), detections)
-                                st.image(img_plate, use_container_width=True, caption="Point-of-Harvest AI Interpretation")
+                                st.image(img_plate, width='stretch', caption="Point-of-Harvest AI Interpretation")
                         else:
                             st.warning(f"Technical Error: Archive file missing at `{record['archive_path']}`")
                         

BIN
palm_history.db


BIN
sawit_tbs.pt


+ 46 - 9
src/api/main.py

@@ -32,12 +32,20 @@ def init_local_db():
             archive_path TEXT,
             detections TEXT,
             summary TEXT,
+            engine TEXT,
             inference_ms REAL,
             processing_ms REAL,
             raw_tensor TEXT,
             timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
         )
     ''')
+    # Migration: Add engine column if it doesn't exist (for existing DBs)
+    try:
+        cursor.execute("ALTER TABLE history ADD COLUMN engine TEXT")
+        print("Migrated History table: Added 'engine' column.")
+    except sqlite3.OperationalError:
+        # Column already exists
+        pass
     conn.commit()
     conn.close()
     print("Local DB Initialized.")
@@ -53,11 +61,13 @@ os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "gemini-embedding-service-key.jso
 app = FastAPI(title="Palm Oil Ripeness Service (DDD)")
 
 class ModelManager:
-    def __init__(self, onnx_path: str, pt_path: str):
+    def __init__(self, onnx_path: str, pt_path: str, benchmark_path: str = 'sawit_tbs.pt'):
         self.onnx_session = ort.InferenceSession(onnx_path)
         self.onnx_input_name = self.onnx_session.get_inputs()[0].name
         self.pt_model = YOLO(pt_path)
         self.class_names = self.pt_model.names
+        self.benchmark_model = YOLO(benchmark_path)
+        self.benchmark_class_names = self.benchmark_model.names
 
     def preprocess_onnx(self, img: Image.Image):
         img = img.convert("RGB")
@@ -110,17 +120,22 @@ class ModelManager:
         raw_sample = detections_batch[0, :5].tolist()
         return detections, raw_sample, inference_ms
 
-    def run_pytorch_inference(self, img: Image.Image, conf_threshold: float):
+    def run_pytorch_inference(self, img: Image.Image, conf_threshold: float, engine_type: str = "pytorch"):
         import time
         start_inf = time.perf_counter()
-        results = self.pt_model(img, conf=conf_threshold, verbose=False)
+        
+        # Selection Logic for Third Engine
+        model = self.pt_model if engine_type == "pytorch" else self.benchmark_model
+        names = self.class_names if engine_type == "pytorch" else self.benchmark_class_names
+        
+        results = model(img, conf=conf_threshold, verbose=False)
         end_inf = time.perf_counter()
         inference_ms = (end_inf - start_inf) * 1000
 
         detections = []
         for i, box in enumerate(results[0].boxes):
             class_id = int(box.cls)
-            class_name = self.class_names.get(class_id, "Unknown")
+            class_name = names.get(class_id, "Unknown")
             detections.append({
                 "bunch_id": i + 1,
                 "class": class_name,
@@ -133,6 +148,7 @@ class ModelManager:
         raw_snippet = results[0].boxes.data[:5].tolist() if len(results[0].boxes) > 0 else []
         return detections, raw_snippet, inference_ms
 
+
 model_manager = ModelManager(onnx_path='best.onnx', pt_path='best.pt')
 
 
@@ -149,7 +165,16 @@ repo = MongoPalmOilRepository(
     db_name=os.getenv("DB_NAME", "palm_oil_db"),
     collection_name=os.getenv("COLLECTION_NAME", "ffb_records")
 )
-repo.ensure_indexes()
+
+db_connected = False
+try:
+    print("Connecting to MongoDB Atlas...")
+    repo.ensure_indexes()
+    db_connected = True
+    print("MongoDB Atlas Connected.")
+except Exception as e:
+    print(f"Warning: Could not connect to MongoDB Atlas (Timeout). Cloud archival will be disabled. Details: {e}")
+
 analyze_use_case = AnalyzeBunchUseCase(vision_service, repo)
 analyze_batch_use_case = AnalyzeBatchUseCase(vision_service, repo)
 search_use_case = SearchSimilarUseCase(vision_service, repo)
@@ -185,7 +210,9 @@ async def analyze_with_health_metrics(file: UploadFile = File(...), model_type:
     start_total = time.perf_counter()
     # Select Inference Engine
     if model_type == "pytorch":
-        detections, raw_sample, inference_ms = model_manager.run_pytorch_inference(img, current_conf)
+        detections, raw_sample, inference_ms = model_manager.run_pytorch_inference(img, current_conf, "pytorch")
+    elif model_type == "benchmark":
+        detections, raw_sample, inference_ms = model_manager.run_pytorch_inference(img, current_conf, "benchmark")
     else:
         detections, raw_sample, inference_ms = model_manager.run_onnx_inference(img, current_conf)
     
@@ -194,7 +221,8 @@ async def analyze_with_health_metrics(file: UploadFile = File(...), model_type:
     processing_ms = total_ms - inference_ms
     
     # Initialize summary
-    summary = {name: 0 for name in model_manager.class_names.values()}
+    active_names = model_manager.class_names if model_type != "benchmark" else model_manager.benchmark_class_names
+    summary = {name: 0 for name in active_names.values()}
     for det in detections:
         summary[det['class']] += 1
     
@@ -210,8 +238,8 @@ async def analyze_with_health_metrics(file: UploadFile = File(...), model_type:
     # Save to SQLite
     conn = sqlite3.connect(DB_PATH)
     cursor = conn.cursor()
-    cursor.execute("INSERT INTO history (filename, archive_path, detections, summary, inference_ms, processing_ms, raw_tensor) VALUES (?, ?, ?, ?, ?, ?, ?)",
-                   (file.filename, archive_path, json.dumps(detections), json.dumps(summary), inference_ms, processing_ms, json.dumps(raw_sample)))
+    cursor.execute("INSERT INTO history (filename, archive_path, detections, summary, engine, inference_ms, processing_ms, raw_tensor) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+                   (file.filename, archive_path, json.dumps(detections), json.dumps(summary), model_type, inference_ms, processing_ms, json.dumps(raw_sample)))
     conn.commit()
     conn.close()
             
@@ -231,6 +259,8 @@ async def analyze_with_health_metrics(file: UploadFile = File(...), model_type:
 @app.post("/vectorize_and_store")
 async def vectorize_and_store(file: UploadFile = File(...), detection_data: str = Form(...)):
     """Cloud-dependent. Requires active billing."""
+    if not db_connected:
+        return {"status": "error", "message": "Cloud Archival is currently unavailable (Database Offline)."}
     import json
     try:
         primary_detection = json.loads(detection_data)
@@ -263,6 +293,10 @@ async def vectorize_and_store(file: UploadFile = File(...), detection_data: str
 @app.post("/process_batch")
 async def process_batch(files: List[UploadFile] = File(...), model_type: str = Form("onnx")):
     """Handles multiple images: Detect -> Vectorize -> Store."""
+    if not db_connected:
+        # We could still do detection locally, but the prompt says 'Detect -> Vectorize -> Store'
+        # For simplicity in this demo, we'll block it if DB is offline.
+        return {"status": "error", "message": "Batch Processing (Cloud Archival) is currently unavailable (Database Offline)."}
     batch_results = []
     temp_files = []
 
@@ -291,6 +325,7 @@ async def process_batch(files: List[UploadFile] = File(...), model_type: str = F
                 batch_results.append({
                     "path": path,
                     "yolo": det,
+                    "engine": model_type, # Track engine
                     "inference_ms": inference_ms,
                     "raw_array_sample": raw_sample
                 })
@@ -352,6 +387,8 @@ async def search_hybrid(
     limit: int = Form(3)
 ):
     """Hybrid Search: Supports Visual Similarity and Natural Language Search."""
+    if not db_connected:
+        return {"status": "error", "message": "Semantic Search is currently unavailable (Database Offline)."}
     temp_path = None
     try:
         try:

+ 32 - 0
test_benchmark.py

@@ -0,0 +1,32 @@
+import os
+import sys
+from PIL import Image
+import io
+import torch
+
+# Add the project root to sys.path to import src
+sys.path.append(os.getcwd())
+
+from src.api.main import ModelManager
+
+def test_inference():
+    print("Testing ModelManager initialization...")
+    manager = ModelManager(onnx_path='best.onnx', pt_path='best.pt', benchmark_path='sawit_tbs.pt')
+    print("ModelManager initialized successfully.")
+
+    # Create a dummy image for testing
+    img = Image.new('RGB', (640, 640), color = (73, 109, 137))
+    
+    print("\nTesting PyTorch inference (Native)...")
+    detections, raw, ms = manager.run_pytorch_inference(img, 0.25, engine_type="pytorch")
+    print(f"Detections: {len(detections)}, Inference: {ms:.2f}ms")
+    
+    print("\nTesting Benchmark inference (Sawit-TBS)...")
+    detections, raw, ms = manager.run_pytorch_inference(img, 0.25, engine_type="benchmark")
+    print(f"Detections: {len(detections)}, Inference: {ms:.2f}ms")
+    print(f"Benchmark Class Names: {manager.benchmark_class_names}")
+
+    print("\nVerification Complete.")
+
+if __name__ == "__main__":
+    test_inference()