{"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":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# Recently I’ve been working on a pet project for realistic data augmentation of ECG-like and other grid-based “paper” images. \n# I decided to try the same approach here using my small library, MeshAugmentor (https://github.com/pashaalex/mesh_augmentor)\n# This notebook demonstrates how to apply geometric augmentations and also how to map points from the original image \n# into the augmented one — which can be useful for keypoint tasks, segmentation borders, or synthetic dataset generation.\n# If this tool helps you in your experiments — feel free to use it, customize it, or share feedback.\n\n!wget -q https://github.com/pashaalex/mesh_augmentor/releases/download/0.1.0/mesh_augmentor-linux.zip\n!unzip mesh_augmentor-linux.zip","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-26T14:03:33.866484Z","iopub.execute_input":"2025-10-26T14:03:33.866991Z","iopub.status.idle":"2025-10-26T14:03:34.435050Z","shell.execute_reply.started":"2025-10-26T14:03:33.866960Z","shell.execute_reply":"2025-10-26T14:03:34.433476Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport csv\nimport math\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nfrom mesh_augmentor import *\n\nFILE_PATH = \"/kaggle/input/physionet-ecg-image-digitization/train/1006427285/1006427285.csv\"\nOUTPUT_SIZE = 640\nPIXELS_PER_MM = 4\nPAPER_SPEED_MM_S = 25\nSAMPLING_RATE = 1000.0  # Hz\nDRAW_DOTTED=False\nSIGNAL_COLOR=(120, 120, 120)\nSIGNAL_THICKNESS=2\n\nlead_names = [\"I\",\"II\",\"III\",\"aVR\",\"aVL\",\"aVF\",\"V1\",\"V2\",\"V3\",\"V4\",\"V5\",\"V6\"]\n\ndef load_leads(path):\n    result = {name: [] for name in lead_names}\n\n    with open(path, newline=\"\", encoding=\"utf-8\") as f:\n        reader = csv.DictReader(f)\n        for row in reader:\n            for col in result:\n                val = row[col].strip()\n                if val:\n                    result[col].append(float(val))\n    return result\n\ndef draw_signal(img, \n                leads, \n                signal_len,\n                signal_name,                 \n                x0, y0,\n                xscale = PIXELS_PER_MM * PAPER_SPEED_MM_S / SAMPLING_RATE, \n                yscale = PIXELS_PER_MM * 10.0, \n                label_y_offset = 5 * PIXELS_PER_MM # 5mm offset to draw lead name\n):\n    signal = leads[signal_name][:int(signal_len * SAMPLING_RATE)]\n    \n    x_prev, y_prev = x0, y0\n\n    for i in range(len(signal) - 1):\n        x_curr = int(x0 + xscale * i)\n        y_curr = int(y0 - yscale * signal[i + 1])\n        cv2.line(img, (x_prev, y_prev), (x_curr, y_curr), color=SIGNAL_COLOR, thickness=SIGNAL_THICKNESS)\n        x_prev, y_prev = x_curr, y_curr\n\n    cv2.putText(img, \n                signal_name, \n                (x0, y0 + label_y_offset), \n                fontFace = cv2.FONT_HERSHEY_SIMPLEX, \n                fontScale=0.9, \n                color = SIGNAL_COLOR, \n                thickness = SIGNAL_THICKNESS, \n                lineType = cv2.LINE_AA)\n    \n    return x_curr\n\ndef draw_ecg(\n        leads,\n        img_width=PIXELS_PER_MM * 270,\n        img_height=PIXELS_PER_MM * 210,\n        grid_line_thickness=1,\n        draw_dotted=False,\n        background_color=(244, 244, 244),\n        mm_line_color=(252, 203, 202)):\n    img = np.full((img_height, img_width, 3), background_color, dtype=np.uint8)\n    if draw_dotted == 1:\n        for x in range(0, img_width, PIXELS_PER_MM):\n            for y in range(0, img_height, PIXELS_PER_MM):\n                cv2.rectangle(img, (x, y), (x + grid_line_thickness, y + grid_line_thickness), thickness=-1, color=mm_line_color)\n    else:\n        for x in range(0, img_width, PIXELS_PER_MM):             \n            cv2.line(img, (x, 0), (x, img_height), color=mm_line_color, thickness=grid_line_thickness)\n        for y in range(0, img_height, PIXELS_PER_MM):\n            cv2.line(img, (0, y), (img_width, y), color=mm_line_color, thickness=grid_line_thickness)\n    # Big lines (each 5mm)\n    for x in range(0, img_width, PIXELS_PER_MM * 5):\n        cv2.line(img, (x, 0), (x, img_height), color=mm_line_color, thickness=grid_line_thickness * 2)\n    for y in range(0, img_height, PIXELS_PER_MM * 5):\n        cv2.line(img, (0, y), (img_width, y), color=mm_line_color, thickness=grid_line_thickness * 2)\n\n    x_next = draw_signal(img, leads, 2.5, 'I', 15 * PIXELS_PER_MM, 90 * PIXELS_PER_MM)\n    x_next = draw_signal(img, leads, 2.5, 'aVR', x_next, 90 * PIXELS_PER_MM)\n    x_next = draw_signal(img, leads, 2.5, 'V1', x_next, 90 * PIXELS_PER_MM)\n    x_next = draw_signal(img, leads, 2.5, 'V4', x_next, 90 * PIXELS_PER_MM)\n\n    x_next = draw_signal(img, leads, 2.5, 'II', 15 * PIXELS_PER_MM, 125 * PIXELS_PER_MM)\n    x_next = draw_signal(img, leads, 2.5, 'aVL', x_next, 125 * PIXELS_PER_MM)\n    x_next = draw_signal(img, leads, 2.5, 'V2', x_next, 125 * PIXELS_PER_MM)\n    x_next = draw_signal(img, leads, 2.5, 'V5', x_next, 125 * PIXELS_PER_MM)\n\n    x_next = draw_signal(img, leads, 2.5, 'III', 15 * PIXELS_PER_MM, 160 * PIXELS_PER_MM)\n    x_next = draw_signal(img, leads, 2.5, 'aVF', x_next, 160 * PIXELS_PER_MM)\n    x_next = draw_signal(img, leads, 2.5, 'V3', x_next, 160 * PIXELS_PER_MM)\n    x_next = draw_signal(img, leads, 2.5, 'V6', x_next, 160 * PIXELS_PER_MM)\n\n    draw_signal(img, leads, 10.0, 'II', 15 * PIXELS_PER_MM, 195 * PIXELS_PER_MM)\n\n    return img\n\nleads = load_leads(FILE_PATH)\necg = draw_ecg(leads)\n\nh, w, _ = ecg.shape\nwcnt = w // 27\nhcnt = h // 21\n\n# Configure optics/lighting/distortion/etc.\noptics = Optics(F=35.0, L=66.7, R=8.0)\ndistortion = Distortion(use=True, k1=-0.5)\nlighting = Lighting(\n    use=True,\n    x=40.0,\n    y=-40.0,\n    z=10.0,\n    intensity=1.5,\n    diameter=170.0,\n    light_mix_koef=0.9 \n)\n\nbg_shadow = BackgroundShadow(use=True, bg_z=150, bottom_shadow_koef=0.5)\npose = CameraPose(tilt_x_rad=math.radians(2.0))\n\n# Create mesh\nmesh = MeshAugmentor(\n    input_width=w, input_height=h,\n    grid_w=wcnt, grid_h=hcnt,\n    optics=optics, \n    distortion=distortion, \n    lighting=lighting,\n    bg_shadow=bg_shadow, \n    pose=pose\n)\n\napply_crumple_with_creases(\n    mesh,\n    K=20,\n    angle_deg_range=(8.0, 24.0),\n    band_px=90,\n    falloff_sigma=18.0,\n    z_jitter_std=0.05,\n    z_scale=0.4\n)\n\n# cylinder\nstride = wcnt + 1\nfor index, point in enumerate(mesh.points):\n    xc = index % stride\n    yc = index // stride\n    point.z -= 20 * math.sin(3.14 * xc / wcnt)\n\nmesh.rotate_z(math.radians(5))\nmesh.rotate_x(math.radians(2))\nmesh.fit_best_geometric(OUTPUT_SIZE, OUTPUT_SIZE, 0.9)\n\n# Render (RGB + optional alpha/mask/uv)\nouts = mesh.render(\n    input_image=ecg,\n    out_size=(OUTPUT_SIZE, OUTPUT_SIZE),\n    background = np.full((224, 224, 3), (255, 255, 255), dtype=np.uint8),\n    attachments=(\"rgb\", \"mask\", \"uv\"),\n)\n\n# Reproject start point from source image coordinates \n# into augmented image coordinates\nx_point, y_point = mesh.reproject_point(15 * PIXELS_PER_MM, \n                                  90 * PIXELS_PER_MM,\n                                  OUTPUT_SIZE,\n                                  OUTPUT_SIZE)\n\ncv2.circle(outs.rgb, (int(x_point), int(y_point)), 8, (255, 0, 0), thickness=-1)\n\nplt.imshow(outs.rgb)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-26T14:04:23.732983Z","iopub.execute_input":"2025-10-26T14:04:23.733334Z","iopub.status.idle":"2025-10-26T14:04:25.479504Z","shell.execute_reply.started":"2025-10-26T14:04:23.733290Z","shell.execute_reply":"2025-10-26T14:04:25.478284Z"}},"outputs":[],"execution_count":null}]}