{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.14","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":71549,"databundleVersionId":8561470,"sourceType":"competition"},{"sourceId":8713513,"sourceType":"datasetVersion","datasetId":5227521}],"dockerImageVersionId":30762,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport glob\nimport random\nimport math\n\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport matplotlib.gridspec as gridspec\nfrom tqdm import tqdm\nfrom types import SimpleNamespace\nimport albumentations as A\nimport cv2\nimport pydicom\nimport matplotlib.pyplot as plt\nimport glob\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport timm\nimport pydicom\nimport glob\nfrom IPython.display import display\ndef set_seed(seed=1234):\n    random.seed(seed)\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = False\n    torch.backends.cudnn.benchmark = True","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:47.879011Z","iopub.execute_input":"2024-09-13T12:28:47.879756Z","iopub.status.idle":"2024-09-13T12:28:47.887965Z","shell.execute_reply.started":"2024-09-13T12:28:47.879715Z","shell.execute_reply":"2024-09-13T12:28:47.886806Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 设置全局参数","metadata":{}},{"cell_type":"code","source":"class CFG:\n    EDA = True    #如果想看数据分析的全过程，这里设置成True，否则就侧重运行\n    #检查正方形\n    ZHENG = False\n    \n    TRAIN = True","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:47.889555Z","iopub.execute_input":"2024-09-13T12:28:47.889883Z","iopub.status.idle":"2024-09-13T12:28:47.898072Z","shell.execute_reply.started":"2024-09-13T12:28:47.889833Z","shell.execute_reply":"2024-09-13T12:28:47.897050Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 训练的全流程\n- 准备数据\n- 处理dataset\n- 准备模型\n- 训练\n- 验证","metadata":{}},{"cell_type":"markdown","source":"- 发现的问题bug之一，是使用已经处理好的图像是经过筛选后的切片，实际上可以是根据原图的长度来进行处理相对坐标位置","metadata":{}},{"cell_type":"markdown","source":"# 第一步，单个调试，第二步，封装函数","metadata":{}},{"cell_type":"code","source":"##坐标数据\ntrain_cor = pd.read_csv('/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_label_coordinates.csv')\nprint('###########坐标数据########')\ndisplay(train_cor.head(10))\nprint()\nprint('series与切片方向对应的表格')\ntrain_series = pd.read_csv('/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_series_descriptions.csv')\ndisplay(train_series.head())","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:47.901950Z","iopub.execute_input":"2024-09-13T12:28:47.902215Z","iopub.status.idle":"2024-09-13T12:28:47.991406Z","shell.execute_reply.started":"2024-09-13T12:28:47.902185Z","shell.execute_reply":"2024-09-13T12:28:47.990563Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"#绘制第一个图片的具体文件与坐标信息\nif CFG.EDA:\n    # 读取 DICOM 文件\n    read_pat = '/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_images/4003253/702807833/7.dcm'\n    img = pydicom.dcmread(read_pat)\n    pixel_array = img.pixel_array\n\n    # 获取图像的宽度和高度\n    img_height, img_width = pixel_array.shape\n    print(f'图像宽度: {img_width}, 图像高度: {img_height}')\n\n    # 绝对坐标 (x1, y1, x2, y2, ..., x5, y5)\n    absolute_coords = [322.83, 227.96, 320.57, 295.71, 323.03, 371.81, 335.29, 427.32, 353.41, 483.96]\n\n    # 将绝对坐标转换为相对坐标\n    relative_coords = [(absolute_coords[i] / img_width if i % 2 == 0 else absolute_coords[i] / img_height) for i in range(len(absolute_coords))]\n\n    # 输出相对坐标\n    print('相对坐标:', relative_coords)\n\n    # 将相对坐标转换回绝对坐标用于绘制\n    draw_coords = [(relative_coords[i] * img_width if i % 2 == 0 else relative_coords[i] * img_height) for i in range(len(relative_coords))]\n\n    # 重新组织为 (x, y) 对\n    points = [(draw_coords[i], draw_coords[i+1]) for i in range(0, len(draw_coords), 2)]\n\n    # 可视化图像并绘制坐标点\n    plt.imshow(pixel_array, cmap=plt.cm.gray)\n    plt.title(\"DICOM Image with Points\")\n    plt.axis('off')\n\n    # 绘制点 (使用红色圆圈表示)\n    for point in points:\n        plt.scatter(point[0], point[1], color='red')\n\n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:47.992463Z","iopub.execute_input":"2024-09-13T12:28:47.992741Z","iopub.status.idle":"2024-09-13T12:28:48.298631Z","shell.execute_reply.started":"2024-09-13T12:28:47.992710Z","shell.execute_reply":"2024-09-13T12:28:48.297629Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### 可视化封装成函数","metadata":{}},{"cell_type":"code","source":"###可视化函数\ndef visualize_dicom_with_points(dicom_path, absolute_coords):\n    \"\"\"\n    读取 DICOM 文件并在图像上绘制给定的绝对坐标点。\n    \n    参数:\n    dicom_path (str): DICOM 文件的路径。\n    absolute_coords (list): 绝对坐标 (x1, y1, x2, y2, ...)。\n    \"\"\"\n    # 读取 DICOM 文件\n    img = pydicom.dcmread(dicom_path)\n    pixel_array = img.pixel_array\n\n    # 获取图像的宽度和高度\n    img_height, img_width = pixel_array.shape\n    print(f'图像宽度: {img_width}, 图像高度: {img_height}')\n\n    # 将绝对坐标转换为相对坐标\n    relative_coords = [(absolute_coords[i] / img_width if i % 2 == 0 else absolute_coords[i] / img_height) for i in range(len(absolute_coords))]\n\n    # 输出相对坐标\n    print('相对坐标:', relative_coords)\n\n    # 将相对坐标转换回绝对坐标用于绘制\n    draw_coords = [(relative_coords[i] * img_width if i % 2 == 0 else relative_coords[i] * img_height) for i in range(len(relative_coords))]\n\n    # 重新组织为 (x, y) 对\n    points = [(draw_coords[i], draw_coords[i+1]) for i in range(0, len(draw_coords), 2)]\n\n    # 可视化图像并绘制坐标点\n    plt.imshow(pixel_array, cmap=plt.cm.gray)\n    plt.title(\"DICOM Image with Points\")\n    plt.axis('off')\n\n    # 绘制点 (使用红色圆圈表示)\n    for point in points:\n        plt.scatter(point[0], point[1], color='red')\n\n    plt.show()\n\n# 使用示例\n\nif CFG.EDA:\n    dicom_path = '/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_images/4003253/702807833/7.dcm'\n    absolute_coords = [322.83, 227.96, 320.57, 295.71, 323.03, 371.81, 335.29, 427.32, 353.41, 483.96]\n\n    visualize_dicom_with_points(dicom_path, absolute_coords)\n","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:48.300647Z","iopub.execute_input":"2024-09-13T12:28:48.300965Z","iopub.status.idle":"2024-09-13T12:28:48.593806Z","shell.execute_reply.started":"2024-09-13T12:28:48.300930Z","shell.execute_reply":"2024-09-13T12:28:48.592894Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### 对训练表格做数据分析，考虑该如何生成dataset ","metadata":{}},{"cell_type":"code","source":"# 給训练数据添加切片方向名称\ntrain_merge = pd.merge(train_cor, train_series, on=['study_id', 'series_id'], how='left')\ntrain_merge.shape\ntrain_merge.head(12)","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:48.594978Z","iopub.execute_input":"2024-09-13T12:28:48.595297Z","iopub.status.idle":"2024-09-13T12:28:48.618546Z","shell.execute_reply.started":"2024-09-13T12:28:48.595260Z","shell.execute_reply":"2024-09-13T12:28:48.617614Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"#将三种数据分离出来\ntrain_Sagittal_T2 = train_merge[train_merge['series_description']=='Sagittal T2/STIR'].reset_index(drop=True)\ntrain_Sagittal_T1 = train_merge[train_merge['series_description']=='Sagittal T1'].reset_index(drop=True)\ntrain_Axial_T2 = train_merge[train_merge['series_description']=='Axial T2'].reset_index(drop=True)\nprint('Sagittal_T1')\ndisplay(train_Sagittal_T1.head(12))\nprint()\nprint('Sagittal_T2')\ndisplay(train_Sagittal_T2.head(12))\nprint()\nprint('Axial_T2')\ndisplay(train_Axial_T2.head(12))","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:48.619572Z","iopub.execute_input":"2024-09-13T12:28:48.619843Z","iopub.status.idle":"2024-09-13T12:28:48.689166Z","shell.execute_reply.started":"2024-09-13T12:28:48.619811Z","shell.execute_reply":"2024-09-13T12:28:48.688216Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"###===========此处可以查看不同方向的切片都与哪些疾病有关=============##\nif CFG.EDA:\n    print('train_Axial_T2:',train_Axial_T2.condition.unique())\n    print()\n    print('train_Sagittal_T1',train_Sagittal_T1.condition.unique())\n    print()\n    print('train_Sagittal_T2:',train_Sagittal_T2.condition.unique())\n# Spinal Canal Stenosis在train_Sagittal_T1与train_Sagittal_T2中都出现过","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:48.690406Z","iopub.execute_input":"2024-09-13T12:28:48.690706Z","iopub.status.idle":"2024-09-13T12:28:48.701480Z","shell.execute_reply.started":"2024-09-13T12:28:48.690672Z","shell.execute_reply":"2024-09-13T12:28:48.700372Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 数据分析结论\n- train_Sagittal_T2每一个series有5个坐标\n- train_Sagittal_T1和train_Axial_T2有10个坐标\n### 因此不同方向的切片数据需要使用不同的坐标预测模型，而且模型的输出的维度也不一样","metadata":{}},{"cell_type":"markdown","source":"# 先处理train_Sagittal_T2数据的\n- 处理dataset\n- ...","metadata":{}},{"cell_type":"code","source":"# 先用train_Sagittal_T2来训练\ntrain_Sagittal_T2 = train_Sagittal_T2.sort_values(['study_id', 'series_id', 'level']).reset_index(drop=True)\nprint(train_Sagittal_T2.shape)\nprint('总共有几个方向',train_Sagittal_T2.series_description.unique())\ndisplay(train_Sagittal_T2.head(12))","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:48.703022Z","iopub.execute_input":"2024-09-13T12:28:48.703378Z","iopub.status.idle":"2024-09-13T12:28:48.727983Z","shell.execute_reply.started":"2024-09-13T12:28:48.703343Z","shell.execute_reply":"2024-09-13T12:28:48.726971Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"if CFG.EDA:\n    lis_de = glob.glob('/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_images/4003253/1054713880/*')\n    for i in lis_de:\n        print(i)","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:48.729324Z","iopub.execute_input":"2024-09-13T12:28:48.729692Z","iopub.status.idle":"2024-09-13T12:28:48.738213Z","shell.execute_reply.started":"2024-09-13T12:28:48.729648Z","shell.execute_reply":"2024-09-13T12:28:48.736615Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def clean_dataframe(df):\n    \"\"\"\n    清洗输入的 DataFrame，按照 'study_id'、'series_id' 和 'series_description' 分组，\n    并过滤掉 'x' 列表长度不等于 10 的记录。\n    参数:\n    df (pd.DataFrame): 输入的原始 DataFrame，包含 'study_id'、'series_id'、'series_description' 列，以及 'x' 和 'y' 列。\n\n    返回:\n    pd.DataFrame: 清洗后的 DataFrame，'x' 列表长度为 10。\n    \"\"\"\n    # 按 'study_id'、'series_id' 和 'series_description' 分组，聚合 'x' 和 'y' 列为列表\n    df_clean = df.groupby(['study_id', 'series_id', 'series_description']).agg({\n        'x': list,\n        'y': list\n    }).reset_index()\n    # 检查 'x' 列表长度为  的记录\n    \n#     OK_length就是考虑到不同的方向有不同的坐标数量来进行筛选数据\n    df_clean['x_length'] = df_clean['x'].apply(len)\n    if df_clean.series_description[0]=='Sagittal T2/STIR':\n        OK_length = 5\n    elif df_clean.series_description[0]=='Axial T2':\n        OK_length = 10\n    elif df_clean.series_description[0]=='Sagittal T1':\n        OK_length = 10\n    valid_count = (df_clean['x_length'] == OK_length).sum()\n    invalid_count = (df_clean['x_length'] != OK_length).sum()\n    \n    print(f\"长度为{OK_length}的记录数量: {valid_count}\")\n    print(f\"长度不为{OK_length}的记录数量: {invalid_count}\")\n    display(df_clean.head(2))\n    # 过滤掉 'x' 列表长度不等于 10 的记录\n    df_clean = df_clean[df_clean['x_length'] == OK_length].reset_index(drop=True)\n    \n    # 返回清洗后的 DataFrame\n    return df_clean\n\n# 使\ntrain_Sagittal_T2_clean = clean_dataframe(train_Sagittal_T2)\nprint(train_Sagittal_T2_clean.shape)\n","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:48.743179Z","iopub.execute_input":"2024-09-13T12:28:48.743670Z","iopub.status.idle":"2024-09-13T12:28:48.891573Z","shell.execute_reply.started":"2024-09-13T12:28:48.743636Z","shell.execute_reply":"2024-09-13T12:28:48.890715Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"- 考虑到目标是检测十个关键点，总会有一些数据的点有点问题，筛查一下","metadata":{}},{"cell_type":"code","source":"# 定义一个函数，将 x 和 y 列交替合并成一个长度为20的列表\ndef combine_xy(row):\n    combined = []\n    for x_val, y_val in zip(row['x'], row['y']):\n        combined.append(x_val)\n        combined.append(y_val)\n    return combined\ndef del_column(df):\n    del df['x']\n    del df['y']\n    del df['x_length']\n    return df\n# 应用该函数，创建新的 label 列\ntrain_Sagittal_T2_clean['label'] = train_Sagittal_T2_clean.apply(combine_xy, axis=1)\n# 不需要，x,y,x_length信息\ntrain_Sagittal_T2_TR = del_column(train_Sagittal_T2_clean)\ntrain_Sagittal_T2_TR =train_Sagittal_T2_TR.reset_index(drop=True)\ndisplay(train_Sagittal_T2_TR.head())","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:48.892781Z","iopub.execute_input":"2024-09-13T12:28:48.893077Z","iopub.status.idle":"2024-09-13T12:28:48.952405Z","shell.execute_reply.started":"2024-09-13T12:28:48.893043Z","shell.execute_reply":"2024-09-13T12:28:48.951503Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 为什么这个地方的图像尺寸使用的是256？\n#### 此代码为stage1，并非代码模型的核心部分，定位感兴趣的区域（大体位置）并不需要特别高的精度，而模型的大小（backbone）与图像的尺寸非常影响模型的推理时间，所以选择一个小的模型与","metadata":{}},{"cell_type":"code","source":"# 处理dataset的时候添加新功能，让图片变成正方向而不随意变形","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:48.953826Z","iopub.execute_input":"2024-09-13T12:28:48.954605Z","iopub.status.idle":"2024-09-13T12:28:48.959106Z","shell.execute_reply.started":"2024-09-13T12:28:48.954553Z","shell.execute_reply":"2024-09-13T12:28:48.958042Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"class PreTrainDataset(torch.utils.data.Dataset):\n    def __init__(self, df, label_len):\n        self.df = df\n        self.label_len = label_len\n\n    def load_dcm_img(self, dcm_path, label, target_size=(256, 256)):\n        \"\"\"\n        读取 DICOM 图像并将其转换为目标大小，同时将绝对坐标归一化。\n        参数:\n        dcm_path (str): DICOM 文件的路径。\n        label (list): 绝对坐标，格式为 [x1, y1, x2, y2, ...]。\n        target_size (tuple): 图像的目标大小，默认为 (256, 256)。\n        返回:\n        tuple: 归一化后的图像和坐标。\n        \"\"\"\n        # 读取 DICOM 文件\n        img = pydicom.dcmread(dcm_path)\n        pixel_array = img.pixel_array\n        # 检查图像是否为正方形\n        if CFG.ZHENG:\n            if pixel_array.shape[0] != pixel_array.shape[1]:\n                print('原图片不是正方形')\n                print('SHAPE',pixel_array.shape)\n            \n        # 将 label 转换为 numpy 数组并进行归一化\n        label = np.array(label, dtype=np.float32)\n        label[::2] = label[::2] / pixel_array.shape[1]  # x 坐标归一化\n        label[1::2] = label[1::2] / pixel_array.shape[0]  # y 坐标归一化\n        # 检查图像是否读取正确\n        if pixel_array is None:\n            print(f\"Error: Unable to load DICOM image at path: {dcm_path}\")\n            return None\n        # Resize 图像到目标大小\n        image_resized = cv2.resize(pixel_array, target_size)\n        # 如果 DICOM 图像是单通道灰度图像，复制通道\n        if len(image_resized.shape) == 2:\n            image_resized = np.stack([image_resized] * 3, axis=-1)\n        # 将图像的维度顺序转换为 (channels, height, width)\n        image_transposed = np.transpose(image_resized, (2, 0, 1))\n        # 将图像数据转换为浮点数\n        image_float = image_transposed.astype(np.float32)\n        # 归一化到 [0, 1] 范围\n        image_normalized = image_float / 255.0\n        return image_normalized, label\n\n    def __len__(self):\n        return len(self.df)  # 返回数据集的大小\n    \n    def __getitem__(self, idx):\n        # 获取人员ID和系列ID\n        st_id = self.df.iloc[idx]['study_id']\n#         print('st_id:', st_id)\n        series_id = self.df.iloc[idx]['series_id']\n        # 获取标签\n        label = self.df.iloc[idx]['label']\n        # 统计多少张图像\n        num_sag = glob.glob(f'/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_images/{st_id}/{series_id}/*.dcm')\n        # 找不到图像就返回\n        if len(num_sag) == 0:  # 如果没有找到图片\n            print(f'数据缺失: {st_id}')\n            return {'img': torch.zeros(3, 256, 256), 'label': torch.zeros(self.label_len).float()}\n        # 获取中间的切片\n        center = len(num_sag) // 2\n        path_sag = f'/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_images/{st_id}/{series_id}/{center}.dcm'\n\n        # 加载图像 图像读取，并且需要把坐标点归一化\n        img, label = self.load_dcm_img(path_sag, label)\n\n        # 检查图像是否成功加载\n        if img is None:\n            print(f\"加载图像失败: {path_sag}\")\n            img = torch.zeros(3, 256, 256)  # 返回全0张量\n\n        # 转换为 PyTorch 张量\n        img = torch.tensor(img)\n        label = torch.tensor(label)\n\n        # 返回图片和标签\n        return {'img': img, 'label': label}\n","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:48.960563Z","iopub.execute_input":"2024-09-13T12:28:48.960852Z","iopub.status.idle":"2024-09-13T12:28:48.977997Z","shell.execute_reply.started":"2024-09-13T12:28:48.960820Z","shell.execute_reply":"2024-09-13T12:28:48.976998Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"\n# 需要手动设置 label_len\n# - train_Sagittal_T2 每一个 series 有 5 个坐标\n# - train_Sagittal_T1 和 train_Axial_T2 有 10 个坐标\nlabel_len = 10\ntrain_ds = PreTrainDataset(train_Sagittal_T2_TR, label_len) \n\n# 打印单个样本的信息\nprint(\"---- 单个样本的形状 -----\")\nfor k, v in train_ds[786].items():\n    if isinstance(v, np.ndarray):\n        print(k, v.shape)  # 打印每个字段的形状\n    else:\n        print(k, v)\n# 原图不是正方形，怎么处理啊？\n# ---- 单个样本的形状 -----\n# SHAPE (456, 384)\n# 原图片不是正方形","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:48.979588Z","iopub.execute_input":"2024-09-13T12:28:48.979919Z","iopub.status.idle":"2024-09-13T12:28:49.008955Z","shell.execute_reply.started":"2024-09-13T12:28:48.979879Z","shell.execute_reply":"2024-09-13T12:28:49.007934Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 🤔如果在定位坐标点的时候，采取了形变，是不是不影响坐标的相对位置，后期只需要在裁剪的时候注意不要发生形变就行了","metadata":{}},{"cell_type":"code","source":"if CFG.EDA:\n    def visualize_sample(sample):\n        \"\"\"\n        可视化单个样本的图像及其坐标。\n        参数:\n        sample (dict): 包含图像和标签的样本。\n        \"\"\"\n        img = sample['img']\n        label = sample['label']\n\n        # 将图像从 PyTorch 张量转换为 numpy 数组\n        img_np = img.numpy()\n        # 转换维度顺序为 (height, width, channels)\n        img_np = np.transpose(img_np, (1, 2, 0))\n        # 将图像归一化到 [0, 1] 范围\n        img_np = np.clip(img_np, 0, 1)\n\n        # 将标签坐标转换为绝对坐标\n        img_height, img_width, _ = img_np.shape\n        print('高：',img_height)\n        print('宽：',img_width)\n        \n        coords = label.numpy()\n        coords[::2] = coords[::2] * img_width\n        coords[1::2] = coords[1::2] * img_height\n\n        # 绘制图像\n        plt.figure(figsize=(6, 6))\n        plt.imshow(img_np, cmap='gray')\n        plt.title('DICOM Image with Coordinates')\n\n        # 绘制坐标点 (使用红色圆圈表示)\n        num_points = len(coords) // 2\n        for i in range(num_points):\n            x, y = coords[2 * i], coords[2 * i + 1]\n            plt.scatter(x, y, color='red', s=100, edgecolor='black', label=f'Point {i+1}')\n\n        plt.axis('on')\n        plt.legend()\n        plt.show()\n\n    # 获取一个样本并可视化\n    sample = train_ds[786]\n    visualize_sample(sample)\n","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:49.010398Z","iopub.execute_input":"2024-09-13T12:28:49.011278Z","iopub.status.idle":"2024-09-13T12:28:49.490776Z","shell.execute_reply.started":"2024-09-13T12:28:49.011192Z","shell.execute_reply":"2024-09-13T12:28:49.489761Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 模型相关功能设置\n","metadata":{}},{"cell_type":"code","source":"def batch_to_device(batch, device, skip_keys=[]):\n    # 将批次数据移动到指定设备（如 GPU）\n    batch_dict = {}\n    for key in batch:\n        if key in skip_keys:\n            # 如果键在跳过列表中，则不移动到设备\n            batch_dict[key] = batch[key]\n        else:\n            # 否则将数据移动到指定设备\n            batch_dict[key] = batch[key].to(device)\n    return batch_dict\ndef load_weights_skip_mismatch(model, weights_path, device):\n    # 加载权重时处理形状不匹配\n    state_dict = torch.load(weights_path, map_location=device)  # 加载权重\n    model_dict = model.state_dict()  # 获取模型当前的状态字典\n    \n    params = {}\n    for (sdk, sfv), (mdk, mdv) in zip(state_dict.items(), model_dict.items()):\n        if sfv.size() == mdv.size():\n            # 如果权重的形状匹配，则添加到参数字典中\n            params[sdk] = sfv\n        else:\n            print(\"Skipping param: {}, {} != {}\".format(sdk, sfv.size(), mdv.size()))  # 打印形状不匹配的信息\n    \n    # 加载权重并忽略形状不匹配的参数\n    model.load_state_dict(params, strict=False)\n    print(\"Loaded weights from:\", weights_path)  # 打印加载权重的路径","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:49.492113Z","iopub.execute_input":"2024-09-13T12:28:49.492543Z","iopub.status.idle":"2024-09-13T12:28:49.503374Z","shell.execute_reply.started":"2024-09-13T12:28:49.492495Z","shell.execute_reply":"2024-09-13T12:28:49.502267Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 设置模型参数","metadata":{}},{"cell_type":"code","source":"# 配置模型训练的参数\ncfg = SimpleNamespace(\n    img_dir=\"/kaggle/input/lumbar-coordinate-pretraining-dataset/data/\",  # 数据集的路径\n    device=torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\"),  # 设置设备为GPU（如果可用），否则使用CPU\n#     n_frames=3,  # 处理的帧数，可能是视频数据的时间步长\n    epochs=5,  # 训练的轮数\n    lr=0.0005,  # 学习率，控制模型权重更新的步长\n    batch_size=16,  # 批量大小，每次训练时处理的样本数\n    backbone=\"resnet18\",  # 网络的主干模型，这里使用 ResNet18\n    seed=0,  # 随机种子，确保结果的可重复性\n)\n\n# 设置随机种子，以保证结果的可重复性\nset_seed(seed=cfg.seed)  # Makes results reproducable","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:49.504609Z","iopub.execute_input":"2024-09-13T12:28:49.505355Z","iopub.status.idle":"2024-09-13T12:28:49.515784Z","shell.execute_reply.started":"2024-09-13T12:28:49.505306Z","shell.execute_reply":"2024-09-13T12:28:49.514859Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 此方向的为5个坐标10个小数，所以设置为10train_Sagittal_T2_TR","metadata":{}},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\n\n# 假设 train_Sagittal_T2_TR 是一个 pandas DataFrame\n# 将数据随机分成 9:1 的训练和验证集\ntrain_df, val_df = train_test_split(\n    train_Sagittal_T2_TR, \n    test_size=0.1,   # 验证集占 10%\n    random_state=42  # 设置随机种子以确保结果可复现\n)\n# 数据集和数据加载器\ntrain_ds = PreTrainDataset(train_df, label_len) \ntrain_dl = torch.utils.data.DataLoader(train_ds, batch_size=cfg.batch_size, shuffle=True, drop_last=True)  # 创建训练数据加载器，设置批量大小，打乱数据，并丢弃最后一个不完整的批次\n\nval_ds = PreTrainDataset(val_df, label_len) \nval_dl = torch.utils.data.DataLoader(val_ds, batch_size=cfg.batch_size, shuffle=False)  # 创建训练数据加载器，设置批量大小，打乱数据，并丢弃最后一个不完整的批次\n\n# 模型\nmodel = timm.create_model('resnet18', pretrained=True, num_classes=10)  # 创建 ResNet18 模型，使用预训练权重，并设置输出类别数为 10\nmodel = model.to(cfg.device)  # 将模型移动到指定设备（如 GPU 或 CPU）\n\n# 损失函数和优化器\ncriterion = nn.MSELoss()  # 设置损失函数为均方误差损失，用于回归任务\noptimizer = torch.optim.AdamW(model.parameters(), lr=cfg.lr)  # 设置优化器为 AdamW，学习率为配置中的值","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:49.517099Z","iopub.execute_input":"2024-09-13T12:28:49.517842Z","iopub.status.idle":"2024-09-13T12:28:49.958595Z","shell.execute_reply.started":"2024-09-13T12:28:49.517797Z","shell.execute_reply":"2024-09-13T12:28:49.957574Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"val_df.head()","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:49.959845Z","iopub.execute_input":"2024-09-13T12:28:49.960155Z","iopub.status.idle":"2024-09-13T12:28:49.974097Z","shell.execute_reply.started":"2024-09-13T12:28:49.960119Z","shell.execute_reply":"2024-09-13T12:28:49.973263Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"train_df.shape","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:49.975546Z","iopub.execute_input":"2024-09-13T12:28:49.976136Z","iopub.status.idle":"2024-09-13T12:28:49.983691Z","shell.execute_reply.started":"2024-09-13T12:28:49.976094Z","shell.execute_reply":"2024-09-13T12:28:49.982621Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"val_df.shape","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:49.985371Z","iopub.execute_input":"2024-09-13T12:28:49.986052Z","iopub.status.idle":"2024-09-13T12:28:49.991776Z","shell.execute_reply.started":"2024-09-13T12:28:49.986008Z","shell.execute_reply":"2024-09-13T12:28:49.990958Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"if CFG.TRAIN:\n    for epoch in range(cfg.epochs + 1):\n\n        # 训练循环\n        loss = torch.tensor([0.]).float().to(cfg.device)  # 初始化训练损失为 0\n        if epoch != 0:  # 第 0 轮跳过训练\n            model = model.train()  # 设置模型为训练模式\n            for batch in tqdm(train_dl):  # 遍历训练数据加载器中的批次\n                batch = batch_to_device(batch, cfg.device)  # 将批次数据移动到指定设备\n\n                optimizer.zero_grad()  # 清零梯度\n\n                x_out = model(batch[\"img\"].float())  # 前向传播，得到模型输出\n                x_out = torch.sigmoid(x_out)  # 使用 Sigmoid 函数将输出映射到 [0, 1] 范围\n\n                loss = criterion(x_out, batch[\"label\"].float())  # 计算损失\n                loss.backward()  # 反向传播计算梯度\n                optimizer.step()  # 更新模型参数\n\n        # 验证循环\n        val_loss = 0  # 初始化验证损失为 0\n        with torch.no_grad():  # 在验证阶段不需要计算梯度\n            model = model.eval()  # 设置模型为评估模式\n            for batch in tqdm(val_dl):  # 遍历验证数据加载器中的批次\n                batch = batch_to_device(batch, cfg.device)  # 将批次数据移动到指定设备\n\n                pred = model(batch[\"img\"].float())  # 前向传播，得到模型预测\n                pred = torch.sigmoid(pred)  # 使用 Sigmoid 函数将预测映射到 [0, 1] 范围\n\n                val_loss += criterion(pred, batch[\"label\"].float()).item()  # 计算并累加验证损失\n            val_loss /= len(val_dl)  # 计算平均验证损失\n\n        # 可视化\n    #     visualize_prediction(batch, pred, epoch)  # 可视化当前批次的预测结果\n\n        print(f\"Epoch {epoch + 1}, Training Loss: {loss.item()}, Validation Loss: {val_loss}\")\n        # 打印当前轮次的训练损失和验证损失\n\n    print(\"训练完成...\")","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:28:49.993154Z","iopub.execute_input":"2024-09-13T12:28:49.993839Z","iopub.status.idle":"2024-09-13T12:31:46.235965Z","shell.execute_reply.started":"2024-09-13T12:28:49.993795Z","shell.execute_reply":"2024-09-13T12:31:46.234984Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 模型保存\n# 名字保存的很具体","metadata":{}},{"cell_type":"code","source":"f= \"{}_{}train_Sagittal_T2.pt\".format(cfg.backbone, cfg.seed)\n\ntorch.save(model.state_dict(), f)\nprint(\"Saved weights: {}\".format(f))","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:31:46.237548Z","iopub.execute_input":"2024-09-13T12:31:46.237954Z","iopub.status.idle":"2024-09-13T12:31:46.345256Z","shell.execute_reply.started":"2024-09-13T12:31:46.237907Z","shell.execute_reply":"2024-09-13T12:31:46.344295Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 加载模型","metadata":{}},{"cell_type":"code","source":"# 加载保存的权重\ndef load_weights_skip_mismatch(model, weights_path, device):\n    # 从权重文件中加载状态字典\n    state_dict = torch.load(weights_path, map_location=device)\n    # 获取模型当前的状态字典\n    model_state_dict = model.state_dict()\n    # 只保留在模型中匹配的权重\n    new_state_dict = {k: v for k, v in state_dict.items() if k in model_state_dict}\n    # 更新模型的状态字典\n    model_state_dict.update(new_state_dict)\n    # 将更新后的状态字典加载到模型中\n    model.load_state_dict(model_state_dict)\n    \n","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:31:46.346617Z","iopub.execute_input":"2024-09-13T12:31:46.347020Z","iopub.status.idle":"2024-09-13T12:31:46.353176Z","shell.execute_reply.started":"2024-09-13T12:31:46.346971Z","shell.execute_reply":"2024-09-13T12:31:46.352278Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# 重新定义模型结构\nmodel = timm.create_model('resnet18', pretrained=True, num_classes=10)\nmodel = model.to(cfg.device)  # 将模型移动到指定设备\n# 示例使用：\nweights_path = \"/kaggle/working/resnet18_0train_Sagittal_T2.pt\"\nload_weights_skip_mismatch(model, weights_path, cfg.device)  # 加载权重到模型\nprint(\"模型的最后一层:\")\nlast_layer = list(model.children())[-1]  # 获取最后一层\nprint(last_layer)","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:31:46.354405Z","iopub.execute_input":"2024-09-13T12:31:46.354720Z","iopub.status.idle":"2024-09-13T12:31:46.983582Z","shell.execute_reply.started":"2024-09-13T12:31:46.354684Z","shell.execute_reply":"2024-09-13T12:31:46.982605Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def predict(model, data_loader, device):\n    model.eval()  # 将模型设置为评估模式\n    all_preds = []\n    \n    with torch.no_grad():  # 禁用梯度计算\n        for batch in tqdm(data_loader):  # 遍历 DataLoader\n            # 将数据移动到指定设备（CPU或GPU）\n            inputs = batch['img'].to(device).float()  # 假设图像数据位于 'img' 键\n            preds = model(inputs)  # 模型预测\n            preds = torch.sigmoid(preds)  # 假设模型输出 logits，需要用 sigmoid 进行转换\n            all_preds.append(preds.cpu().numpy())  # 收集预测结果\n\n    all_preds = np.concatenate(all_preds, axis=0)  # 合并所有批次的预测\n    return all_preds\n\n# 使用示例\n\n","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:31:46.985012Z","iopub.execute_input":"2024-09-13T12:31:46.985498Z","iopub.status.idle":"2024-09-13T12:31:46.992635Z","shell.execute_reply.started":"2024-09-13T12:31:46.985445Z","shell.execute_reply":"2024-09-13T12:31:46.991665Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nall_predictions = predict(model, val_dl, device)","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:31:46.993931Z","iopub.execute_input":"2024-09-13T12:31:46.994300Z","iopub.status.idle":"2024-09-13T12:31:50.148051Z","shell.execute_reply.started":"2024-09-13T12:31:46.994255Z","shell.execute_reply":"2024-09-13T12:31:50.147083Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print('预测结果的维度:',all_predictions.shape)\nprint('##########')\nprint(all_predictions)","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:31:50.152720Z","iopub.execute_input":"2024-09-13T12:31:50.153028Z","iopub.status.idle":"2024-09-13T12:31:50.159276Z","shell.execute_reply.started":"2024-09-13T12:31:50.152994Z","shell.execute_reply":"2024-09-13T12:31:50.158291Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### 对预测结果可视化","metadata":{}},{"cell_type":"code","source":"val_df.head()","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:31:50.160722Z","iopub.execute_input":"2024-09-13T12:31:50.161097Z","iopub.status.idle":"2024-09-13T12:31:50.179914Z","shell.execute_reply.started":"2024-09-13T12:31:50.161060Z","shell.execute_reply":"2024-09-13T12:31:50.179013Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def visualize_val(dcm_path, normalized_coords):\n    \"\"\"\n    可视化 DICOM 图像及其归一化后的坐标。\n    \n    参数:\n    dcm_path (str): DICOM 文件的路径。\n    normalized_coords (numpy array): 归一化的坐标，范围为 [0, 1]。\n    \"\"\"\n    # 读取 DICOM 图像\n    dcm_data = pydicom.dcmread(dcm_path)\n    img = dcm_data.pixel_array\n\n    # 将图像归一化到 [0, 1] 范围\n    img = img.astype(np.float32)\n    img = (img - np.min(img)) / (np.max(img) - np.min(img))\n\n    # 获取图像的宽度和高度\n    img_height, img_width = img.shape\n    print('高：', img_height)\n    print('宽：', img_width)\n\n    # 将归一化的坐标转换为绝对坐标\n    coords = normalized_coords.copy()\n    coords[::2] = coords[::2] * img_width\n    coords[1::2] = coords[1::2] * img_height\n\n    # 绘制图像\n    plt.figure(figsize=(5, 5))\n    plt.imshow(img, cmap='gray')\n    plt.title('DICOM Image with Coordinates')\n\n    # 绘制坐标点 (使用红色圆圈表示)\n    num_points = len(coords) // 2\n    for i in range(num_points):\n        x, y = coords[2 * i], coords[2 * i + 1]\n        plt.scatter(x, y, color='red', s=100, edgecolor='black', label=f'Point {i+1}')\n\n    plt.axis('on')\n    plt.legend()\n    plt.show()\n","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:31:50.180936Z","iopub.execute_input":"2024-09-13T12:31:50.182732Z","iopub.status.idle":"2024-09-13T12:31:50.192285Z","shell.execute_reply.started":"2024-09-13T12:31:50.182696Z","shell.execute_reply":"2024-09-13T12:31:50.191290Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 边框怎么取z","metadata":{}},{"cell_type":"code","source":"CFG.boxshape = 1.4","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:33:17.160681Z","iopub.execute_input":"2024-09-13T12:33:17.161445Z","iopub.status.idle":"2024-09-13T12:33:17.165450Z","shell.execute_reply.started":"2024-09-13T12:33:17.161403Z","shell.execute_reply":"2024-09-13T12:33:17.164430Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def visualize_sample_with_box(dcm_path, normalized_coords):\n    \"\"\"\n    可视化 DICOM 图像，并根据第一个和第五个点的距离绘制正方形框。\n    \n    参数:\n    dcm_path (str): DICOM 文件的路径。\n    normalized_coords (numpy array): 归一化的坐标，范围为 [0, 1]。\n    \"\"\"\n    # 读取 DICOM 图像\n    dcm_data = pydicom.dcmread(dcm_path)\n    img = dcm_data.pixel_array\n\n    # 将图像归一化到 [0, 1] 范围\n    img = img.astype(np.float32)\n    img = (img - np.min(img)) / (np.max(img) - np.min(img))\n\n    # 获取图像的宽度和高度\n    img_height, img_width = img.shape\n\n    # 将归一化的坐标转换为绝对坐标\n    coords = normalized_coords.copy()\n    coords[::2] = coords[::2] * img_width\n    coords[1::2] = coords[1::2] * img_height\n\n    # 获取第一个点和第五个点的坐标\n    x1, y1 = coords[0], coords[1]  # 第一个点\n    x5, y5 = coords[8], coords[9]  # 第五个点\n\n    # 计算第一个点和第五个点之间的欧几里得距离\n    distance = np.sqrt((x5 - x1) ** 2 + (y5 - y1) ** 2)\n    print(f'第一个点和第五个点之间的距离: {distance}')\n\n    # 计算中心点 (第一个点和第五个点的平均值)\n    center_x = (x1 + x5) / 2\n    center_y = (y1 + y5) / 2\n\n    # 正方形的边长是距离的 1.5 倍\n    side_length = distance * CFG.boxshape\n    half_side = side_length / 2\n\n    # 计算正方形的四个角点坐标\n    top_left = (center_x - half_side, center_y - half_side)\n    top_right = (center_x + half_side, center_y - half_side)\n    bottom_left = (center_x - half_side, center_y + half_side)\n    bottom_right = (center_x + half_side, center_y + half_side)\n\n    # 绘制图像\n    plt.figure(figsize=(5, 5))\n    plt.imshow(img, cmap='gray')\n    plt.title('DICOM Image with Coordinates and Square Box')\n\n    # 绘制坐标点 (使用红色圆圈表示)\n    num_points = len(coords) // 2\n    for i in range(num_points):\n        x, y = coords[2 * i], coords[2 * i + 1]\n        plt.scatter(x, y, color='red', s=100, edgecolor='black', label=f'Point {i+1}' if i < 5 else '')\n\n    # 绘制正方形框\n    square_x = [top_left[0], top_right[0], bottom_right[0], bottom_left[0], top_left[0]]\n    square_y = [top_left[1], top_right[1], bottom_right[1], bottom_left[1], top_left[1]]\n    plt.plot(square_x, square_y, color='blue', linewidth=2, label='Square Box')\n\n    plt.axis('on')\n    plt.legend()\n    plt.show()\n\n","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"deb_idx = 45\nst_id = val_df.iloc[deb_idx]['study_id']\nseries_id = val_df.iloc[deb_idx]['series_id']\n# 统计多少张图像\nnum_sag = glob.glob(f'/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_images/{st_id}/{series_id}/*.dcm')\n# 找不到图像就返回\nif len(num_sag) == 0:  # 如果没有找到图片\n    print(f'数据缺失: {st_id}')\ncenter = len(num_sag) // 2\npath_sag = f'/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_images/{st_id}/{series_id}/{center}.dcm'\nprint(path_sag)\n# 对应的坐标数据\nCORD = all_predictions[deb_idx]\nprint('坐标数据',CORD)\n","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:32:57.108911Z","iopub.execute_input":"2024-09-13T12:32:57.109306Z","iopub.status.idle":"2024-09-13T12:32:57.129664Z","shell.execute_reply.started":"2024-09-13T12:32:57.109267Z","shell.execute_reply":"2024-09-13T12:32:57.128571Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 在验证数据上查看效果","metadata":{}},{"cell_type":"code","source":"visualize_val(path_sag, CORD)","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:32:59.267069Z","iopub.execute_input":"2024-09-13T12:32:59.268243Z","iopub.status.idle":"2024-09-13T12:32:59.729204Z","shell.execute_reply.started":"2024-09-13T12:32:59.268182Z","shell.execute_reply":"2024-09-13T12:32:59.728212Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 坐标裁剪\n-已知道其中的5个坐标，求出第一个点和第五个点之间的距离，这两个点之间的中心为原点，距离的1.5倍作为正方形的边长，在原图上绘制一个框","metadata":{}},{"cell_type":"code","source":"visualize_sample_with_box(path_sag, CORD)","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:33:33.527541Z","iopub.execute_input":"2024-09-13T12:33:33.527916Z","iopub.status.idle":"2024-09-13T12:33:34.015771Z","shell.execute_reply.started":"2024-09-13T12:33:33.527878Z","shell.execute_reply":"2024-09-13T12:33:34.014803Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 看看原图的坐标点位置","metadata":{}},{"cell_type":"code","source":"sample = val_ds[deb_idx]\nvisualize_sample(sample)","metadata":{"execution":{"iopub.status.busy":"2024-09-13T12:34:00.632567Z","iopub.execute_input":"2024-09-13T12:34:00.632950Z","iopub.status.idle":"2024-09-13T12:34:01.117366Z","shell.execute_reply.started":"2024-09-13T12:34:00.632912Z","shell.execute_reply":"2024-09-13T12:34:01.116465Z"},"trusted":true},"execution_count":null,"outputs":[]}]}