{"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":"gpu","dataSources":[{"sourceId":97984,"databundleVersionId":14096757,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\n#for dirname, _, filenames in os.walk('/kaggle/input'):\n #   for filename in filenames:\n  #      print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:11:07.353132Z","iopub.execute_input":"2026-01-10T17:11:07.353290Z","iopub.status.idle":"2026-01-10T17:11:09.031461Z","shell.execute_reply.started":"2026-01-10T17:11:07.353275Z","shell.execute_reply":"2026-01-10T17:11:09.030767Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Zusammenfassung","metadata":{}},{"cell_type":"markdown","source":"Bilder (12 pro base_id)\n   ↓\nRaster erkennen & entfernen (UNet)   ✅\n   ↓\n12 Leads im Bild lokalisieren\n   ↓\npro Lead: Kurve als Pixelpfad extrahieren\n   ↓\nPixel → (Zeit, mV) umrechnen\n   ↓\nZeitreihe resamplen (fs)\n   ↓\nalle Leads zusammenführen\n   ↓\nCSV im Submission-Format","metadata":{}},{"cell_type":"code","source":"from PIL import Image, ImageDraw\nimport random\nfrom pathlib import Path\n\n# Basisverzeichnis für die generierten Trainingsdaten anlegen\nbase = Path(\"/kaggle/working/ekg_data\")\n(base / \"images\").mkdir(parents=True, exist_ok=True)\n(base / \"masks\").mkdir(parents=True, exist_ok=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:11:09.033089Z","iopub.execute_input":"2026-01-10T17:11:09.033492Z","iopub.status.idle":"2026-01-10T17:11:09.048324Z","shell.execute_reply.started":"2026-01-10T17:11:09.033471Z","shell.execute_reply":"2026-01-10T17:11:09.047659Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Raster-Masken: Trainingsbilder generieren","metadata":{}},{"cell_type":"code","source":"# Erzeugen eines synthetischen EKG-Rasters\n\ndef generate_grid_image(\n    size=512,\n    small_step=10,       # entspricht z.B. 1-mm-Raster, anpassbar\n    big_step=50,         # 5-mm-Raster\n    line_width_small=1,\n    line_width_big=2,\n    noise_std=5\n):\n    \"\"\"\n    Erzeugt ein synthetisches EKG-Rasterbild und die dazugehörige Maske.\n    \n    Rückgabe:\n        img  = RGB-Bild mit Raster\n        mask = SW-Bild (0=Hintergrund, 255=Rasterlinien)\n    \"\"\"\n    \n    # Leere weiße Basisbilder\n    img = Image.new(\"RGB\", (size, size), \"white\")\n    mask = Image.new(\"L\", (size, size), 0)\n\n    draw_img = ImageDraw.Draw(img)\n    draw_mask = ImageDraw.Draw(mask)\n\n    # Variation der Rasterfarben (schwarz/grau oder rosa/rot)\n    p_bw = 0.5  # Wahrscheinlichkeit für Schwarz-Weiß Raster\n\n    if random.random() < p_bw:\n        # Graustufen-Raster für Schwarz-Weiß EKGs\n        # zufälliger Grauton zwischen dunkelgrau und fast schwarz\n        gray_small = random.randint(50, 100)\n        gray_big   = random.randint(30, 70)\n        color_small = (gray_small, gray_small, gray_small)\n        color_big   = (gray_big, gray_big, gray_big)\n            \n    else:\n        # Rötliche Rasterfarben für Farb-EKGs\n        color_small = (\n            255,\n            random.randint(150, 180),\n            random.randint(150, 180)\n        )\n        color_big = (\n            255,\n            random.randint(80, 120),\n            random.randint(80, 120)\n        )\n\n    # Rasterlinien zeichnen\n    for step in range(0, size, small_step):\n\n        # Dickere Linien für große Kästchen\n        if step % big_step == 0:\n            width = line_width_big\n            color = color_big\n        else:\n            width = line_width_small\n            color = color_small\n\n        # Vertikale Linien\n        draw_img.line((step, 0, step, size), fill=color, width=width)\n        draw_mask.line((step, 0, step, size), fill=255, width=width)\n\n        # Horizontale Linien\n        draw_img.line((0, step, size, step), fill=color, width=width)\n        draw_mask.line((0, step, size, step), fill=255, width=width)\n\n    # Leichtes Bildrauschen simulieren (als ob gescannt oder fotografiert)\n    arr = np.array(img, dtype=np.int32)\n    noise = np.random.normal(0, noise_std, arr.shape).astype(np.int32)\n    arr = np.clip(arr + noise, 0, 255).astype(np.uint8)\n\n    img = Image.fromarray(arr)\n\n    return img, mask\n\nprint(\"Raster-Generierungsfunktion bereit.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:11:22.746531Z","iopub.execute_input":"2026-01-10T17:11:22.747102Z","iopub.status.idle":"2026-01-10T17:11:22.755772Z","shell.execute_reply.started":"2026-01-10T17:11:22.747075Z","shell.execute_reply":"2026-01-10T17:11:22.754952Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Bilder mit Rahmen / Hintergrund erstellen?","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T11:45:59.361374Z","iopub.execute_input":"2026-01-02T11:45:59.361613Z","iopub.status.idle":"2026-01-02T11:45:59.364684Z","shell.execute_reply.started":"2026-01-02T11:45:59.361598Z","shell.execute_reply":"2026-01-02T11:45:59.364060Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def generate_grid_image_with_context(\n    grid_size=512,\n    canvas_size=800,      # echtes Bild ist größer\n    small_step=10,\n    big_step=50,\n    noise_std=5\n):\n    \"\"\"\n    Erzeugt:\n      - ein größeres Bild mit Hintergrund + EKG-Raster\n      - eine Maske, die NUR das Raster markiert\n    \"\"\"\n\n    # 1️⃣ Hintergrund erzeugen (z.B. grauer Monitor oder Papier)\n    bg_color = random.randint(150, 220)   # hellgrau\n    canvas = Image.new(\"RGB\", (canvas_size, canvas_size), (bg_color, bg_color, bg_color))\n    mask_full = Image.new(\"L\", (canvas_size, canvas_size), 0)\n\n    # 2️⃣ Dein bisheriges Raster erzeugen (kleiner)\n    grid_img, grid_mask = generate_grid_image(\n        size=grid_size,\n        small_step=small_step,\n        big_step=big_step,\n        noise_std=noise_std\n    )\n\n    # 3️⃣ Zufällige Position bestimmen\n    max_off = canvas_size - grid_size\n    offset_x = random.randint(int(max_off*0.2), int(max_off*0.8))\n    offset_y = random.randint(int(max_off*0.2), int(max_off*0.8))\n\n    # 4️⃣ Raster auf Hintergrund kleben\n    canvas.paste(grid_img, (offset_x, offset_y))\n\n    # 5️⃣ Maske passend einkopieren\n    mask_full.paste(grid_mask, (offset_x, offset_y))\n\n    return canvas, mask_full\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:11:29.656852Z","iopub.execute_input":"2026-01-10T17:11:29.657147Z","iopub.status.idle":"2026-01-10T17:11:29.663211Z","shell.execute_reply.started":"2026-01-10T17:11:29.657123Z","shell.execute_reply":"2026-01-10T17:11:29.662433Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Vorschau","metadata":{}},{"cell_type":"code","source":"# Vorschau generieren\n\nimg, mask = generate_grid_image_with_context()\n\ndisplay(img)\ndisplay(mask)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:11:34.318896Z","iopub.execute_input":"2026-01-10T17:11:34.319365Z","iopub.status.idle":"2026-01-10T17:11:34.472346Z","shell.execute_reply.started":"2026-01-10T17:11:34.319343Z","shell.execute_reply":"2026-01-10T17:11:34.471786Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Automatisch Trainingsdaten erzeugen","metadata":{}},{"cell_type":"code","source":"num_samples = 300   # Anzahl der Trainingsbilder (z. B. 1000)\n\nfor i in range(num_samples):\n    img, mask = generate_grid_image_with_context()\n\n    img.save(base / \"images\" / f\"grid_{i:04d}.png\")\n    mask.save(base / \"masks\" / f\"grid_{i:04d}.png\")\n\nprint(f\"{num_samples} synthetische Rasterbilder und Masken erzeugt!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:11:41.262134Z","iopub.execute_input":"2026-01-10T17:11:41.262755Z","iopub.status.idle":"2026-01-10T17:12:28.651838Z","shell.execute_reply.started":"2026-01-10T17:11:41.262731Z","shell.execute_reply":"2026-01-10T17:12:28.651174Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"- Bilder zeigen synthetische EKG-Raster\n- Masken sind perfekte Ground-Truth-Labels für die Segmentierung\n- Bereit für DataBlock + UNet","metadata":{}},{"cell_type":"code","source":"# Funktion zur Prüfung und Konvertierung einer Maske\n\nimage_folder = Path(\"/kaggle/working/ekg_data/images\")\nmask_folder  = Path(\"/kaggle/working/ekg_data/masks\")\n\ndef check_and_fix_mask(mask_file, img_file):\n    mask = Image.open(mask_file).convert(\"L\")  # 1-Kanal\n    arr = np.array(mask)\n    \n    # Alle Pixel > 0 auf 1 setzen\n    arr = (arr > 0).astype(np.uint8)\n    \n    # Prüfen: gleiche Größe wie das Bild\n    img = Image.open(img_file)\n    if arr.shape != (img.height, img.width):\n        arr = np.array(Image.fromarray(arr).resize((img.width, img.height)))\n        arr = (arr > 0).astype(np.uint8)\n    \n    # Maske zurückspeichern\n    Image.fromarray(arr).save(mask_file)\n    \n    # Validierung\n    unique = np.unique(arr)\n    if not set(unique).issubset({0,1}):\n        raise ValueError(f\"Maskenwerte außer 0/1 in {mask_file}\")\n    if arr.shape != (img.height, img.width):\n        raise ValueError(f\"Maskengröße stimmt nicht mit Bild überein: {mask_file}\")\n\n# Alle Masken prüfen\nfor img_file in image_folder.glob(\"*.png\"):\n    mask_file = mask_folder / img_file.name\n    if not mask_file.exists():\n        raise FileNotFoundError(f\"Maskendatei fehlt für {img_file.name}\")\n    check_and_fix_mask(mask_file, img_file)\n\nprint(\"✅ Alle Masken sind jetzt 1-Kanal, Werte 0/1 und passen zu den Bildern.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:12:36.900064Z","iopub.execute_input":"2026-01-10T17:12:36.900765Z","iopub.status.idle":"2026-01-10T17:12:41.612229Z","shell.execute_reply.started":"2026-01-10T17:12:36.900737Z","shell.execute_reply":"2026-01-10T17:12:41.611583Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## DataLoaders für fast.ai vorbereiten","metadata":{}},{"cell_type":"code","source":"data_path = \"/kaggle/working/ekg_data\"\n\nprint(os.listdir(data_path))\n\npath = Path(\"/kaggle/working/ekg_data\")\n\nimages_path = f\"{data_path}/images\"\nmasks_path = f\"{data_path}/masks\"\n\nprint(\"Bilder:\", len(os.listdir(images_path)))\nprint(\"Masken:\", len(os.listdir(masks_path)))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:12:51.137276Z","iopub.execute_input":"2026-01-10T17:12:51.137814Z","iopub.status.idle":"2026-01-10T17:12:51.143347Z","shell.execute_reply.started":"2026-01-10T17:12:51.137789Z","shell.execute_reply":"2026-01-10T17:12:51.142653Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from fastai.vision.all import *","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:12:54.304023Z","iopub.execute_input":"2026-01-10T17:12:54.304264Z","iopub.status.idle":"2026-01-10T17:13:06.991818Z","shell.execute_reply.started":"2026-01-10T17:12:54.304247Z","shell.execute_reply":"2026-01-10T17:13:06.991024Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# preparing Datablocks\ndef get_mask_dblock(fn):\n    return masks_path/fn.name\n\ndblock = DataBlock(\n    blocks=(ImageBlock, MaskBlock(codes=[0,1])),\n    get_items=get_image_files,\n    get_y=get_mask_dblock,\n    splitter=RandomSplitter(),\n    item_tfms=Resize(512),\n    batch_tfms=aug_transforms()\n)\n\ndls1 = dblock.dataloaders(path, bs=4)\n\ndls1.show_batch(max_n=4)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T15:21:20.297067Z","iopub.execute_input":"2026-01-05T15:21:20.297803Z","iopub.status.idle":"2026-01-05T15:21:33.956653Z","shell.execute_reply.started":"2026-01-05T15:21:20.297778Z","shell.execute_reply":"2026-01-05T15:21:33.955867Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# using SegmentationLoaders\n\ndata_path = \"/kaggle/working/ekg_data\"\n\npath = Path(\"/kaggle/working/ekg_data\")\n\nimages_path = path / \"images\" #f\"{data_path}/images\"\nmasks_path = path / \"masks\" # f\"{data_path}/masks\"\n\n# Codes für Masken\ncodes = ['background','grid']\n\ndef label_func(fn):\n    return masks_path/fn.name #f'{fn.name}'\n\ndls2 = SegmentationDataLoaders.from_label_func(\n    path,\n    fnames=get_image_files(images_path),\n    label_func=label_func,\n    codes=codes,\n    item_tfms=Resize(512),\n    batch_tfms=aug_transforms(),\n    bs=4\n)\n\ndls2.show_batch(max_n=6)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:13:15.621178Z","iopub.execute_input":"2026-01-10T17:13:15.621465Z","iopub.status.idle":"2026-01-10T17:13:17.107440Z","shell.execute_reply.started":"2026-01-10T17:13:15.621443Z","shell.execute_reply":"2026-01-10T17:13:17.106662Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Eventuell Rotation schon in die Trainingsbilder einbauen? Vielleicht hilft das?","metadata":{}},{"cell_type":"markdown","source":"## Modell trainieren (Raster-Erstellung)","metadata":{}},{"cell_type":"code","source":"learn = unet_learner(dls2, resnet34)\nlearn.to_fp16()   # kleinere Zahlen 16 statt 32 Bit => halbiert Speicherverbrauch, meist trotzdem ausreichend\nlearn.fine_tune(4)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:13:34.699058Z","iopub.execute_input":"2026-01-10T17:13:34.699429Z","iopub.status.idle":"2026-01-10T17:16:19.827990Z","shell.execute_reply.started":"2026-01-10T17:13:34.699403Z","shell.execute_reply":"2026-01-10T17:16:19.827099Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Visualisierung","metadata":{}},{"cell_type":"code","source":"learn.show_results(max_n=6, figsize=(7,8))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:16:23.980808Z","iopub.execute_input":"2026-01-10T17:16:23.981116Z","iopub.status.idle":"2026-01-10T17:16:25.061590Z","shell.execute_reply.started":"2026-01-10T17:16:23.981093Z","shell.execute_reply":"2026-01-10T17:16:25.060950Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"interp = SegmentationInterpretation.from_learner(learn)\nplt.figure(figsize=(12,8))      # größer, z.B. 12x8 Zoll\ninterp.plot_top_losses(k=2)\nplt.show()\n# -> napari ausprobieren","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:20:27.882230Z","iopub.execute_input":"2026-01-10T17:20:27.882896Z","iopub.status.idle":"2026-01-10T17:20:31.124656Z","shell.execute_reply.started":"2026-01-10T17:20:27.882863Z","shell.execute_reply":"2026-01-10T17:20:31.123933Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Predictions für die echten Bilder","metadata":{}},{"cell_type":"code","source":"# Bildpfad generieren\n\ndef get_img_path(root, ecg_id, img_number):\n    \"\"\"\n    root: Basisordner, z.B. Path(\"/kaggle/input/.../train\")\n    ecg_id: die 10-stellige ID (Ordnername)\n    img_number: 1–12\n    \"\"\"\n    folder = root / ecg_id\n    img_name = f\"{ecg_id}-{img_number:04d}.png\"\n    return folder / img_name\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:21:30.015427Z","iopub.execute_input":"2026-01-10T17:21:30.015874Z","iopub.status.idle":"2026-01-10T17:21:30.021111Z","shell.execute_reply.started":"2026-01-10T17:21:30.015836Z","shell.execute_reply":"2026-01-10T17:21:30.020367Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Maske vorhersagen und speichern\n\n\ndef predict_mask(learn, img_path, save_path=None):\n    # Originalbildgröße holen\n    img = Image.open(img_path)\n    W, H = img.size\n\n    # Prediction\n    pred_mask, _, _ = learn.predict(img_path)\n\n    # -> numpy (0/1)\n    mask = pred_mask.numpy().astype(np.uint8)\n\n    # Maske auf Originalgröße bringen\n    mask = cv2.resize(\n        mask,\n        (W, H),\n        interpolation=cv2.INTER_NEAREST\n    )\n\n    # Optional speichern (weiß/schwarz)\n    if save_path:\n        Image.fromarray(mask * 255).save(save_path)\n\n    return mask\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:21:33.934865Z","iopub.execute_input":"2026-01-10T17:21:33.935599Z","iopub.status.idle":"2026-01-10T17:21:33.940250Z","shell.execute_reply.started":"2026-01-10T17:21:33.935574Z","shell.execute_reply":"2026-01-10T17:21:33.939570Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Halbtransparentes Overlay erzeugen\n\nimport cv2\nimport numpy as np\n\ndef create_overlay(img_path, mask, alpha=0.4, save_path=None):\n    img = cv2.imread(str(img_path))\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    \n    mask_bin = (mask > 0)\n    overlay = img.copy()\n    red = np.array([255, 0, 0], dtype=np.uint8)\n    \n    overlay[mask_bin] = (\n        alpha * red + (1 - alpha) * overlay[mask_bin]\n    ).astype(np.uint8)\n    \n    if save_path:\n        cv2.imwrite(\n            str(save_path),\n            cv2.cvtColor(overlay, cv2.COLOR_RGB2BGR)\n        )\n    \n    return overlay","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:21:37.168566Z","iopub.execute_input":"2026-01-10T17:21:37.168890Z","iopub.status.idle":"2026-01-10T17:21:37.360815Z","shell.execute_reply.started":"2026-01-10T17:21:37.168866Z","shell.execute_reply":"2026-01-10T17:21:37.360009Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Raster entfernen (Inpainting)\n\ndef remove_raster(img_path, mask, save_path=None):\n    img = cv2.imread(str(img_path))\n    mask_gray = (mask > 0).astype(np.uint8) * 255\n    \n    cleaned = cv2.inpaint(img, mask_gray, 3, cv2.INPAINT_TELEA)\n    \n    if save_path:\n        cv2.imwrite(str(save_path), cleaned)\n    \n    return cleaned\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:21:40.102705Z","iopub.execute_input":"2026-01-10T17:21:40.103407Z","iopub.status.idle":"2026-01-10T17:21:40.107329Z","shell.execute_reply.started":"2026-01-10T17:21:40.103382Z","shell.execute_reply":"2026-01-10T17:21:40.106737Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Ergebnisse speichern (dynamische Dateinamen)\n\ndef make_output_paths(output_root, ecg_id, img_number):\n    out_dir = output_root\n    out_dir.mkdir(exist_ok=True)\n    out_dir = output_root / ecg_id\n    out_dir.mkdir(exist_ok=True)\n    \n    return {\n        'original' : input_root/f\"{ecg_id}/{ecg_id}-{img_number:04d}.png\",\n        \"mask\": out_dir / f\"mask_{img_number:04d}.png\",\n        \"overlay\": out_dir / f\"overlay_{img_number:04d}.png\",\n        \"clean\": out_dir / f\"clean_{img_number:04d}.png\",\n        #\"folder\": out_dir\n    }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:21:42.254525Z","iopub.execute_input":"2026-01-10T17:21:42.254869Z","iopub.status.idle":"2026-01-10T17:21:42.259910Z","shell.execute_reply.started":"2026-01-10T17:21:42.254838Z","shell.execute_reply":"2026-01-10T17:21:42.258965Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# EIN Bild komplett verarbeiten\n\ndef process_single_image(\n    learn,\n    input_root,\n    output_root,\n    ecg_id,\n    img_number,\n    alpha=0.4\n):\n    img_path = get_img_path(input_root, ecg_id, img_number)\n    assert img_path.exists(), f\"Bild nicht gefunden: {img_path}\"\n\n    paths = make_output_paths(output_root, ecg_id, img_number)\n    \n    # Maske\n    mask = predict_mask(learn, img_path, save_path=paths[\"mask\"])\n    \n    # Overlay\n    overlay = create_overlay(img_path, mask, alpha=alpha, save_path=paths[\"overlay\"])\n    \n    # Raster entfernen\n    cleaned = remove_raster(img_path, mask, save_path=paths[\"clean\"])\n    \n    return paths\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:21:46.564792Z","iopub.execute_input":"2026-01-10T17:21:46.565094Z","iopub.status.idle":"2026-01-10T17:21:46.570040Z","shell.execute_reply.started":"2026-01-10T17:21:46.565072Z","shell.execute_reply":"2026-01-10T17:21:46.569352Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Ein Bild verarbeiten","metadata":{}},{"cell_type":"code","source":"# EIN Bild verarbeiten\n\ninput_root = Path(\"/kaggle/input/physionet-ecg-image-digitization/train/\")\noutput_root = Path(\"/kaggle/working/predictions\")\necg_id = \"10140238\"\nimg_number= 6\n\nres = process_single_image(\n    learn=learn,\n    input_root=input_root,\n    output_root=output_root,\n    ecg_id=ecg_id,\n    img_number=img_number\n)\n\nres","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:21:52.634962Z","iopub.execute_input":"2026-01-10T17:21:52.635493Z","iopub.status.idle":"2026-01-10T17:21:57.571069Z","shell.execute_reply.started":"2026-01-10T17:21:52.635468Z","shell.execute_reply":"2026-01-10T17:21:57.570197Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"imgs = res","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:22:01.311499Z","iopub.execute_input":"2026-01-10T17:22:01.311822Z","iopub.status.idle":"2026-01-10T17:22:01.315687Z","shell.execute_reply.started":"2026-01-10T17:22:01.311798Z","shell.execute_reply":"2026-01-10T17:22:01.314947Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load(p):\n    return Image.open(p).convert('RGBA') if p.is_file() else Image.new('RGBA',(256,256),(240,240,240,255))\n\nkeys = list(imgs.keys())\npics = [load(imgs[k]) for k in keys]\n\nfig, axes = plt.subplots(2, 2, figsize=(8,8))\naxes_flat = axes.flatten()            # -> Liste von Axes-Objekten\nfor ax, im, title in zip(axes_flat, pics, keys):\n    ax.imshow(im)\n    ax.set_title(title)\n    ax.axis('off')\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:22:04.564311Z","iopub.execute_input":"2026-01-10T17:22:04.565144Z","iopub.status.idle":"2026-01-10T17:22:09.868290Z","shell.execute_reply.started":"2026-01-10T17:22:04.565114Z","shell.execute_reply":"2026-01-10T17:22:09.867304Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"p = output_root / f\"clean_{img_number:04d}.png\"\nimg = Image.open(p).convert('RGB')\n\n# Bildgröße in Pixeln\nw, h = img.size\n\n# figsize in Zoll: Pixel / dpi\ndpi = 100  # anpassen; Jupyter/Kaggle standardmäßig ~100\nfigsize = (w / dpi, h / dpi)\n\nfig = plt.figure(figsize=figsize, dpi=dpi)\nax = fig.add_subplot(111)\nax.imshow(img)\nax.axis('off')\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T17:22:09.869385Z","iopub.execute_input":"2026-01-10T17:22:09.869597Z","iopub.status.idle":"2026-01-10T17:22:09.913774Z","shell.execute_reply.started":"2026-01-10T17:22:09.869579Z","shell.execute_reply":"2026-01-10T17:22:09.912826Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Kurzer Test","metadata":{}},{"cell_type":"code","source":"tst_img = Path('/kaggle/input/physionet-ecg-image-digitization/train/1124135100/1124135100-0010.png')\nimg = cv2.imread(tst_img, cv2.IMREAD_GRAYSCALE)\nsmall = cv2.resize(img, (768, 768), interpolation=cv2.INTER_AREA)\n\nplt.figure(figsize=(14,6))\nplt.subplot(1,2,1); plt.title(\"Original\"); plt.imshow(img, cmap=\"gray\")\nplt.subplot(1,2,2); plt.title(\"Resize 768\"); plt.imshow(small, cmap=\"gray\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T11:48:36.288574Z","iopub.status.idle":"2026-01-02T11:48:36.288934Z","shell.execute_reply.started":"2026-01-02T11:48:36.288714Z","shell.execute_reply":"2026-01-02T11:48:36.288732Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Alle 12 Bilder eines Ordners verarbeiten\n\ndef process_all_images_for_id(learn, input_root, output_root, ecg_id):\n    for i in range(1, 13):\n        try:\n            process_single_image(learn, input_root, output_root, ecg_id, i)\n        except AssertionError:\n            print(f\"Bild fehlt: {ecg_id}-{i:04d}.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T11:48:36.289963Z","iopub.status.idle":"2026-01-02T11:48:36.290565Z","shell.execute_reply.started":"2026-01-02T11:48:36.290433Z","shell.execute_reply":"2026-01-02T11:48:36.290450Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Alle Ordner automatisch durchlaufen\n\ndef process_entire_dataset(learn, input_root, output_root):\n    for folder in input_root.iterdir():\n        if folder.is_dir():\n            ecg_id = folder.name\n            print(\"Verarbeite:\", ecg_id)\n            process_all_images_for_id(learn, input_root, output_root, ecg_id)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T11:48:36.291287Z","iopub.status.idle":"2026-01-02T11:48:36.291498Z","shell.execute_reply.started":"2026-01-02T11:48:36.291397Z","shell.execute_reply":"2026-01-02T11:48:36.291406Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Prozess \"alle Ordner durchlaufen\" anstoßen\n# process_entire_dataset(learn, input_root, output_root)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T11:48:36.292881Z","iopub.status.idle":"2026-01-02T11:48:36.293166Z","shell.execute_reply.started":"2026-01-02T11:48:36.293044Z","shell.execute_reply":"2026-01-02T11:48:36.293057Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Auf dem Papier:","metadata":{}},{"cell_type":"code","source":"I     aVR    V1     V4\nII    aVL    V2     V5\nIII   aVF    V3     V6","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T11:48:36.293958Z","iopub.status.idle":"2026-01-02T11:48:36.294169Z","shell.execute_reply.started":"2026-01-02T11:48:36.294066Z","shell.execute_reply":"2026-01-02T11:48:36.294075Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"In der CSV:\n\njede dieser Kurven wird \n- separat\n- mit eigener Zeitachse\n- aber gleicher Samplingrate fs\ngespeichert\n\n**Sonderfall: explizit berücksichtigen**\n\n- Lead II: 10 Sekunden\n\n- alle anderen: 2.5 Sekunden","metadata":{}},{"cell_type":"markdown","source":"Bild wird vertikal in drei gleich hohe Zonen geteilt, wir arbeiten nur mit Prozenten der Bildgröße, nicht expliziten Angaben wie \"Lead I liegt bei y = 200 bis 400. (Prozentuale ROIs (region of interest) sind überraschend robust.)  \nJede Zone enthält 4 Leads nebeneinander:","metadata":{}},{"cell_type":"markdown","source":"| I   | avR | V1 | V4 |\n|-----|-----|----|----|\n| II  | aVL | V2 | V5 |\n| III | aVF | V3 | V6 |","metadata":{}},{"cell_type":"markdown","source":"Das heißt:\n\n- x-Achse ≈ Zeit\n\n- y-Achse ≈ Amplitude\n\n\nBild-y läuft von oben nach unten, \nSpannung läuft von unten nach oben\n\n-> Das drehen wir später um.","metadata":{}},{"cell_type":"markdown","source":"Prinzip \"Edge Detection und folge der Kante“ scheitert bei:\n\n- Rauschen\n- dicken Linien\n- Überlagerungen\n\n-> Connected Components benutzen","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}