{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":52254,"databundleVersionId":9674523,"sourceType":"competition"}],"dockerImageVersionId":31012,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# -*- coding: utf-8 -*-\nimport gc\nimport os\nimport numpy as np\nimport pandas as pd\nimport cv2\nimport pydicom\nfrom pydicom.pixel_data_handlers.util import apply_voi_lut\nimport random\nimport glob\nimport nibabel as nib\nimport math\nimport json\nimport time\nimport shutil\nimport traceback  # 添加此库来获取详细的错误信息\nimport psutil  # 添加此库来监控内存使用情况\nfrom concurrent.futures import ProcessPoolExecutor, as_completed\nfrom tqdm.notebook import tqdm\nimport matplotlib.pyplot as plt\nimport cv2\n\n# === 基本配置 ===\n# --- 数据路径 ---\nDATA_DIR = '/kaggle/input/rsna-2023-abdominal-trauma-detection'\nTRAIN_IMAGES_DIR = os.path.join(DATA_DIR, 'train_images')\nSEGMENTATION_DIR = os.path.join(DATA_DIR, 'segmentations')\nMETA_DIR = DATA_DIR # Assuming meta files are in the root DATA_DIR\n\n# --- 输出路径 (在 Kaggle 工作目录中) ---\nPREPROCESSED_DIR = '/kaggle/working/preprocessed_data'\n# 为了能够在笔记本重启后恢复进度，我们需要使用Kaggle的输出目录\nOUTPUT_DATASET_DIR = '/kaggle/output/preprocessed-segmentation-data'\n\n# 确保目录存在\nos.makedirs(PREPROCESSED_DIR, exist_ok=True)\nos.makedirs(OUTPUT_DATASET_DIR, exist_ok=True)\n\n# --- 图像和模型参数 ---\nIMG_SIZE = (224, 224) # 你可以选择 128, 192, 224 等\nTARGET_SIZE_INT = IMG_SIZE[0]\nTARGET_SIZE = IMG_SIZE[0]\nN_INPUT_CHANNELS = 3 # 如果你的模型需要 3 通道输入\nBEST_NIFTI_ORIENTATION_TRANSFORM = ['rot90'] # 根据您的调试结果\nUSE_REVERSE_NIFTI_MAPPING = True # 设置为True来启用反向映射\n\n# --- 器官映射 ---\n# NII标签: 1:肝脏, 2:脾脏, 3:左肾, 4:右肾, 5:肠道\nORGAN_MAP_NII = {\n    1: 'liver',\n    2: 'spleen',\n    3: 'kidney',  # 合并标签 3 和 4\n    5: 'bowel'\n}\n# 输出通道映射 (模型输出顺序)\nORGAN_CHANNEL_MAP = {\n    'liver': 0,\n    'spleen': 1,\n    'kidney': 2,\n    'bowel': 3\n}\nNUM_ORGANS = len(ORGAN_CHANNEL_MAP)\n\n# --- 并行处理 ---\n# 减少工作进程数，避免内存问题\nNUM_WORKERS = 2  # 从4减少到2\n# 批处理参数\nPATIENT_BATCH_SIZE = 5  # 从20减少到5\nSLICE_BATCH_SIZE = 10   # 从20减少到10\nSAVE_INTERVAL = 5       # 从10减少到5，更频繁保存进度\n\n# --- 日志和进度文件 ---\nLOG_FILE = os.path.join(PREPROCESSED_DIR, 'preprocessing_log.txt')\n\n# --- 添加内存监控阈值 ---\nMEMORY_THRESHOLD = 85  # 如果内存使用超过85%，暂停处理\n\n# === 辅助函数 ===\ndef log_message(message):\n    \"\"\"记录消息到日志文件和控制台\"\"\"\n    timestamp = time.strftime(\"%Y-%m-%d %H:%M:%S\")\n    log_line = f\"[{timestamp}] {message}\"\n    \n    print(log_line)\n    \n    with open(LOG_FILE, 'a') as f:\n        f.write(log_line + '\\n')\n\ndef get_memory_usage():\n    \"\"\"获取当前内存使用百分比\"\"\"\n    return psutil.virtual_memory().percent\n\ndef check_memory():\n    \"\"\"检查内存使用情况，如果超过阈值则等待\"\"\"\n    mem_usage = get_memory_usage()\n    if mem_usage > MEMORY_THRESHOLD:\n        log_message(f\"内存使用率高: {mem_usage}%，超过阈值 {MEMORY_THRESHOLD}%，暂停处理并强制GC\")\n        gc.collect()\n        time.sleep(5)  # 等待5秒，让系统有时间释放内存\n        return False\n    return True\n\n# ... (log_message, get_memory_usage, check_memory 等函数之后) ...\n\ndef apply_orientation_transform(mask_slice, transform_ops=None):\n    \"\"\"\n    应用方向变换到掩码切片。\n    transform_ops: 一个包含操作字符串的列表，例如 ['rot90', 'fliplr']\n                   可能的op: 'rot90', 'rot90_2', 'rot90_3', 'fliplr', 'flipud'\n    \"\"\"\n    if transform_ops is None:\n        return mask_slice\n\n    transformed_mask = mask_slice.copy() #确保在副本上操作\n    for op in transform_ops:\n        if op == 'rot90':\n            transformed_mask = np.rot90(transformed_mask)\n        elif op == 'rot90_2': # 旋转180度\n            transformed_mask = np.rot90(transformed_mask, k=2)\n        elif op == 'rot90_3': # 旋转270度\n            transformed_mask = np.rot90(transformed_mask, k=3)\n        elif op == 'fliplr': # 左右翻转\n            transformed_mask = np.fliplr(transformed_mask)\n        elif op == 'flipud': # 上下翻转\n            transformed_mask = np.flipud(transformed_mask)\n        else:\n            log_message(f\"警告: 未知的方向变换操作 '{op}'\") # 使用log_message\n    return transformed_mask\n\ndef backup_progress_files():\n    \"\"\"备份进度文件到输出数据集目录\"\"\"\n    # 备份全局进度文件\n    global_progress_file = os.path.join(PREPROCESSED_DIR, 'global_progress.json')\n    if os.path.exists(global_progress_file):\n        shutil.copy(global_progress_file, os.path.join(OUTPUT_DATASET_DIR, 'global_progress.json'))\n    \n    # 备份日志文件\n    if os.path.exists(LOG_FILE):\n        shutil.copy(LOG_FILE, os.path.join(OUTPUT_DATASET_DIR, 'preprocessing_log.txt'))\n    \n    log_message(\"进度文件已备份到输出数据集目录\")\n\ndef restore_progress_files():\n    \"\"\"从输出数据集目录恢复进度文件\"\"\"\n    # 恢复全局进度文件\n    output_global_progress = os.path.join(OUTPUT_DATASET_DIR, 'global_progress.json')\n    if os.path.exists(output_global_progress):\n        shutil.copy(output_global_progress, os.path.join(PREPROCESSED_DIR, 'global_progress.json'))\n        log_message(\"已从输出数据集目录恢复全局进度文件\")\n    \n    # 恢复日志文件\n    output_log = os.path.join(OUTPUT_DATASET_DIR, 'preprocessing_log.txt')\n    if os.path.exists(output_log):\n        # 追加模式，保留当前日志\n        with open(output_log, 'r') as src, open(LOG_FILE, 'a') as dst:\n            dst.write(src.read())\n        log_message(\"已从输出数据集目录恢复日志文件\")\n\ndef load_dicom_slice(path):\n    try:\n        dicom_file = pydicom.dcmread(path)\n        instance_number = int(dicom_file.InstanceNumber)\n        image = apply_voi_lut(dicom_file.pixel_array, dicom_file)\n\n        min_val = np.min(image)\n        max_val = np.max(image)\n        if max_val > min_val:\n            image = (image - min_val) / (max_val - min_val)\n        else:\n            image = np.zeros_like(image)\n\n        image = image.astype(np.float32)\n\n        if 'PhotometricInterpretation' in dicom_file and dicom_file.PhotometricInterpretation == \"MONOCHROME1\":\n             image = 1.0 - image\n\n        return image, instance_number\n    except Exception as e:\n        # 增加更详细的错误日志\n        log_message(f\"加载 DICOM 错误 {path}: {e}\")\n        return None, None\n\ndef load_multi_organ_segmentation_mask(nii_data_array, slice_index, \n                                       organ_map_nii, organ_channel_map_local, # 避免与全局变量冲突\n                                       num_organs_local, target_size_local_int, # 使用局部变量名\n                                       orientation_transform_ops_local=None,\n                                       nii_path_for_error_msg=None): # 新增参数\n    try:\n        seg_data = nii_data_array\n        if not isinstance(seg_data, np.ndarray) or seg_data.ndim != 3:\n             log_message(f\"NII数据无效: 不是3D数组，形状: {seg_data.shape if hasattr(seg_data, 'shape') else '未知'}\")\n             return None\n\n        num_slices = seg_data.shape[2]\n        if not (0 <= slice_index < num_slices):\n            # log_message(f\"切片索引 {slice_index} 超出范围 (0-{num_slices-1})\") # 此日志可以在调用处处理\n            return None\n\n        mask_slice_float = seg_data[:, :, slice_index]\n\n        # ***** 应用方向变换 *****\n        if orientation_transform_ops_local:\n            mask_slice_float = apply_orientation_transform(mask_slice_float, orientation_transform_ops_local)\n        # ***********************\n            \n        mask_slice = np.round(mask_slice_float).astype(np.int16)\n\n        # multi_channel_mask 的尺寸基于变换后的 mask_slice\n        multi_channel_mask = np.zeros((mask_slice.shape[0], mask_slice.shape[1], num_organs_local), dtype=np.float32)\n\n        for nii_value, organ_name in organ_map_nii.items():\n            if organ_name in organ_channel_map_local:\n                channel_idx = organ_channel_map_local[organ_name]\n                if organ_name == 'kidney':\n                    binary_mask_organ = ((mask_slice == 3) | (mask_slice == 4)).astype(np.float32)\n                else:\n                    binary_mask_organ = (mask_slice == nii_value).astype(np.float32)\n                \n                # 确保维度匹配后才相加\n                if binary_mask_organ.shape == multi_channel_mask.shape[:2]:\n                    multi_channel_mask[:, :, channel_idx] += binary_mask_organ\n                else:\n                    log_message(f\"警告: 器官 {organ_name} 的二值掩码形状 {binary_mask_organ.shape} 与基底 {multi_channel_mask.shape[:2]} 不匹配，尝试resize。\")\n                    resized_bmo = cv2.resize(binary_mask_organ, (multi_channel_mask.shape[1], multi_channel_mask.shape[0]), interpolation=cv2.INTER_NEAREST)\n                    multi_channel_mask[:, :, channel_idx] += resized_bmo\n\n\n        # 现在 multi_channel_mask 是在（可能经过变换的）原始NII切片分辨率下的\n        # 将其resize到目标尺寸\n        if multi_channel_mask.shape[0] != target_size_local_int or multi_channel_mask.shape[1] != target_size_local_int:\n            resized_mask = cv2.resize(\n                multi_channel_mask,\n                (target_size_local_int, target_size_local_int),\n                interpolation=cv2.INTER_NEAREST\n            )\n            if len(resized_mask.shape) == 2 and num_organs_local == 1:\n                 resized_mask = np.expand_dims(resized_mask, axis=-1)\n            elif len(resized_mask.shape) == 2 and num_organs_local > 1:\n                 log_message(f\"调整大小后的掩码形状错误: {resized_mask.shape} (应为3D)，用于切片索引 {slice_index}\")\n                 return None # 多通道压缩通常是错误\n        else:\n             resized_mask = multi_channel_mask # 如果尺寸已匹配，则无需resize\n        \n        final_mask = (resized_mask > 0.5).astype(np.float32) # 确保是 0 或 1\n\n        if final_mask.shape != (target_size_local_int, target_size_local_int, num_organs_local):\n             log_message(f\"最终掩码形状错误: {final_mask.shape}, 应为 ({target_size_local_int}, {target_size_local_int}, {num_organs_local}) 用于切片索引 {slice_index}\")\n             return None\n\n        return final_mask\n\n    except Exception as e:\n        log_message(f\"生成掩码时出错 (切片索引 {slice_index}): {e}\")\n        # log_message(f\"详细错误: {traceback.format_exc()}\") # 可选，用于更详细调试\n        return None\n  \n\ndef preprocess_image_for_unet(image, target_size):\n    try:\n        image_resized = cv2.resize(image, (target_size, target_size), interpolation=cv2.INTER_LINEAR)\n        if N_INPUT_CHANNELS == 3:\n            image_rgb = np.stack([image_resized] * 3, axis=-1)\n            return image_rgb.astype(np.float32)\n        else: # Assuming 1 channel if not 3\n            return np.expand_dims(image_resized, axis=-1).astype(np.float32)\n    except Exception as e:\n        log_message(f\"预处理图像时出错: {e}\")\n        return None\n\ndef get_dicom_files_dict(patient_id, series_id, dicom_tags_df):\n    sorted_dicom_info = []\n    patient_dir = os.path.join(TRAIN_IMAGES_DIR, str(patient_id))\n    series_folder = os.path.join(patient_dir, str(series_id))\n    \n    try:\n        dicom_files = glob.glob(os.path.join(series_folder, '*.dcm'))\n        if not dicom_files: \n            log_message(f\"患者 {patient_id} 系列 {series_id} 没有找到DICOM文件\")\n            return []\n\n        dicom_tuples = []\n        use_tags = False\n        # 尝试使用 DICOM tags 排序 (如果提供了 tags 文件)\n        if dicom_tags_df is not None and all(col in dicom_tags_df.columns for col in ['PatientID', 'SeriesInstanceUID', 'InstanceNumber', 'SOPInstanceUID', 'series_id_extracted']):\n            try:\n                patient_id_str = str(patient_id)\n                tags_subset = dicom_tags_df[\n                     (dicom_tags_df['PatientID'].astype(str) == patient_id_str) &\n                     (dicom_tags_df['series_id_extracted'] == str(series_id)) # 使用提取的 series_id\n                ][['InstanceNumber', 'SOPInstanceUID']].dropna()\n\n                if not tags_subset.empty:\n                    sop_to_inst = dict(zip(tags_subset['SOPInstanceUID'], tags_subset['InstanceNumber'].astype(int)))\n                    use_tags = True\n                    for f_path in dicom_files:\n                        try:\n                            # 只读头，不读像素数据，更快\n                            ds_header = pydicom.dcmread(f_path, stop_before_pixels=True)\n                            ds_sop = ds_header.SOPInstanceUID\n                            if ds_sop in sop_to_inst:\n                                 dicom_tuples.append((sop_to_inst[ds_sop], f_path))\n                            else: # Fallback: read InstanceNumber directly from header\n                                 dicom_tuples.append((int(ds_header.InstanceNumber), f_path))\n                        except Exception as e:\n                            log_message(f\"读取DICOM头失败 {f_path}: {e}\")\n            except Exception as e:\n                log_message(f\"使用DICOM tags排序失败: {e}\")\n                use_tags = False\n\n        # 如果 Tags 失败或不可用，尝试直接从 DICOM 头读取 InstanceNumber\n        if not use_tags or not dicom_tuples:\n            dicom_tuples = []\n            for f_path in dicom_files:\n                try:\n                    ds = pydicom.dcmread(f_path, stop_before_pixels=True)\n                    dicom_tuples.append((int(ds.InstanceNumber), f_path))\n                except Exception as e:\n                    log_message(f\"读取DICOM头失败 {f_path}: {e}\")\n\n        # 如果 DICOM 头也失败，按文件名中的数字排序\n        if not dicom_tuples:\n            try:\n               dicom_tuples = sorted([(int(os.path.splitext(os.path.basename(f))[0]), f) for f in dicom_files])\n            except ValueError:\n               dicom_tuples = sorted([(i, f) for i, f in enumerate(sorted(dicom_files))]) # 按字母顺序\n\n        dicom_tuples.sort(key=lambda x: x[0])\n        sorted_dicom_info = [(item[0], item[1]) for item in dicom_tuples] # (InstanceNumber, path)\n        \n        return sorted_dicom_info\n    except Exception as e:\n        log_message(f\"获取DICOM文件列表失败 (患者 {patient_id}, 系列 {series_id}): {e}\")\n        return []\n\ndef copy_patient_data_to_output(patient_id):\n    \"\"\"将患者的预处理数据复制到输出数据集目录\"\"\"\n    try:\n        src_dir = os.path.join(PREPROCESSED_DIR, str(patient_id))\n        dst_dir = os.path.join(OUTPUT_DATASET_DIR, str(patient_id))\n        \n        if os.path.exists(src_dir):\n            # 确保目标目录存在\n            os.makedirs(dst_dir, exist_ok=True)\n            \n            # 复制所有文件\n            for filename in os.listdir(src_dir):\n                src_file = os.path.join(src_dir, filename)\n                dst_file = os.path.join(dst_dir, filename)\n                if os.path.isfile(src_file):\n                    shutil.copy2(src_file, dst_file)\n            \n            return True\n    except Exception as e:\n        log_message(f\"复制患者数据到输出目录失败 (患者 {patient_id}): {e}\")\n        return False\n\n# === 预处理核心函数 ===\n# 在 === 预处理核心函数 === 部分\ndef preprocess_and_save_data(args):\n    patient_id, dicom_info_list, nii_path, output_dir_base, target_size_int = args # 修改了dicom_paths为dicom_info_list\n    \n    # dicom_info_list 是 [(instance_number, dicom_path), ...] 的列表，已经排序\n\n    try:\n        log_message(f\"开始处理患者 {patient_id}...\")\n        patient_output_dir = os.path.join(output_dir_base, str(patient_id)) # 使用 output_dir_base\n        os.makedirs(patient_output_dir, exist_ok=True)\n        \n        progress_file = os.path.join(patient_output_dir, 'progress.json')\n        # ... (进度文件加载逻辑不变) ...\n        if os.path.exists(progress_file):\n            try:\n                with open(progress_file, 'r') as f:\n                    progress = json.load(f)\n                    if progress.get('completed', False):\n                        log_message(f\"患者 {patient_id} 已经处理完成 (根据进度文件)，跳过。\")\n                        return progress.get('processed_count', 0), patient_id\n                    processed_instances = set(map(str, progress.get('processed_instances', []))) # 确保是字符串集合\n                    log_message(f\"患者 {patient_id} 恢复进度: 已处理 {len(processed_instances)} 个切片。\")\n            except Exception as e:\n                log_message(f\"读取患者 {patient_id} 的进度文件 {progress_file} 失败: {e}。重新开始处理该患者。\")\n                processed_instances = set()\n        else:\n            processed_instances = set()\n\n        try:\n            # log_message(f\"尝试加载患者 {patient_id} 的NII文件 {nii_path}\")\n            nii_img = nib.load(nii_path)\n            # log_message(f\"成功加载NII文件头部，准备读取数据\")\n            nii_data = nii_img.get_fdata(dtype=np.float32)\n            # log_message(f\"成功加载NII数据，形状: {nii_data.shape}\")\n        except Exception as e:\n            log_message(f\"!!! 错误 - 患者 {patient_id}: 无法加载NII文件 {nii_path}: {e}\")\n            # log_message(f\"详细错误: {traceback.format_exc()}\")\n            return 0, patient_id\n\n        actual_processed_count = 0 # 本次运行实际处理的切片数\n        nii_total_slices = nii_data.shape[2]\n        num_dicom_files = len(dicom_info_list)\n        \n        # log_message(f\"患者 {patient_id} 共有 {num_dicom_files} 个DICOM文件和 {nii_total_slices} 个NII切片\")\n        if num_dicom_files == 0:\n            log_message(f\"患者 {patient_id}: DICOM文件列表为空，跳过。\")\n            return 0, patient_id\n\n        for dicom_list_idx, (instance_number, dicom_path) in enumerate(dicom_info_list):\n            if str(instance_number) in processed_instances: # 检查是否已处理\n                continue\n\n            # ***** 核心修改：应用反向索引映射 *****\n            if USE_REVERSE_NIFTI_MAPPING: # 使用全局配置\n                nii_slice_idx_to_use = nii_total_slices - 1 - dicom_list_idx\n            else: # 直接映射 (如果以后想改回来)\n                nii_slice_idx_to_use = dicom_list_idx\n            # *****************************************\n\n            if not (0 <= nii_slice_idx_to_use < nii_total_slices):\n                # log_message(f\"患者 {patient_id}: 为DICOM索引 {dicom_list_idx} (Inst: {instance_number}) 计算的NIFTI索引 {nii_slice_idx_to_use} 超出范围 [0, {nii_total_slices-1}]。跳过。\")\n                continue\n            \n            # 分批逻辑似乎在 perform_offline_preprocessing 中，这里是单个 slice\n            # 检查内存可以放在外层循环（例如 perform_offline_preprocessing 中每处理一个病人后）\n            # 或者如果单个切片处理也可能导致问题，可以保留，但可能过于频繁\n\n            image_data, _ = load_dicom_slice(dicom_path) # instance_number 已从 dicom_info_list 获取\n            if image_data is None:\n                continue\n            \n            mask_data = load_multi_organ_segmentation_mask(\n                nii_data,\n                nii_slice_idx_to_use,\n                ORGAN_MAP_NII,          # 全局\n                ORGAN_CHANNEL_MAP,      # 全局\n                NUM_ORGANS,             # 全局\n                target_size_int,        # 确保是整数\n                nii_path_for_error_msg=f\"Pat_{patient_id}_DcmIdx_{dicom_list_idx}_NiiIdx_{nii_slice_idx_to_use}\", # 传递信息\n                orientation_transform_ops_local=BEST_NIFTI_ORIENTATION_TRANSFORM # 使用全局定义的变换\n            )\n\n            if mask_data is None:\n                continue\n            \n            processed_image_data = preprocess_image_for_unet(image_data, target_size_int)\n            if processed_image_data is None:\n                continue\n\n            output_filename = f\"{instance_number}.npz\" # 使用 InstanceNumber 作为文件名\n            output_file_path = os.path.join(patient_output_dir, output_filename)\n\n            try:\n                np.savez_compressed(output_file_path, image=processed_image_data, mask=mask_data)\n                actual_processed_count += 1\n                processed_instances.add(str(instance_number)) # 添加到已处理集合\n\n                if actual_processed_count > 0 and actual_processed_count % (SAVE_INTERVAL * 5) == 0: # 减少打印频率\n                    log_message(f\"患者 {patient_id}: 已处理 {actual_processed_count} 个新切片...\")\n            except Exception as e_save_npz:\n                log_message(f\"保存NPZ文件失败 {output_file_path}: {e_save_npz}\")\n        \n        # 患者所有切片处理（或尝试处理）完毕后，更新并保存该患者的进度文件\n        final_processed_this_run = len(processed_instances)\n        with open(progress_file, 'w') as f:\n            json.dump({\n                'processed_instances': list(processed_instances),\n                'processed_count': final_processed_this_run, # 总共处理的实例数\n                'completed': True, # 标记此患者处理尝试已完成\n                'completion_time': time.strftime('%Y-%m-%d %H:%M:%S')\n            }, f)\n        \n        log_message(f\"患者 {patient_id} 处理完成。本次运行新处理 {actual_processed_count} 个切片。总计已处理 {final_processed_this_run} 个切片。\")\n        \n        # 显式释放内存\n        del nii_data, nii_img\n        gc.collect()\n        \n        return final_processed_this_run, patient_id # 返回的是该患者总共完成的切片数\n\n    except Exception as e_outer:\n        log_message(f\"处理患者 {patient_id} 时发生顶层异常: {e_outer}\")\n        log_message(f\"详细错误: {traceback.format_exc()}\")\n        return 0, patient_id # 返回处理失败\n\n\ndef perform_offline_preprocessing(image_paths_map, segmentation_map, output_dir, target_size):\n    \"\"\"分批处理患者数据并保存进度\"\"\"\n    log_message(f\"开始离线预处理数据，将使用序列处理替代并行处理...\")\n    start_time = time.time()\n    \n    # 尝试从输出数据集目录恢复进度文件\n    restore_progress_files()\n\n    # 检查全局进度文件\n    global_progress_file = os.path.join(output_dir, 'global_progress.json')\n    if os.path.exists(global_progress_file):\n        try:\n            with open(global_progress_file, 'r') as f:\n                global_progress = json.load(f)\n                processed_patient_ids = set(global_progress.get('processed_patient_ids', []))\n                log_message(f\"加载全局进度：已处理 {len(processed_patient_ids)} 位患者\")\n        except Exception as e:\n            log_message(f\"读取全局进度文件失败: {e}\")\n            processed_patient_ids = set()\n    else:\n        processed_patient_ids = set()\n\n    # 获取未处理的患者列表\n    patient_ids_to_process = [pid for pid in sorted(list(image_paths_map.keys())) \n                         if pid in segmentation_map and pid not in processed_patient_ids]\n    \n    log_message(f\"需要处理 {len(patient_ids_to_process)} 位患者\")\n    \n    # 分批处理患者，每批PATIENT_BATCH_SIZE个\n    for batch_idx in range(0, len(patient_ids_to_process), PATIENT_BATCH_SIZE):\n        batch_end = min(batch_idx + PATIENT_BATCH_SIZE, len(patient_ids_to_process))\n        current_batch = patient_ids_to_process[batch_idx:batch_end]\n        \n        log_message(f\"\\n处理批次 {batch_idx//PATIENT_BATCH_SIZE + 1}/{math.ceil(len(patient_ids_to_process)/PATIENT_BATCH_SIZE)}, \"\n              f\"患者 {batch_idx}-{batch_end-1}\")\n        \n        tasks = []\n        for patient_id in current_batch:\n            task_args = (\n                patient_id,\n                image_paths_map[patient_id],\n                segmentation_map[patient_id],\n                output_dir,\n                target_size\n            )\n            tasks.append(task_args)\n\n        batch_processed_slices = 0\n        batch_processed_patients = []\n        batch_failed_patients = []\n\n        # 使用序列处理代替ProcessPoolExecutor\n        log_message(\"使用序列处理替代并行处理...\")\n        for task_idx, task in enumerate(tasks):\n            patient_id = task[0]\n            log_message(f\"开始处理患者 {task_idx+1}/{len(tasks)}: {patient_id}\")\n            \n            # 检查内存使用情况\n            if not check_memory():\n                log_message(f\"内存使用过高，跳过患者 {patient_id}\")\n                batch_failed_patients.append(patient_id)\n                continue\n                \n            try:\n                processed_count, patient_id = preprocess_and_save_data(task)\n                if processed_count > 0:\n                    batch_processed_slices += processed_count\n                    batch_processed_patients.append(patient_id)\n                    processed_patient_ids.add(patient_id)\n                    \n                    # 将处理好的患者数据复制到输出数据集目录\n                    copy_patient_data_to_output(patient_id)\n                    log_message(f\"成功处理患者 {patient_id}，共 {processed_count} 个切片\")\n                else:\n                    batch_failed_patients.append(patient_id)\n                    log_message(f\"处理患者 {patient_id} 失败，返回切片数为0\")\n            except Exception as e:\n                log_message(f\"处理患者 {patient_id} 时发生异常: {e}\")\n                log_message(f\"详细错误: {traceback.format_exc()}\")\n                batch_failed_patients.append(patient_id)\n            \n            # 每个患者处理完后立即清理内存\n            gc.collect()\n            log_message(f\"已完成 {task_idx+1}/{len(tasks)} 个患者\")\n        \n        # 每批处理完后更新全局进度并备份\n        with open(global_progress_file, 'w') as f:\n            json.dump({\n                'processed_patient_ids': list(processed_patient_ids),\n                'last_updated': time.strftime('%Y-%m-%d %H:%M:%S'),\n                'total_patients': len(image_paths_map),\n                'failed_patients': batch_failed_patients\n            }, f)\n        \n        # 备份进度文件\n        backup_progress_files()\n        \n        log_message(f\"批次 {batch_idx//PATIENT_BATCH_SIZE + 1} 完成，处理了 {len(batch_processed_patients)} 位患者，\"\n              f\"{batch_processed_slices} 个切片\")\n        \n        # 清理内存\n        gc.collect()\n\n    end_time = time.time()\n    log_message(f\"\\n预处理完成。\")\n    log_message(f\"  耗时: {end_time - start_time:.2f} 秒\")\n    log_message(f\"  成功处理患者数: {len(processed_patient_ids)} / {len(image_paths_map)}\")\n    log_message(f\"  数据保存在: {output_dir} 和 {OUTPUT_DATASET_DIR}\")\n\n    return list(processed_patient_ids)\n# 在 === 辅助函数 === 部分或主流程中\ndef visualize_alignment_check(patient_id, slice_idx_in_dicom_list, # 改为DICOM列表中的索引\n                              dicom_info_list, nii_path, target_display_size_int): # target_size -> target_display_size_int\n    \n    if not (0 <= slice_idx_in_dicom_list < len(dicom_info_list)):\n        log_message(f\"错误: DICOM列表索引 {slice_idx_in_dicom_list} 超出范围 (患者 {patient_id})\")\n        return\n\n    instance_number, dicom_path = dicom_info_list[slice_idx_in_dicom_list]\n\n    log_message(f\"可视化对齐检查 - 患者: {patient_id}, DICOM列表索引: {slice_idx_in_dicom_list}, InstanceNum: {instance_number}\")\n\n    try:\n        dicom_file = pydicom.dcmread(dicom_path)\n        image_orig = apply_voi_lut(dicom_file.pixel_array, dicom_file)\n        img_min, img_max = np.min(image_orig), np.max(image_orig)\n        image_display = (image_orig - img_min) / (img_max - img_min) if img_max > img_min else np.zeros_like(image_orig)\n        if 'PhotometricInterpretation' in dicom_file and dicom_file.PhotometricInterpretation == \"MONOCHROME1\":\n            image_display = 1.0 - image_display\n    except Exception as e:\n        log_message(f\"  加载DICOM {dicom_path} 错误: {e}\")\n        return\n\n    try:\n        nii_img = nib.load(nii_path)\n        nii_data = nii_img.get_fdata(dtype=np.float32)\n        nii_total_slices = nii_data.shape[2]\n\n        # ***** 核心修改：应用反向索引映射和方向变换 *****\n        nii_slice_idx_to_use = nii_total_slices - 1 - slice_idx_in_dicom_list # 反向映射\n        \n        if not (0 <= nii_slice_idx_to_use < nii_total_slices):\n            log_message(f\"  错误: 为DICOM索引 {slice_idx_in_dicom_list} 计算的NIFTI索引 {nii_slice_idx_to_use} 超出范围 ({nii_total_slices}片)。\")\n            return\n\n        mask_slice_raw = nii_data[:, :, nii_slice_idx_to_use]\n        mask_slice_oriented = apply_orientation_transform(mask_slice_raw, BEST_NIFTI_ORIENTATION_TRANSFORM) # 全局\n        mask_slice_int = np.round(mask_slice_oriented).astype(np.int16)\n        # ****************************************************\n\n        colors_rgb = { # Matplotlib 使用 RGB\n            'liver': [1, 0, 0],  # Red\n            'spleen': [0, 1, 0],  # Green\n            'kidney': [0, 0, 1],  # Blue\n            'bowel': [1, 1, 0]   # Yellow\n        }\n        original_mask_shape = mask_slice_int.shape\n        mask_overlay_rgb = np.zeros((original_mask_shape[0], original_mask_shape[1], 3), dtype=np.float32) # Use float for overlay\n\n        for nii_val, organ_name in ORGAN_MAP_NII.items(): # 全局\n             if organ_name in ORGAN_CHANNEL_MAP: # 确保是我们关心的器官\n                color_to_use = colors_rgb.get(organ_name, [0.5, 0.5, 0.5]) # 默认灰色\n                if organ_name == 'kidney':\n                     current_mask_bool = ((mask_slice_int == 3) | (mask_slice_int == 4))\n                else:\n                     current_mask_bool = (mask_slice_int == nii_val)\n                \n                for c in range(3):\n                    mask_overlay_rgb[current_mask_bool, c] = color_to_use[c]\n        \n    except Exception as e:\n        log_message(f\"  加载NII或生成掩码时出错 (NII路径 {nii_path}, NII索引 {nii_slice_idx_to_use}): {e}\")\n        # log_message(f\"  详细错误: {traceback.format_exc()}\")\n        return\n\n    fig, axes = plt.subplots(1, 3, figsize=(18, 6))\n    \n    # 调整DICOM和掩码以匹配显示 (例如，都resize到target_display_size_int)\n    image_display_resized = cv2.resize(image_display, (target_display_size_int, target_display_size_int), interpolation=cv2.INTER_LINEAR)\n    mask_overlay_rgb_resized = cv2.resize(mask_overlay_rgb, (target_display_size_int, target_display_size_int), interpolation=cv2.INTER_NEAREST)\n\n    axes[0].imshow(image_display_resized, cmap='gray')\n    axes[0].set_title(f\"DICOM (Idx:{slice_idx_in_dicom_list}, Inst:{instance_number})\")\n    axes[0].axis('off')\n\n    axes[1].imshow(mask_overlay_rgb_resized) # mask_overlay_rgb_resized 已经是RGB float\n    axes[1].set_title(f\"NII Mask (NII Idx:{nii_slice_idx_to_use}, Transform:{BEST_NIFTI_ORIENTATION_TRANSFORM})\")\n    axes[1].axis('off')\n    \n    image_display_rgb_resized = cv2.cvtColor((image_display_resized * 255).astype(np.uint8), cv2.COLOR_GRAY2RGB)\n    # alpha blend: image_display_rgb_resized 和 mask_overlay_rgb_resized (确保mask_overlay_rgb_resized也是0-1范围或通过alpha混合)\n    # 确保 mask_overlay_rgb_resized 的非零部分才参与混合\n    mask_for_blending = (np.sum(mask_overlay_rgb_resized, axis=2) > 0).astype(np.uint8) # 找到有颜色的区域\n    \n    blended_image = image_display_rgb_resized.copy()\n    # 只在有掩码的地方进行混合\n    alpha = 0.4\n    for r_idx in range(image_display_rgb_resized.shape[0]):\n        for c_idx in range(image_display_rgb_resized.shape[1]):\n            if mask_for_blending[r_idx, c_idx] > 0: # 如果掩码在此处有颜色\n                for channel in range(3):\n                    blended_image[r_idx, c_idx, channel] = \\\n                        (1 - alpha) * image_display_rgb_resized[r_idx, c_idx, channel] + \\\n                        alpha * (mask_overlay_rgb_resized[r_idx, c_idx, channel] * 255) # 假设mask_overlay是0-1 float\n    blended_image = np.clip(blended_image, 0, 255).astype(np.uint8)\n\n\n    axes[2].imshow(blended_image)\n    axes[2].set_title(\"Overlay\")\n    axes[2].axis('off')\n\n    # Add legend (using ORGAN_CHANNEL_MAP keys for consistency with model output)\n    legend_elements = [plt.Rectangle((0, 0), 1, 1, color=colors_rgb[org], label=org)\n                       for org in ORGAN_CHANNEL_MAP.keys() if org in colors_rgb]\n    fig.legend(handles=legend_elements, loc='lower center', ncol=len(ORGAN_CHANNEL_MAP.keys()), bbox_to_anchor=(0.5, -0.01))\n    plt.suptitle(f\"Alignment Check - Patient {patient_id}\", fontsize=16) # y调整到1.0或略大\n    plt.tight_layout(rect=[0, 0.03, 1, 0.95])\n    plt.show()\n\n# 在主流程中调用 visualize_alignment_check 时 (Cell 12 的末尾 \"--- 3. 执行可视化对齐检查 (抽样) ---\" 部分)\n# 确保传入的是 TARGET_SIZE_INT\n# visualize_alignment_check(\n#     patient_id,\n#     slice_idx,\n#     dicom_info_list,\n#     nii_path,\n#     TARGET_SIZE_INT # 使用整数尺寸\n# )\n\n# === 主执行流程 ===\nif __name__ == \"__main__\":\n    log_message(\"--- 开始预处理 Notebook ---\")\n    log_message(f\"目标图像尺寸: {IMG_SIZE}\")\n    log_message(f\"输出目录: {PREPROCESSED_DIR}\")\n    log_message(f\"输出数据集目录: {OUTPUT_DATASET_DIR}\")\n    \n    os.makedirs(PREPROCESSED_DIR, exist_ok=True)\n    os.makedirs(OUTPUT_DATASET_DIR, exist_ok=True)\n\n    log_message(\"\\n--- 1. 构建文件映射 ---\")\n    # 加载元数据\n    train_meta_path = os.path.join(META_DIR, 'train_series_meta.csv')\n    dicom_tags_path = os.path.join(META_DIR, 'train_dicom_tags.parquet') # parquet 文件路径\n\n    if not os.path.exists(train_meta_path):\n        raise FileNotFoundError(f\"找不到系列元数据文件: {train_meta_path}\")\n    \n    try:\n        log_message(f\"加载训练系列元数据: {train_meta_path}\")\n        train_series_meta = pd.read_csv(train_meta_path)\n        log_message(f\"成功加载元数据，包含 {len(train_series_meta)} 条记录\")\n    except Exception as e:\n        log_message(f\"加载元数据失败: {e}\")\n        log_message(f\"详细错误: {traceback.format_exc()}\")\n        raise SystemExit(\"无法继续预处理\")\n\n    # 创建 series_id -> patient_id 映射\n    try:\n        series_to_patient = dict(zip(\n            train_series_meta['series_id'].astype(str),\n            train_series_meta['patient_id'].astype(str)\n        ))\n        log_message(f\"创建了 {len(series_to_patient)} 个系列ID到患者ID的映射\")\n    except Exception as e:\n        log_message(f\"创建系列ID映射失败: {e}\")\n        series_to_patient = {}\n\n    # 创建 series_id -> nii_path 映射\n    series_to_nii = {}\n    try:\n        log_message(f\"查找分割文件目录: {SEGMENTATION_DIR}\")\n        segmentation_files = glob.glob(os.path.join(SEGMENTATION_DIR, \"*.nii\"))\n        # 添加对.nii.gz文件的支持\n        segmentation_files.extend(glob.glob(os.path.join(SEGMENTATION_DIR, \"*.nii.gz\")))\n        log_message(f\"发现 {len(segmentation_files)} 个 NII/NII.GZ 文件。\")\n        \n        for fpath in segmentation_files:\n            # NII 文件名通常是 series_id.nii 或 series_id.nii.gz\n            if fpath.endswith('.nii.gz'):\n                series_id = os.path.basename(fpath).replace('.nii.gz','')\n            else:\n                series_id = os.path.basename(fpath).replace('.nii','')\n                \n            if series_id in series_to_patient:\n                series_to_nii[series_id] = fpath\n                \n        log_message(f\"成功映射 {len(series_to_nii)} 个 NII 文件到有效的 series_id。\")\n    except Exception as e:\n        log_message(f\"创建NII文件映射失败: {e}\")\n        log_message(f\"详细错误: {traceback.format_exc()}\")\n\n    # 加载 DICOM tags (如果可用)\n    dicom_tags_df = None\n    if os.path.exists(dicom_tags_path):\n        log_message(\"加载 DICOM tags (parquet)...\")\n        try:\n            dicom_tags_df = pd.read_parquet(dicom_tags_path)\n            # 预处理 tags DataFrame (确保提取 series_id)\n            if 'PatientID' in dicom_tags_df.columns:\n                dicom_tags_df['PatientID'] = dicom_tags_df['PatientID'].astype(str)\n            if 'SeriesInstanceUID' in dicom_tags_df.columns:\n                # 提取 series_id (假设它在 UID 的倒数第二部分，根据实际情况调整)\n                dicom_tags_df['series_id_extracted'] = dicom_tags_df['SeriesInstanceUID'].astype(str) # 假设 UID 就是 series_id\n                dicom_tags_df = dicom_tags_df.dropna(subset=['series_id_extracted'])\n            log_message(f\"DICOM tags 加载完成，包含 {len(dicom_tags_df)} 条记录。\")\n        except Exception as e:\n            log_message(f\"加载 DICOM tags 失败: {e}. 将不使用 tags 进行排序。\")\n            log_message(f\"详细错误: {traceback.format_exc()}\")\n            dicom_tags_df = None\n    else:\n        log_message(\"未找到 DICOM tags 文件，将仅依赖DICOM头或文件名排序。\")\n\n    log_message(\"\\n--- 2. 关联图像和分割文件 ---\")\n    # {patient_id: [(inst_num1, path1), (inst_num2, path2), ...]}\n    image_paths_map_full = {}\n    # {patient_id: nii_path}\n    segmentation_map_full = {}\n    valid_patients_found = []\n\n    try:\n        all_patient_ids = sorted(train_series_meta['patient_id'].astype(str).unique())\n        log_message(f\"总共有 {len(all_patient_ids)} 个独特的患者 ID 在元数据中。\")\n\n        # 遍历所有在元数据中有记录的患者\n        for patient_id in all_patient_ids:\n            try:\n                log_message(f\"处理患者 {patient_id} 的数据...\")\n                patient_series_ids = train_series_meta[train_series_meta['patient_id'].astype(str) == patient_id]['series_id'].astype(str).tolist()\n\n                if not patient_series_ids:\n                    log_message(f\"患者 {patient_id} 没有关联的系列ID，跳过\")\n                    continue\n\n                found_valid_series_for_patient = False\n                # 尝试为每个患者找到一个有效的 (图像存在 + NII 存在) 的序列\n                for series_id in patient_series_ids:\n                    series_img_path = os.path.join(TRAIN_IMAGES_DIR, patient_id, series_id)\n                    nii_path = series_to_nii.get(series_id)\n\n                    # 检查图像目录和NII文件是否都存在\n                    if os.path.isdir(series_img_path) and nii_path:\n                        log_message(f\"找到患者 {patient_id} 的有效系列: {series_id}\")\n                        # 获取并排序该序列的 DICOM 文件信息 (InstanceNumber, Path)\n                        dicom_info_list = get_dicom_files_dict(patient_id, series_id, dicom_tags_df)\n\n                        if dicom_info_list: # 确保序列中有有效的 DICOM 文件\n                            log_message(f\"患者 {patient_id} 系列 {series_id} 有 {len(dicom_info_list)} 个DICOM文件\")\n                            image_paths_map_full[patient_id] = dicom_info_list\n                            segmentation_map_full[patient_id] = nii_path\n                            valid_patients_found.append(patient_id)\n                            found_valid_series_for_patient = True\n                            break # 每个患者只使用找到的第一个有效 series\n                        else:\n                            log_message(f\"患者 {patient_id} 系列 {series_id} 没有有效的DICOM文件\")\n                    \n                if not found_valid_series_for_patient:\n                    log_message(f\"患者 {patient_id} 没有找到有效的系列\")\n            except Exception as e:\n                log_message(f\"处理患者 {patient_id} 时出错: {e}\")\n                log_message(f\"详细错误: {traceback.format_exc()}\")\n    except Exception as e:\n        log_message(f\"关联图像和分割文件时出错: {e}\")\n        log_message(f\"详细错误: {traceback.format_exc()}\")\n\n    # 去重并排序最终的患者 ID 列表\n    final_patient_ids = sorted(list(set(valid_patients_found)))\n    log_message(f\"\\n成功映射了 {len(final_patient_ids)} 位患者的图像和对应分割文件。\")\n\n    if not final_patient_ids:\n        raise SystemExit(\"错误：未能找到任何包含有效图像序列及对应NII文件的患者。停止执行。\")\n\n    # 过滤字典，只保留有效患者的数据\n    image_paths_map_final = {pid: image_paths_map_full[pid] for pid in final_patient_ids}\n    segmentation_map_final = {pid: segmentation_map_full[pid] for pid in final_patient_ids}\n\n    log_message(\"\\n--- 3. 执行可视化对齐检查 (抽样) ---\")\n    num_patients_to_check = 1  # 减少到1个以节省时间和内存\n    num_slices_per_patient = 1  # 减少到1个以节省时间和内存\n\n    if len(final_patient_ids) > 0:\n        try:\n            # Ensure we don't request more patients than available\n            num_patients_to_check = min(num_patients_to_check, len(final_patient_ids))\n\n            # Select random patients\n            sample_patient_ids = random.sample(final_patient_ids, num_patients_to_check)\n\n            for patient_id in sample_patient_ids:\n                dicom_info_list = image_paths_map_final.get(patient_id)\n                nii_path = segmentation_map_final.get(patient_id)\n\n                if not dicom_info_list or not nii_path:\n                    log_message(f\"警告: 样本患者 {patient_id} 缺少 DICOM 或 NII 信息，跳过可视化。\")\n                    continue\n\n                num_available_slices = len(dicom_info_list)\n                if num_available_slices == 0:\n                     log_message(f\"警告: 样本患者 {patient_id} DICOM 列表为空，跳过可视化。\")\n                     continue\n\n                # Ensure we don't request more slices than available\n                num_slices_to_check_this_patient = min(num_slices_per_patient, num_available_slices)\n\n                # Select random slice indices (from the list index 0 to n-1)\n                # Avoid checking only the very first/last slices\n                middle_slice_idx = num_available_slices // 2\n                slice_indices_to_check = [middle_slice_idx]  # 只使用中间切片以简化\n\n                for slice_idx in slice_indices_to_check:\n                    try:\n                        log_message(f\"尝试可视化患者 {patient_id} 的切片 {slice_idx}\")\n                        visualize_alignment_check(\n                            patient_id,\n                            slice_idx,\n                            dicom_info_list,\n                            nii_path,\n                            TARGET_SIZE_INT # Pass target size if needed by mask func internally (though visualization uses original)\n                        )\n                        log_message(f\"成功可视化患者 {patient_id} 的切片 {slice_idx}\")\n                    except Exception as e:\n                        log_message(f\"可视化患者 {patient_id} 切片 {slice_idx} 时出错: {e}\")\n                        log_message(f\"详细错误: {traceback.format_exc()}\")\n                \n                log_message(\"-\" * 30) # Separator between patients\n                \n                # 每个患者后清理内存\n                gc.collect()\n        except Exception as e:\n            log_message(f\"可视化对齐检查时出错: {e}\")\n            log_message(f\"详细错误: {traceback.format_exc()}\")\n            log_message(\"继续执行预处理，跳过可视化\")\n    else:\n        log_message(\"没有找到有效的患者进行可视化检查。\")\n\n    log_message(\"--- 可视化对齐检查完成 ---\")\n\n    # --- NOW START THE FULL PREPROCESSING ---\n    log_message(\"\\n--- 4. 执行离线数据预处理 ---\")\n    \n    try:\n        # 执行预处理并保存到工作目录和输出数据集目录\n        processed_patients = perform_offline_preprocessing(\n            image_paths_map_final,\n            segmentation_map_final,\n            PREPROCESSED_DIR,\n            TARGET_SIZE\n        )\n        \n        # 统计预处理结果\n        log_message(f\"\\n预处理统计:\")\n        log_message(f\"  总患者数: {len(final_patient_ids)}\")\n        log_message(f\"  成功处理患者数: {len(processed_patients)}\")\n    except Exception as e:\n        log_message(f\"执行离线预处理时发生严重错误: {e}\")\n        log_message(f\"详细错误: {traceback.format_exc()}\")\n        processed_patients = []\n    \n    # 确保所有数据都已复制到输出数据集目录\n    log_message(\"\\n--- 5. 确保数据已保存到输出数据集 ---\")\n    for patient_id in processed_patients:\n        try:\n            copy_success = copy_patient_data_to_output(patient_id)\n            if copy_success:\n                log_message(f\"患者 {patient_id} 数据已成功复制到输出目录\")\n            else:\n                log_message(f\"警告: 患者 {patient_id} 数据复制失败\")\n        except Exception as e:\n            log_message(f\"复制患者 {patient_id} 数据时出错: {e}\")\n    \n    # 最终备份进度文件\n    try:\n        backup_progress_files()\n    except Exception as e:\n        log_message(f\"备份进度文件时出错: {e}\")\n    \n    # 输出一些有用的统计信息\n    try:\n        total_slices = 0\n        total_file_size = 0\n        for patient_id in processed_patients:\n            patient_dir = os.path.join(PREPROCESSED_DIR, str(patient_id))\n            if os.path.exists(patient_dir):\n                npz_files = glob.glob(os.path.join(patient_dir, \"*.npz\"))\n                total_slices += len(npz_files)\n                for npz_file in npz_files:\n                    total_file_size += os.path.getsize(npz_file)\n        \n        log_message(f\"  总切片数: {total_slices}\")\n        log_message(f\"  总文件大小: {total_file_size / (1024*1024):.2f} MB\")\n    except Exception as e:\n        log_message(f\"计算统计信息时出错: {e}\")\n    \n    # 如果使用Kaggle API，可以添加提交数据集的命令\n    log_message(\"\\n要将预处理数据作为数据集保存，请确保在笔记本设置中添加了输出数据集。\")\n    log_message(\"数据已保存到: \" + OUTPUT_DATASET_DIR)\n\n    log_message(\"\\n--- 预处理 Notebook 执行完毕 ---\")\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-04-27T15:17:10.529425Z","iopub.execute_input":"2025-04-27T15:17:10.530153Z","iopub.status.idle":"2025-04-27T15:17:10.645554Z","shell.execute_reply.started":"2025-04-27T15:17:10.530114Z","shell.execute_reply":"2025-04-27T15:17:10.644069Z"}},"outputs":[],"execution_count":null}]}