{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":97984,"databundleVersionId":14096757,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Detect grid intersections using Harris corner detection.\n\nAutomatically detects if the image is color or grayscale and applies the appropriate method:\n- Color images: Uses amplified A channel from LAB color space to focus on grid structure\n- Grayscale images: Uses Harris corner detection directly on grayscale","metadata":{}},{"cell_type":"code","source":"import cv2\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\nfrom tqdm import tqdm\n\nDATA_PATH = Path(\"/kaggle/input/physionet-ecg-image-digitization\")\nTRAIN_PATH = DATA_PATH / 'train'\n\ntrain = pd.read_csv(DATA_PATH / 'train.csv')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-12T16:20:27.773941Z","iopub.execute_input":"2025-11-12T16:20:27.774246Z","iopub.status.idle":"2025-11-12T16:20:30.500478Z","shell.execute_reply.started":"2025-11-12T16:20:27.774217Z","shell.execute_reply":"2025-11-12T16:20:30.499360Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def detect_grid_intersections(cropped_img, block_size=13, ksize=7, k=0.1,\n                              threshold_min_ratio=0.05, threshold_max_ratio=1.0,\n                              visualize=False):\n    \"\"\"\n    Detect grid intersections using Harris corner detection.\n\n    Automatically detects if the image is color or grayscale and applies the appropriate method:\n    - Color images: Uses amplified A channel from LAB color space to focus on grid structure\n    - Grayscale images: Uses Harris corner detection directly on grayscale\n\n    Parameters:\n    -----------\n    cropped_img : numpy.ndarray\n        Cropped image containing the grid (BGR format)\n    block_size : int, optional\n        Size of neighborhood for Harris corner detection (default: 13)\n    ksize : int, optional\n        Aperture parameter for Sobel derivative (default: 7)\n    k : float, optional\n        Harris detector free parameter (default: 0.1)\n    threshold_min_ratio : float, optional\n        Minimum threshold as ratio of max corner response (default: 0.05)\n    threshold_max_ratio : float, optional\n        Maximum threshold as ratio of max corner response (default: 1.0)\n    visualize : bool, optional\n        If True, display the detection process (default: False)\n\n    Returns:\n    --------\n    centroids : list of tuples\n        List of (x, y) coordinates of detected grid intersections\n    corner_image : numpy.ndarray\n        Binary image showing detected corners\n    detection_method : str\n        Method used: 'color_lab_a' or 'grayscale'\n    \"\"\"\n    # Check if image is grayscale or color\n    is_grayscale = len(cropped_img.shape) == 2 or (\n        len(cropped_img.shape) == 3 and\n        np.allclose(cropped_img[:,:,0], cropped_img[:,:,1]) and\n        np.allclose(cropped_img[:,:,1], cropped_img[:,:,2])\n    )\n\n    if is_grayscale:\n        # Grayscale approach: use Harris on grayscale directly\n        if len(cropped_img.shape) == 3:\n            gray_for_harris = cv2.cvtColor(cropped_img, cv2.COLOR_BGR2GRAY)\n        else:\n            gray_for_harris = cropped_img.copy()\n\n        detection_method = 'grayscale'\n        processing_image = gray_for_harris\n        print(\"Using grayscale Harris corner detection\")\n    else:\n        # Color approach: use amplified A channel from LAB color space\n        cropped_lab = cv2.cvtColor(cropped_img, cv2.COLOR_BGR2LAB)\n        l, a, b_lab = cv2.split(cropped_lab)\n\n        # Normalize and amplify the A channel\n        a_normalized = cv2.normalize(a, None, 0, 255, cv2.NORM_MINMAX)\n\n        # Amplify using contrast stretching\n        a_amplified = np.clip(a_normalized * 1.5, 0, 255).astype(np.uint8)\n\n        detection_method = 'color_lab_a'\n        processing_image = a_amplified\n        print(\"Using amplified LAB A channel for corner detection\")\n\n    # Apply Harris Corner Detection\n    gray_for_harris = np.float32(processing_image)\n    harris_corners = cv2.cornerHarris(gray_for_harris, blockSize=block_size,\n                                      ksize=ksize, k=k)\n\n    # Dilate to mark the corners more clearly\n    harris_corners = cv2.dilate(harris_corners, None)\n\n    # Threshold to get strong corners\n    threshold_max = threshold_max_ratio * harris_corners.max()\n    threshold_min = threshold_min_ratio * harris_corners.max()\n    corner_image = np.zeros_like(processing_image)\n\n    # Mark corners based on thresholds\n    corner_image[(harris_corners > threshold_min) & (harris_corners < threshold_max)] = 255\n\n    # Find contours of detected corners\n    contours_corners, _ = cv2.findContours(corner_image, cv2.RETR_LIST,\n                                          cv2.CHAIN_APPROX_SIMPLE)\n\n    # Extract centroids\n    centroids = []\n    for cnt in contours_corners:\n        mom = cv2.moments(cnt)\n        if mom['m00'] != 0:\n            x = int(mom['m10'] / mom['m00'])\n            y = int(mom['m01'] / mom['m00'])\n            centroids.append((x, y))\n\n    print(f\"Detected {len(centroids)} corner points using {detection_method} method\")\n\n    if visualize:\n        # Visualize the process and detected corners\n        img_with_corners = cropped_img.copy()\n        for (x, y) in centroids:\n            cv2.circle(img_with_corners, (x, y), 5, (0, 0, 255), -1)  # Red filled circle\n\n        fig, axes = plt.subplots(2, 3, figsize=(18, 12))\n\n        # Row 1: Processing steps\n        axes[0, 0].imshow(processing_image, cmap='gray')\n        axes[0, 0].set_title(f'Processing Image\\n({detection_method})')\n        axes[0, 0].axis('off')\n\n        axes[0, 1].imshow(harris_corners, cmap='hot')\n        axes[0, 1].set_title('Harris Corner Response')\n        axes[0, 1].axis('off')\n\n        axes[0, 2].imshow(corner_image, cmap='gray')\n        axes[0, 2].set_title(f'Detected Corners\\n(threshold: {threshold_min:.2f} - {threshold_max:.2f})')\n        axes[0, 2].axis('off')\n\n        # Row 2: Results\n        axes[1, 0].imshow(cv2.cvtColor(cropped_img, cv2.COLOR_BGR2RGB))\n        axes[1, 0].set_title('Original Cropped')\n        axes[1, 0].axis('off')\n\n        axes[1, 1].imshow(cv2.cvtColor(img_with_corners, cv2.COLOR_BGR2RGB))\n        axes[1, 1].set_title(f'Detected {len(centroids)} Corner Points')\n        axes[1, 1].axis('off')\n\n        # Show corners on processing image\n        processing_with_corners = cv2.cvtColor(processing_image, cv2.COLOR_GRAY2BGR)\n        for (x, y) in centroids:\n            cv2.circle(processing_with_corners, (x, y), 3, (0, 0, 255), -1)\n\n        axes[1, 2].imshow(cv2.cvtColor(processing_with_corners, cv2.COLOR_BGR2RGB))\n        axes[1, 2].set_title('Corners on Processing Image')\n        axes[1, 2].axis('off')\n\n        plt.tight_layout()\n        plt.show()\n\n        # Show final result in a larger figure\n        plt.figure(figsize=(20, 14))\n        plt.imshow(cv2.cvtColor(img_with_corners, cv2.COLOR_BGR2RGB))\n        plt.title(f'Grid Intersections Detected: {len(centroids)} ({detection_method})',\n                 fontsize=16)\n        plt.axis('off')\n        plt.tight_layout()\n        plt.show()\n\n    # Print summary\n    print(f\"\\nSummary:\")\n    print(f\"- Number of corner points detected: {len(centroids)}\")\n    print(f\"- Harris threshold range: {threshold_min:.6f} to {threshold_max:.6f}\")\n    print(f\"- Detection method: {detection_method}\")\n    print(f\"- Harris parameters: block_size={block_size}, ksize={ksize}, k={k}\")\n\n    return centroids, corner_image, detection_method","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T16:24:27.546913Z","iopub.execute_input":"2025-11-12T16:24:27.547273Z","iopub.status.idle":"2025-11-12T16:24:27.570431Z","shell.execute_reply.started":"2025-11-12T16:24:27.547244Z","shell.execute_reply":"2025-11-12T16:24:27.569173Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Pick one test image to work with.\ntest_row = train.iloc[42]\ntest_id = str(test_row['id'])\n\n# Choose image type.\n# For info:\nimage_types = {\n    '0001': 'Original color ECG',\n    '0003': 'Printed & scanned in color',\n    '0004': 'Printed & scanned in B&W',\n    '0005': 'Mobile photo of printed',\n    '0006': 'Mobile photo of screen',\n    '0009': 'Stained and soaked',\n    '0010': 'Extensively damaged',\n    '0011': 'Scanned with mold (color)',\n    '0012': 'Scanned with mold (B&W)'\n}\n\nimg_type = '0012'  # Try: '0001', '0009', '0010', '0011', etc.\nimg_path = TRAIN_PATH / test_id / f\"{test_id}-{img_type}.png\"\n\n# Load image.\nimg = cv2.imread(str(img_path))\nprint(f\"Image shape: {img.shape}\")\n\n# Display original.\nplt.figure(figsize=(6, 4))\nplt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))\nplt.title(f\"Original Image: {test_id}-{img_type}\")\nplt.axis('off')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T16:37:44.943804Z","iopub.execute_input":"2025-11-12T16:37:44.944172Z","iopub.status.idle":"2025-11-12T16:37:45.678345Z","shell.execute_reply.started":"2025-11-12T16:37:44.944149Z","shell.execute_reply":"2025-11-12T16:37:45.677320Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"_ = detect_grid_intersections(img, visualize=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T16:37:51.012000Z","iopub.execute_input":"2025-11-12T16:37:51.012357Z","iopub.status.idle":"2025-11-12T16:37:56.816408Z","shell.execute_reply.started":"2025-11-12T16:37:51.012334Z","shell.execute_reply":"2025-11-12T16:37:56.815042Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}