{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":91249,"databundleVersionId":11294684,"sourceType":"competition"},{"sourceId":735178,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":560423,"modelId":573010}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# BYU Locating Flagellar Motors - Submission Script\n\nMinimal submission script using Roboflow inference for motor detection.\nThis script:\n1. Loads images with percentile normalization\n2. Runs Roboflow model inference\n3. Generates submission CSV","metadata":{"_uuid":"c54b18c1-dd26-4184-b031-fdea50f22eba","_cell_guid":"9c31d03f-6a7d-47a4-88fa-90a3ed29e8fb","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"!pip install -q https://github.com/roboflow/rf-detr/archive/refs/heads/develop.zip","metadata":{"_uuid":"262987be-cf9b-4fd4-a1a5-8d65079d3b73","_cell_guid":"9502795f-607f-4c75-9630-d8ae539d49d4","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-29T14:29:45.995295Z","iopub.execute_input":"2026-01-29T14:29:45.995625Z","iopub.status.idle":"2026-01-29T14:30:01.851581Z","shell.execute_reply.started":"2026-01-29T14:29:45.995588Z","shell.execute_reply":"2026-01-29T14:30:01.850720Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nimport cv2\nfrom tqdm.auto import tqdm\nfrom rfdetr import RFDETRMedium\nimport supervision as sv\nimport matplotlib.pyplot as plt # Added for visualization\n\n# Set random seed for reproducibility\nnp.random.seed(42)","metadata":{"_uuid":"b17665a2-9b51-488b-819a-6e58b54bd117","_cell_guid":"f0858fe0-57f8-46a5-bcc8-6de19105d8b9","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-29T14:30:01.853484Z","iopub.execute_input":"2026-01-29T14:30:01.853909Z","iopub.status.idle":"2026-01-29T14:30:32.411868Z","shell.execute_reply.started":"2026-01-29T14:30:01.853878Z","shell.execute_reply":"2026-01-29T14:30:32.411268Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define paths\ndata_path = \"/kaggle/input/byu-locating-bacterial-flagellar-motors-2025/\"\ntest_dir = os.path.join(data_path, \"test\")\nsubmission_path = \"submission.csv\"\nWEIGHTS_PATH = \"/kaggle/input/byu-bacterial-flagell-rfdetr/pytorch/default/1/rfdetr-medium_weights.pt\"\n\n# Detection parameters\nCONFIDENCE_THRESHOLD = 0.01 # Lowered for debugging\nNMS_IOU_THRESHOLD = 0.2\n\n# Initialize annotators globally for visualization\nbox_annotator = sv.BoxAnnotator(thickness=2)\nlabel_annotator = sv.LabelAnnotator(text_thickness=2, text_scale=1)","metadata":{"_uuid":"2a70127d-8b04-4471-b025-a71c3a54e861","_cell_guid":"48f2937b-823e-4d9d-bdf1-3b727c7312d9","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-29T14:30:32.412672Z","iopub.execute_input":"2026-01-29T14:30:32.412915Z","iopub.status.idle":"2026-01-29T14:30:32.417163Z","shell.execute_reply.started":"2026-01-29T14:30:32.412885Z","shell.execute_reply":"2026-01-29T14:30:32.416463Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def normalize_slice(slice_data):\n    \"\"\"Normalize slice data using 2nd and 98th percentiles for better contrast\"\"\"\n    p2 = np.percentile(slice_data, 2)\n    p98 = np.percentile(slice_data, 98)\n    clipped_data = np.clip(slice_data, p2, p98)\n    normalized = 255 * (clipped_data - p2) / (p98 - p2)\n    return np.uint8(normalized)","metadata":{"_uuid":"fa21d1ca-3789-4179-b2ad-6cc7049aadfc","_cell_guid":"23bdd2af-e104-4cca-932c-b0453bded103","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-29T14:30:32.418231Z","iopub.execute_input":"2026-01-29T14:30:32.418630Z","iopub.status.idle":"2026-01-29T14:30:32.430057Z","shell.execute_reply.started":"2026-01-29T14:30:32.418582Z","shell.execute_reply":"2026-01-29T14:30:32.429453Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_and_normalize_image(image_path):\n    \"\"\"Load image and apply percentile normalization\"\"\"\n    # Load image as grayscale\n    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)\n    if img is None:\n        # Fallback to PIL\n        img = np.array(Image.open(image_path).convert('L'))\n    # Apply normalization\n    img_normalized = normalize_slice(img)\n    # Convert to 3-channel for model compatibility\n    return cv2.cvtColor(img_normalized, cv2.COLOR_GRAY2RGB)","metadata":{"_uuid":"d2d9c7f2-d5ba-4919-86e2-186d6c422d7c","_cell_guid":"1c16262c-c5f8-4f16-9482-ea3a91ee72f3","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-29T14:30:32.430992Z","iopub.execute_input":"2026-01-29T14:30:32.431428Z","iopub.status.idle":"2026-01-29T14:30:32.441440Z","shell.execute_reply.started":"2026-01-29T14:30:32.431385Z","shell.execute_reply":"2026-01-29T14:30:32.440747Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def perform_3d_nms(detections, iou_threshold):\n    \"\"\"Perform 3D Non-Maximum Suppression on detections to merge nearby motors\"\"\"\n    if not detections:\n        print(\"No detections to perform NMS on.\")\n        return []\n    \n    # Sort by confidence (highest first)\n    detections = sorted(detections, key=lambda x: x['confidence'], reverse=True)\n    final_detections = []\n    \n    def distance_3d(d1, d2):\n        return np.sqrt((d1['z'] - d2['z'])**2 +  (d1['y'] - d2['y'])**2 +  (d1['x'] - d2['x'])**2)\n    \n    # Distance threshold based on box size\n    box_size = 24\n    distance_threshold = box_size * iou_threshold\n    \n    while detections:\n        best_detection = detections.pop(0)\n        final_detections.append(best_detection)\n        detections = [d for d in detections if distance_3d(d, best_detection) > distance_threshold]\n    \n    return final_detections","metadata":{"_uuid":"b7f4d672-2ad7-48ad-a132-49282462446f","_cell_guid":"9714726d-04f1-4eda-90bf-5cc51e385b78","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-29T14:30:32.442312Z","iopub.execute_input":"2026-01-29T14:30:32.442524Z","iopub.status.idle":"2026-01-29T14:30:32.453531Z","shell.execute_reply.started":"2026-01-29T14:30:32.442503Z","shell.execute_reply":"2026-01-29T14:30:32.452955Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def process_tomogram(tomo_id, model, box_annotator, label_annotator):\n    \"\"\"Process a single tomogram and return the most confident motor detection\"\"\"\n    print(f\"Processing tomogram -> {tomo_id}\")\n\n    # Get all slice files\n    tomo_dir = os.path.join(test_dir, tomo_id)\n    slice_files = sorted([f for f in os.listdir(tomo_dir) if f.endswith('.jpg')])\n\n    # Calculate middle slice index and number for visualization\n    middle_slice_idx = len(slice_files) // 2\n    middle_slice_num = int(slice_files[middle_slice_idx].split('_')[1].split('.')[0])\n\n    # All slices will be processed now\n\n    all_detections = []\n\n    # Process each slice\n    for slice_file in tqdm(slice_files, desc=f\"Slices for {tomo_id}\", leave=False):\n        slice_path = os.path.join(tomo_dir, slice_file)\n        slice_num = int(slice_file.split('_')[1].split('.')[0])\n\n        # Load and normalize image\n        image = load_and_normalize_image(slice_path)\n\n        # Run inference (Local RFDETR)\n        # verbose=False supresses per-image logs\n        results = model.predict(images=image, conf=CONFIDENCE_THRESHOLD, verbose=False)[0]\n\n        # Load results into supervision\n        # RFDETR typically returns Ultralytics-compatible results\n        detections = sv.Detections.from_ultralytics(results)\n\n        # Visualize if this is the middle slice\n        if slice_num == middle_slice_num:\n            # Prepare labels with class name and confidence\n            labels = [\n                f\"{model.names[class_id]} {confidence:.2f}\"\n                for class_id, confidence in zip(detections.class_id, detections.confidence)\n            ]\n            # Annotate image\n            annotated_frame = box_annotator.annotate(scene=image.copy(), detections=detections)\n            annotated_frame = label_annotator.annotate(scene=annotated_frame, detections=detections, labels=labels)\n\n            # Display image\n            plt.figure(figsize=(10, 10))\n            plt.imshow(annotated_frame)\n            plt.title(f\"Tomogram: {tomo_id}, Middle Slice: {slice_num} (Detections: {len(detections)})\")\n            plt.axis('off')\n            plt.show()\n\n        # Process detections for 3D NMS\n        for i in range(len(detections)):\n            # Get bounding box\n            x1, y1, x2, y2 = detections.xyxy[i]\n\n            # Calculate center\n            all_detections.append({\n                'z': round(slice_num),\n                'y': round((y1 + y2) / 2),\n                'x': round((x1 + x2) / 2),\n                'confidence': float(detections.confidence[i])\n            })\n\n    # Apply 3D NMS\n    final_detections = perform_3d_nms(all_detections, NMS_IOU_THRESHOLD)\n    final_detections.sort(key=lambda x: x['confidence'], reverse=True)\n\n    # Return best detection or NA values\n    if not final_detections:\n        return { \n            'tomo_id': tomo_id, # Added tomo_id here\n            'Motor axis 0': -1,\n            'Motor axis 1': -1,\n            'Motor axis 2': -1\n        }\n\n    best_detection = final_detections[0]\n    return {\n        'tomo_id': tomo_id,\n        'Motor axis 0': round(best_detection['z']),\n        'Motor axis 1': round(best_detection['y']),\n        'Motor axis 2': round(best_detection['x'])\n    }","metadata":{"_uuid":"e6d710aa-7e4a-411d-9634-e358669535b8","_cell_guid":"09d9d187-5f19-4ca9-b791-c76dac4fd5e0","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-29T14:30:32.455324Z","iopub.execute_input":"2026-01-29T14:30:32.455691Z","iopub.status.idle":"2026-01-29T14:30:32.468482Z","shell.execute_reply.started":"2026-01-29T14:30:32.455663Z","shell.execute_reply":"2026-01-29T14:30:32.467791Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\nstart_time = time.time()\n\n# Get list of test tomograms\ntest_tomos = sorted([d for d in os.listdir(test_dir)\n                    if os.path.isdir(os.path.join(test_dir, d))])\ntotal_tomos = len(test_tomos)\n\nprint(f\"Found {total_tomos} tomograms in test directory \\n {test_tomos}\")\n\n# Load Local Model\nprint(f\"Loading local model from: {WEIGHTS_PATH}\")\ntry:\n    model = RFDETRMedium(pretrain_weights=WEIGHTS_PATH)\nexcept Exception as e:\n    if not os.path.exists(WEIGHTS_PATH):\n       print(f\"WARNING: Weights not found at {WEIGHTS_PATH}.\")\n    raise\n\n# Process all tomograms\nresults = []\nmotors_found = 0\n\nfor i, tomo_id in enumerate(test_tomos, 1):\n    try:\n        # Pass annotators to process_tomogram\n        result = process_tomogram(tomo_id, model, box_annotator, label_annotator)\n        results.append(result)\n\n        # Update statistics\n        has_motor = result['Motor axis 0'] != -1\n        if has_motor:\n            motors_found += 1\n            print(f\"Motor found in {tomo_id} at position: \"\n                  f\"z={result['Motor axis 0']}, y={result['Motor axis 1']}, x={result['Motor axis 2']}\")\n        else:\n            print(f\"No motor detected in {tomo_id}\")\n        print(f\"Detection rate: {motors_found}/{len(results)} ({motors_found/len(results)*100:.1f}%)\\n\")\n\n    except Exception as e:\n        print(f\"Error processing {tomo_id}: {e}\")\n        raise\n\n# Create submission dataframe\nsubmission_df = pd.DataFrame(results)\nsubmission_df = submission_df[['tomo_id', 'Motor axis 0', 'Motor axis 1', 'Motor axis 2']]\n\n# Save submission\nsubmission_df.to_csv(submission_path, index=False)\n\nprint(f\"\\nSubmission complete!\")\nif total_tomos > 0:\n    print(f\"Motors detected: {motors_found}/{total_tomos} ({motors_found/total_tomos*100:.1f}%)\")\nprint(f\"Submission saved to: {submission_path}\")\nprint(\"\\nSubmission preview:\")\nprint(submission_df.head())\n\nelapsed = time.time() - start_time\nprint(f\"\\nTotal execution time: {elapsed:.2f} seconds ({elapsed/60:.2f} minutes)\")","metadata":{"_uuid":"99e7a9ea-c68f-4bb5-bc3b-079b765cb0ec","_cell_guid":"3f9ff87a-3941-44eb-b925-5fbfa7540dd4","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-29T14:30:32.469516Z","iopub.execute_input":"2026-01-29T14:30:32.470012Z","iopub.status.idle":"2026-01-29T14:46:02.302171Z","shell.execute_reply.started":"2026-01-29T14:30:32.469989Z","shell.execute_reply":"2026-01-29T14:46:02.301305Z"}},"outputs":[],"execution_count":null}]}