From 43a34aaf00d2cb7c67685f00307880f73434b950 Mon Sep 17 00:00:00 2001 From: dillonj Date: Tue, 23 Dec 2025 18:12:01 -0700 Subject: [PATCH] added tk gui --- README.md | 89 ++++---- annotation_gui.py | 522 ++-------------------------------------------- 2 files changed, 54 insertions(+), 557 deletions(-) diff --git a/README.md b/README.md index 0025a3d1..e90ac85d 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,23 @@ -# Saw Mill Knot Detection (YOLOX/YOLO) +# Saw Mill Knot Detection -This repository contains a complete wood defect detection system using YOLOX/YOLO models, trained to detect 10 different types of wood surface defects. The system includes a web-based annotation GUI, automated training pipeline, and is optimized for deployment on OAK-D cameras. +This repository contains a complete wood defect detection system with a web-based annotation GUI and separate training/deployment scripts. Supports multiple model frameworks (RF-DETR, RT-DETR, YOLOv6, YOLOX) and is optimized for deployment on OAK-D cameras. ## šŸŽÆ Project Overview -- **Model**: YOLOX-nano (Ultralytics YOLO framework) -- **Dataset**: 20,276 wood surface defect images with 10 defect categories -- **Training**: 5 epochs, mAP50: 0.612, mAP50-95: 0.357 -- **Deployment Target**: OAK-D 4 Pro camera -- **Framework**: Ultralytics 8.3.240 +- **Models**: RF-DETR, RT-DETR, YOLOv6, YOLOX (all MIT/Apache 2.0 licensed) +- **Dataset**: 20,276 wood surface defect images +- **Annotation GUI**: Gradio-based web interface for manual annotation +- **Training Scripts**: Separate Python scripts for model training +- **Deployment**: OAK-D camera optimization with OpenVINO conversion +- **License**: All models free for commercial use ## šŸ“Š Dataset Information **Source**: [Kaggle Wood Surface Defects Dataset](https://www.kaggle.com/datasets/kirs0816/wood-surface-defects) **Classes** (10 total): -- Live knot -- Dead knot -- Knot with crack -- Crack -- Resin -- Marrow -- Quartzity -- Knot missing -- Blue stain -- Overgrown +- Live knot, Dead knot, Knot with crack, Crack, Resin +- Marrow, Quartzity, Knot missing, Blue stain, Overgrown **Dataset Split**: - Train: 16,220 images @@ -65,7 +58,8 @@ source .venv/bin/activate # or conda activate your_env python annotation_gui.py ``` -# Install dependencies +Install dependencies: +```bash pip install -U pip pip install ultralytics gradio rfdetr ``` @@ -88,44 +82,45 @@ python setup_datasets.py # Creates dataset_coco/ and updates configs python annotation_gui.py ``` +Tkinter version (new): + +```bash +python tk_annotation_gui.py +# or +./run_tk_gui.sh +``` + Open http://localhost:7860 in your browser to access the web-based annotation interface with: - Image navigation with index display -- Auto-labeling with trained YOLOX model -- Manual annotation tools +- Auto-labeling with trained models +- Manual annotation tools with delete buttons - Real-time result visualization +- Export to COCO format ### 4. Train Models -Choose from three different frameworks: +Use the dedicated training script for all frameworks: -#### RF-DETR (Highest accuracy, slower training) ```bash -python train_rfdetr.py \ - --dataset-dir dataset_coco \ - --output-dir runs/rfdetr_medium \ - --model medium \ - --epochs 50 \ - --batch-size 4 \ - --grad-accum-steps 4 \ - --lr 1e-4 +# Prepare dataset from annotations (optional) +python train_model.py --prepare-dataset --images-dir IMAGE --annotations annotations.json --dataset dataset_prepared + +# Train models with different frameworks +python train_model.py --framework rf-detr --dataset dataset_prepared --output runs/rfdetr_training --model-size medium --epochs 50 +python train_model.py --framework rtdetr --dataset dataset_prepared --output runs/rtdetr_training --model-size small --epochs 30 +python train_model.py --framework yolox --dataset dataset_prepared --output runs/yolox_training --model-size nano --epochs 50 +python train_model.py --framework yolov6 --dataset dataset_prepared --output runs/yolov6_training --model-size nano --epochs 50 + +# See TRAINING_README.md for detailed training options ``` -#### YOLOX (Balanced performance/speed) -```bash -python train_yolox.py \ - --dataset-dir dataset_yolo \ - --model yolox-nano \ - --epochs 50 \ - --batch-size 8 -``` +### 5. Convert for OAK-D Deployment -#### YOLOv6 (Fastest, edge-optimized) ```bash -python train_yolov6.py \ - --dataset-dir dataset_yolo \ - --model yolov6n \ - --epochs 50 \ - --batch-size 8 +# Convert trained model for edge deployment +python convert_for_deployment.py --model runs/training/weights/best.pt --output oak_d_deployment --img-size 640 + +# See TRAINING_README.md for deployment instructions ``` ## šŸ“ Project Structure @@ -133,9 +128,9 @@ python train_yolov6.py \ ``` saw_mill_knot_detection/ ā”œā”€ā”€ annotation_gui.py # Gradio web interface for annotation -ā”œā”€ā”€ train_rfdetr.py # RF-DETR training script -ā”œā”€ā”€ train_yolox.py # YOLOX training script -ā”œā”€ā”€ train_yolov6.py # YOLOv6 training script +ā”œā”€ā”€ train_model.py # Unified training script for all frameworks +ā”œā”€ā”€ convert_for_deployment.py # Model conversion for OAK-D deployment +ā”œā”€ā”€ TRAINING_README.md # Detailed training and deployment guide ā”œā”€ā”€ setup_datasets.py # Multi-format dataset setup script ā”œā”€ā”€ split_coco_dataset.py # Dataset splitting utility ā”œā”€ā”€ config.py # Configuration settings diff --git a/annotation_gui.py b/annotation_gui.py index 590c4aa0..f0529045 100644 --- a/annotation_gui.py +++ b/annotation_gui.py @@ -296,9 +296,6 @@ class AnnotationApp: self.current_idx = 0 self.annotations = {} # image_name -> list of boxes self.model = None - self.training_process = None - self.training_thread = None - self.training_status = "Not training" # Load images if directory provided if images_dir and images_dir.exists(): @@ -958,7 +955,13 @@ class AnnotationApp: # Add annotations boxes = self.annotations.get(filename, []) for box in boxes: - x1, y1, x2, y2 = box["bbox"] + if isinstance(box, dict) and "bbox" in box: + x1, y1, x2, y2 = box["bbox"] + score = box.get("confidence", 1.0) + else: + # Backward/experimental compatibility: [x1, y1, x2, y2] + x1, y1, x2, y2 = box + score = 1.0 w = x2 - x1 h = y2 - y1 @@ -969,7 +972,7 @@ class AnnotationApp: "bbox": [x1, y1, w, h], "area": w * h, "iscrowd": 0, - "score": box.get("confidence", 1.0) + "score": score }) ann_id += 1 @@ -978,224 +981,6 @@ class AnnotationApp: return f"āœ“ Exported {len(coco_data['annotations'])} annotations to {output_path}" - def prepare_training_dataset(self, output_dir: Path, train_split: float = 0.8, valid_split: float = 0.1): - """Prepare dataset in RF-DETR format (train/valid/test splits).""" - output_dir.mkdir(parents=True, exist_ok=True) - - # Create splits - import random - annotated_images = [img for img in self.image_paths if img.name in self.annotations and self.annotations[img.name]] - - if len(annotated_images) < 10: - return f"āš ļø Need at least 10 annotated images, have {len(annotated_images)}" - - random.shuffle(annotated_images) - n = len(annotated_images) - train_n = int(n * train_split) - valid_n = int(n * valid_split) - - splits = { - "train": annotated_images[:train_n], - "valid": annotated_images[train_n:train_n + valid_n], - "test": annotated_images[train_n + valid_n:] - } - - # Create directories and copy images - import shutil - for split_name, split_images in splits.items(): - split_dir = output_dir / split_name - split_dir.mkdir(exist_ok=True) - - # Prepare COCO JSON for this split - coco_data = { - "images": [], - "annotations": [], - "categories": [{"id": 0, "name": "knot", "supercategory": "defect"}] - } - - ann_id = 0 - for img_id, img_path in enumerate(split_images): - # Copy image - dest = split_dir / img_path.name - shutil.copy2(img_path, dest) - - # Add to COCO - img = Image.open(img_path) - width, height = img.size - - coco_data["images"].append({ - "id": img_id, - "file_name": img_path.name, - "width": width, - "height": height - }) - - # Add annotations - boxes = self.annotations.get(img_path.name, []) - for box in boxes: - x1, y1, x2, y2 = box["bbox"] - w = x2 - x1 - h = y2 - y1 - - coco_data["annotations"].append({ - "id": ann_id, - "image_id": img_id, - "category_id": 0, - "bbox": [x1, y1, w, h], - "area": w * h, - "iscrowd": 0 - }) - ann_id += 1 - - # Save COCO JSON - with (split_dir / "_annotations.coco.json").open("w") as f: - json.dump(coco_data, f, indent=2) - - return f"āœ“ Dataset prepared: {len(splits['train'])} train, {len(splits['valid'])} valid, {len(splits['test'])} test" - - def start_training(self, framework: str, dataset_dir: str, output_dir: str, model_size: str, - epochs: int, batch_size: int, lr: float, progress=gr.Progress()): - """Start training in background.""" - dataset_path = Path(dataset_dir) - output_path = Path(output_dir) - - if not dataset_path.exists(): - return "āŒ Dataset directory not found" - - if self.training_process and self.training_process.poll() is None: - return "āš ļø Training already in progress" - - output_path.mkdir(parents=True, exist_ok=True) - - # Build training command based on framework - venv_python = Path(__file__).parent / ".venv/bin/python" - - if framework == "RF-DETR": - train_script = Path(__file__).parent / "train_rfdetr.py" - # Map sizes: nano->nano, small->small, medium->medium, base->base - size_map = {"nano": "nano", "small": "small", "medium": "medium", "base": "base"} - model_arg = size_map.get(model_size, "medium") - - cmd = [ - str(venv_python), - str(train_script), - "--dataset-dir", str(dataset_path), - "--output-dir", str(output_path), - "--model", model_arg, - "--epochs", str(epochs), - "--batch-size", str(batch_size), - "--grad-accum-steps", "2", # Default grad accum - "--lr", str(lr) - ] - elif framework == "RT-DETR": - train_script = Path(__file__).parent / "train_rtdetr.py" - # Map sizes: nano->r18, small->r34, medium->r50, base->l - size_map = {"nano": "rtdetr-r18", "small": "rtdetr-r34", "medium": "rtdetr-r50", "base": "rtdetr-l"} - model_arg = size_map.get(model_size, "rtdetr-r18") - - cmd = [ - str(venv_python), - str(train_script), - "--dataset-dir", str(dataset_path), - "--output-dir", str(output_path), - "--model", model_arg, - "--epochs", str(epochs), - "--batch-size", str(batch_size), - "--lr", str(lr) - ] - elif framework == "YOLOv6": - train_script = Path(__file__).parent / "train_yolov6.py" - # Map sizes: nano->n, small->s, medium->m, base->l - size_map = {"nano": "yolov6n", "small": "yolov6s", "medium": "yolov6m", "base": "yolov6l"} - model_arg = size_map.get(model_size, "yolov6n") - - cmd = [ - str(venv_python), - str(train_script), - "--dataset-dir", str(dataset_path), - "--output-dir", str(output_path), - "--model", model_arg, - "--epochs", str(epochs), - "--batch-size", str(batch_size), - "--lr", str(lr) - ] - elif framework == "YOLOX": - train_script = Path(__file__).parent / "train_yolox.py" - # Map sizes: nano->nano, small->s, medium->m, base->l - size_map = {"nano": "yolox-nano", "small": "yolox-s", "medium": "yolox-m", "base": "yolox-l"} - model_arg = size_map.get(model_size, "yolox-nano") - - cmd = [ - str(venv_python), - str(train_script), - "--dataset-dir", str(dataset_path), - "--output-dir", str(output_path), - "--model", model_arg, - "--epochs", str(epochs), - "--batch-size", str(batch_size), - "--lr", str(lr) - ] - else: - return f"āŒ Unknown framework: {framework}" - - # Start training process - log_file = output_path / "training.log" - self.training_status = f"šŸš€ Starting {framework} training..." - - def run_training(): - try: - with log_file.open("w") as f: - self.training_process = subprocess.Popen( - cmd, - stdout=f, - stderr=subprocess.STDOUT, - text=True - ) - self.training_status = f"ā³ Training in progress (PID: {self.training_process.pid})" - self.training_process.wait() - - if self.training_process.returncode == 0: - self.training_status = "[OK] Training completed successfully!" - # Reload model with new weights - if framework == "RF-DETR": - # RF-DETR uses checkpoint_best_total.pth - best_weights = output_path / "checkpoint_best_total.pth" - model_type = "rf-detr" - elif framework == "RT-DETR": - # RT-DETR uses best.pt in weights/ subdirectory (Ultralytics) - best_weights = output_path / "weights" / "best.pt" - model_type = "rt-detr" - elif framework == "YOLOv6": - best_weights = output_path / "weights" / "best.pt" - model_type = "yolov6" - elif framework == "YOLOX": - best_weights = output_path / "weights" / "best.pt" - model_type = "yolox" - - if best_weights.exists(): - self._load_model(best_weights, model_type) - else: - self.training_status = f"āŒ Training failed (exit code {self.training_process.returncode})" - except Exception as e: - self.training_status = f"āŒ Error: {e}" - - self.training_thread = threading.Thread(target=run_training, daemon=True) - self.training_thread.start() - - return f"āœ“ Training started! Check {log_file} for progress" - - def get_training_status(self): - """Get current training status.""" - return self.training_status - - def stop_training(self): - """Stop the training process.""" - if self.training_process and self.training_process.poll() is None: - self.training_process.terminate() - self.training_status = "ā¹ļø Training stopped by user" - return "āœ“ Training process terminated" - return "āš ļø No training in progress" - def get_model_path_from_display(self, model_display: str) -> Path | None: """Get the actual model path from a display name.""" if not hasattr(self, 'available_models') or not self.available_models: @@ -1206,104 +991,6 @@ class AnnotationApp: return model['path'] return None - - def export_for_oak_d(self, model_display: str, output_dir: str = "oak_d_export", img_size: int = 640): - """Export trained model for OAK-D camera deployment.""" - try: - # Convert display name to actual path - weights_path = self.get_model_path_from_display(model_display) - if not weights_path: - return f"āŒ Model '{model_display}' not found. Try clicking 'šŸ” Scan for Models' first." - - output_path = Path(output_dir) - - if not weights_path.exists(): - return f"āŒ Model weights not found at: {weights_path}" - - output_path.mkdir(parents=True, exist_ok=True) - - # Determine model type - model_type = self._guess_model_type_from_path(weights_path) - - print(f"Exporting {model_type} model for OAK-D...") - - if model_type == "rf-detr": - # RF-DETR export - use existing export_onnx.py logic - from rfdetr import RFDETRBase - - model = RFDETRBase(pretrain_weights=str(weights_path)) - model.export() # Creates output/model.onnx - - # Move to output directory - onnx_source = Path("output/model.onnx") - if onnx_source.exists(): - onnx_dest = output_path / "rf_detr_model.onnx" - onnx_source.rename(onnx_dest) - - return f"āœ“ RF-DETR exported for OAK-D!\nšŸ“ Output: {output_path}\nšŸ”— Next: Convert ONNX to blob using blobconverter.luxonis.com" - else: - return "āŒ ONNX export failed" - - else: - # Ultralytics models (RT-DETR, YOLOv6, YOLOX) - if model_type == "rt-detr": - from ultralytics import RTDETR - model = RTDETR(str(weights_path)) - else: - from ultralytics import YOLO - model = YOLO(str(weights_path)) - - # Export to ONNX - onnx_path = model.export( - format="onnx", - imgsz=img_size, - simplify=True, - opset=11, # OAK-compatible opset - ) - - # Move ONNX to output directory - if Path(onnx_path).exists(): - final_onnx = output_path / f"{model_type}_model.onnx" - Path(onnx_path).rename(final_onnx) - onnx_path = final_onnx - - # Try to export to OpenVINO if available - try: - openvino_path = model.export( - format="openvino", - imgsz=img_size, - half=False, # Use FP32 for better compatibility - ) - - # Move OpenVINO files to output directory - if Path(openvino_path).exists(): - import shutil - openvino_dir = Path(openvino_path) - for file in openvino_dir.glob("*"): - if file.is_file(): - shutil.move(str(file), str(output_path / file.name)) - openvino_dir.rmdir() # Remove empty dir - - return f"āœ“ {model_type.upper()} exported for OAK-D!\nšŸ“ Output: {output_path}\nšŸ”— Next: Convert .xml/.bin to blob using blobconverter.luxonis.com" - - except Exception as e: - # OpenVINO not available, just return ONNX - import shutil - docker_hint = "" - if shutil.which("docker") is None: - docker_hint = "\nāš ļø Docker not found (needed for offline conversion via ModelConverter)." - return ( - f"āœ“ {model_type.upper()} exported to ONNX!\n" - f"šŸ“ Output: {output_path}\n" - f"Next: Convert ONNX -> RVC using HubAI (online) or ModelConverter (offline).\n" - f"Docs: https://docs.luxonis.com/software-v3/ai-inference/conversion/\n" - f"šŸ’” Offline conversion: Use Luxonis ModelConverter with Docker\n" - f"āš ļø OpenVINO export not available: {str(e)}" - f"{docker_hint}" - ) - - except Exception as e: - return f"āŒ Export failed: {str(e)}" def create_ui(app: AnnotationApp) -> gr.Blocks: @@ -1312,13 +999,11 @@ def create_ui(app: AnnotationApp) -> gr.Blocks: with gr.Blocks(title="Knot Annotation Tool") as demo: gr.Markdown(""" # Wood Knot Annotation Tool - **Label -> Train -> Auto-Label -> Repeat** + **Label -> Auto-Label -> Export** - Manually annotate images or use **Auto-Label** with your trained model - - Export and prepare dataset for training - - Train **RF-DETR, RT-DETR, YOLOv6, or YOLOX** (all free for commercial use!) - - Optimized for OAK-D camera deployment - - Use trained model to auto-label more images + - Export annotations to COCO format for training + - Use separate training and deployment scripts for model development """) # Settings section at the top @@ -1398,7 +1083,7 @@ def create_ui(app: AnnotationApp) -> gr.Blocks: delete_btn = gr.Button("šŸ—‘ļø Delete Last") clear_btn = gr.Button("āŒ Clear All") - gr.Markdown("### Export & Training") + gr.Markdown("### Export Annotations") export_path = gr.Textbox( label="Export Path", value="annotations_coco.json" @@ -1406,151 +1091,6 @@ def create_ui(app: AnnotationApp) -> gr.Blocks: export_btn = gr.Button("Export COCO") export_result = gr.Textbox(label="Export Result", lines=1) - # Training tab - with gr.Tab("Training"): - gr.Markdown(""" - ### Train Object Detection Model - - **Choose your framework:** - - **RF-DETR** (MIT): Custom transformer, high accuracy - - **RT-DETR** (Apache 2.0): Ultralytics transformer, great accuracy - - **YOLOv6** (MIT): Fast, proven on OAK cameras - - **YOLOX** (MIT): Similar to YOLOv6, slight differences - - **All MIT/Apache 2.0 licensed - free for commercial use!** - - **Steps:** - 1. Annotate at least 50-100 images in the Annotation tab - 2. Click "Prepare Dataset" to create train/valid/test splits - 3. Select your framework and configure training parameters - 4. Click "Start Training" (runs in background) - 5. After training, export for OAK-D deployment - """) - - with gr.Row(): - with gr.Column(): - dataset_prep_dir = gr.Textbox( - label="Dataset Output Directory", - value="dataset_prepared" - ) - train_split = gr.Slider(0.5, 0.9, 0.8, label="Train Split Ratio") - valid_split = gr.Slider(0.05, 0.3, 0.1, label="Valid Split Ratio") - prep_btn = gr.Button("šŸ“¦ Prepare Dataset", variant="secondary") - prep_result = gr.Textbox(label="Preparation Result", lines=2) - - with gr.Column(): - gr.Markdown("### Training Configuration") - model_framework = gr.Dropdown( - choices=["RF-DETR", "RT-DETR", "YOLOv6", "YOLOX"], - value="RT-DETR", - label="Model Framework", - info="All MIT/Apache 2.0 licensed - free for commercial use. Optimized for OAK cameras." - ) - train_dataset_dir = gr.Textbox( - label="Dataset Directory", - value="dataset_prepared" - ) - train_output_dir = gr.Textbox( - label="Output Directory", - value="runs/gui_training" - ) - model_size = gr.Dropdown( - choices=["nano", "small", "medium", "base"], - value=DEFAULT_MODEL_SIZE, - label="Model Size" - ) - epochs = gr.Slider(5, 100, DEFAULT_TRAIN_EPOCHS, step=5, label="Epochs") - batch_size = gr.Slider(1, 16, DEFAULT_BATCH_SIZE, step=1, label="Batch Size") - learning_rate = gr.Number(value=DEFAULT_LEARNING_RATE, label="Learning Rate") - - with gr.Row(): - start_train_btn = gr.Button("šŸš€ Start Training", variant="primary") - stop_train_btn = gr.Button("ā¹ļø Stop Training", variant="stop") - refresh_status_btn = gr.Button("šŸ”„ Refresh Status") - - training_status = gr.Textbox( - label="Training Status", - value="Not training", - lines=3 - ) - - gr.Markdown(""" - **Note**: Training runs in the background. You can continue annotating while training. - Check the training log file for detailed progress. - """) - - # OAK-D Deployment tab - with gr.Tab("šŸš€ OAK-D Deployment"): - gr.Markdown(""" - ### Deploy Trained Model to OAK-D Camera - - Convert your trained model to work with the **OAK-D 4 Pro** camera for real-time edge inference. - - **Supported Models**: RF-DETR, RT-DETR, YOLOv6, YOLOX - - **Process**: - 1. Select a trained model from your runs/ directory - 2. Export to ONNX and OpenVINO formats - 3. Convert OpenVINO model to blob for OAK-D - 4. Deploy blob to your OAK-D camera - """) - - with gr.Row(): - with gr.Column(): - oak_model_selector = gr.Dropdown( - choices=app.get_available_models_list(), - value=None, - label="Select Trained Model", - info="Choose a model from your training runs", - allow_custom_value=True - ) - oak_output_dir = gr.Textbox( - label="Output Directory", - value="oak_d_deployment", - placeholder="oak_d_deployment" - ) - oak_img_size = gr.Dropdown( - choices=[320, 416, 512, 640, 800, 1024], - value=640, - label="Image Size", - info="Input size for the model (should match training)" - ) - - with gr.Row(): - oak_scan_btn = gr.Button("šŸ” Scan for Models") - oak_export_btn = gr.Button("šŸš€ Export for OAK-D", variant="primary") - - oak_status = gr.Textbox( - label="Export Status", - value="Ready to export", - lines=4 - ) - - with gr.Column(): - gr.Markdown(""" - ### šŸ“‹ Deployment Instructions - - **After Export:** - 1. **Test OpenVINO Model** (optional): - ```bash - python -c "from openvino.runtime import Core; core = Core(); model = core.read_model('model.xml'); print('āœ“ Model loaded')" - ``` - - 2. **Convert to RVC compiled format** (recommended by Luxonis): - - Online: HubAI conversion (fastest setup) - - Offline: ModelConverter (requires Docker) - - Docs: https://docs.luxonis.com/software-v3/ai-inference/conversion/ - - 3. **Deploy to OAK-D**: - - Use DepthAI Python API - - Or use OAK-D examples with your blob - - ### šŸ’” Tips - - **Nano models** work best on edge devices - - If you quantize, use real calibration images for best accuracy - - Test inference speed vs accuracy trade-off - """) - # Event handlers def on_load(): return app.load_image("current") @@ -1643,44 +1183,6 @@ def create_ui(app: AnnotationApp) -> gr.Blocks: outputs=[export_result] ) - # Training handlers - prep_btn.click( - lambda out, train, valid: app.prepare_training_dataset(Path(out), train, valid), - inputs=[dataset_prep_dir, train_split, valid_split], - outputs=[prep_result] - ) - - start_train_btn.click( - app.start_training, - inputs=[model_framework, train_dataset_dir, train_output_dir, model_size, epochs, batch_size, learning_rate], - outputs=[training_status] - ) - - stop_train_btn.click( - app.stop_training, - outputs=[training_status] - ) - - refresh_status_btn.click( - app.get_training_status, - outputs=[training_status] - ) - - # OAK-D Deployment handlers - oak_scan_btn.click( - app.scan_for_models, - outputs=[oak_status] - ).then( - app.get_available_models_list, - outputs=[oak_model_selector] - ) - - oak_export_btn.click( - app.export_for_oak_d, - inputs=[oak_model_selector, oak_output_dir, oak_img_size], - outputs=[oak_status] - ) - # Load first image on start demo.load(on_load, outputs=[image_display, boxes_html, image_index_text, info_text])