LiON 论文学习笔记:多期相融合、Query 与实例匹配
读 LiON 时,我最先卡住的是 Query 到底是什么。这篇笔记从多期相特征融合讲起,再用一个配对例子串起 Query、匈牙利匹配、训练损失和全局分类。
阅读全文 »Archive
全部博客文章
读 LiON 时,我最先卡住的是 Query 到底是什么。这篇笔记从多期相特征融合讲起,再用一个配对例子串起 Query、匈牙利匹配、训练损失和全局分类。
阅读全文 »这份项目参考 Java 集合的基本操作,用 C++17 实现七种常见容器,并提供 CMake 工程、演示程序和 GoogleTest 测试。 完整项目下载 下载 C++ 容器项目(ZIP,约 876 KiB) 压缩包包含: 七种容器的完整 C++ 源码; CMakeLists.txt 构建文件; GoogleTest 测试代码及官方 v1.15.2 源码包; 演示程序、中文 README 和测试报告。 完整项目可离线构建测试,前提是本机已经安装 CMake、C++ 编译器和相应构建工具。 实现了哪些容器 容器 基本操作与实现方式 动态数组 DynamicArray<T> 连续存储、自动扩容、按下标访问、插入和删除 双向链表 LinkedList<T> 头尾插入删除、按位置访问、删除匹配元素 栈 Stack<T> 基于动态数组,支持 push、pop、top 队列 Queue<T> 基于链表,支持 enqueue、dequeue、front 二叉搜索树 BinarySearchTree<T> 查找、插入、删除、中序遍历、最小和最大键 B 树 BTree<T, t> 多路平衡搜索,包含结点分裂、借位、合并和根收缩 B+ 树 BPlusTree<T, t> 数据保存在叶子,维护叶子链表,额外支持闭区间范围查询 数组、链表、栈和队列允许重复元素;三种树采用不重复键的集合语义。树的 insert 和 erase 返回操作是否实际改变了集合。 Java 的 TreeSet 和 TreeMap 基于红黑树,与这里实现的普通二叉搜索树、B 树和 B+ 树不同。项目参考的是基本容器操作,而非复刻 Java 标准库的全部接口。 编译和运行 环境要求为支持 C++17 的 GCC、Clang 或 MSVC,以及 CMake 3.16 及以上版本。在解压后的 cpp-containers 目录执行: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug cmake --build build --config Debug --parallel ctest --test-dir build -C Debug --output-on-failure 上面的 ctest --test-dir 写法要求 CMake 3.20 及以上版本;使用 3.16~3.19 时,进入 build 目录后执行 ctest -C Debug --output-on-failure。
阅读全文 »破天荒想写点学习笔记,实则是数据跑太慢了,找事情打发时间,简单记录一下两周的计算机网络学习内容。 这份笔记按“先理解在解决什么问题,再记概念和公式”的顺序整理。后面补充的是物理层与数据链路层的常见基础内容,具体考试范围还是以老师讲授和教材为准。 阅读导航: 开始之前:先把分层这件事理顺 一、物理层 二、数据链路层 开始之前:先把分层这件事理顺 首先明确一点: 计算机网络并不是只有一种分层方式。 OSI 参考模型分为七层,TCP/IP 体系结构通常按四层描述,而课程中常用的是方便讲解的五层模型。 这里统一采用五层模型,从上到下是: 应用层 应用程序之间交换信息 ↓ 运输层 为应用进程之间的通信提供服务,也常称“传输层” ↓ 网络层 让分组能够跨越不同网络到达目的地 ↓ 数据链路层 在一段链路上组织和传送帧 ↓ 物理层 通过实际信号传输比特流 通过老师讲的例子,计算机网络可以类比快递的实现。发送方把应用数据逐层交给下一层,下层根据需要添加控制信息,这个过程叫 封装 ;接收方反过来逐层处理,这个过程叫 解封装 。 不过,“一层一层贴 tag”只是帮助理解的比喻:不是每层都一定添加同样形式的首部,物理层主要负责把比特表示成可传输的信号。 接下来记录物理层与数据链路层。 话说为什么我们从下往上学,好多网课都是从上往下,hyw。 一、物理层 1. 物理层负责什么 计算机中的数据可以表示成 0 和 1,但网线、光纤和无线空间并不能直接传输抽象的数字,需要把它们转换成实际信号。 传输介质 传输的信号 双绞线等铜缆 电信号 光纤 光信号 无线空间 电磁波信号 通信过程可以简化为: 发送方的比特 → 转换为信号 → 经过传输介质 → 接收方恢复比特 物理层解决的是: 怎样在传输介质上传输比特流。 它通常不关心这些比特是聊天消息、图片,还是视频。 2. 物理层接口的四个特性 物理层不仅涉及信号,也规定通信设备与传输介质之间的接口。 特性 规定什么 直观例子 机械特性 接口的形状、尺寸、引脚数量等 插头是什么形状 电气特性 电压、阻抗等电气参数 什么电平表示什么状态 功能特性 各条线路或引脚的功能 哪个引脚负责发送 过程特性 各种操作的步骤和顺序 怎样建立、进行和结束传输 可以记成: 长什么样、用什么信号、各部分做什么、按什么顺序操作。 3. 数据、信号和码元 数据 :需要传递的信息。 信号 :数据在传输过程中的物理表现。 码元 :数字通信中,在一个符号时间内发送的基本信号单位。 信号可以分为下面两类。信号与系统还在追我。
阅读全文 »从 patch 热力图到 U-Net:肝脏识别进度记录 0. 背景 好久没更新博客了。 上一次还在看 nnU-Net 的论文,想着先把数据空间、预处理和输入格式这些基础问题弄明白。后来真正开始做肝脏识别,路线也从一开始的 patch 分类和粗 ROI,慢慢走到了现在的 U-Net 像素级分割。 这篇不打算写成很完整的教程,主要想记一下: 为什么换模型、换完以后做了什么,以及现在到底做到哪一步了。 先说明一下,目前实际训练的是自己写的轻量 2D U-Net baseline,并不是直接运行官方 nnU-Net。完整代码放在公开仓库里:liver-segmentation code。 1. 换模型前,旧方案卡在哪里 换 U-Net 之前,做的是一个监督式对比学习的 patch 分类器。 它会判断一个 96 × 96 的 CT patch 是 liver 还是 non-liver,然后在完整 CT 上滑窗,把每个 patch 的概率重新叠回三维空间,最后得到一张肝脏热力图。 这条路线不是完全没用。它确实能大致找到肝脏,也很适合验证“模型到底有没有学到肝脏区域”。但越往后做,问题越明显: 它学的是“这个 patch 里像不像有肝脏”,不是“每一个像素是不是肝脏”; 滑窗概率回填以后,边缘容易变厚、变糊; 模型只看局部,容易把附近灰度相似的器官和软组织一起圈进来; 阈值一改,Recall 和 Precision 就会明显拉扯; 最大连通域、闭运算、矢状面过滤都能清掉一些小块,但更像是在补救结果。 旧方案最后选中的是 epoch 40、阈值 0.80。在 4 个完整验证 CT 上,平均 Recall 是 0.9504,Precision 是 0.7266,Dice 是 0.8220。 召回率已经不算低,但误检还是很多。这个时候继续给后处理叠规则,收益越来越有限。后来想明白,问题不只是模型容量不够,而是任务本身没有对齐: 想要的是一张像素级 mask,却一直在训练一个 patch 级分类器。 所以决定换成 U-Net。 2. 先把 U-Net 的最小闭环跑通 这次没有一上来就堆很复杂的结构,而是先写了一个比较小的 2D U-Net: 单通道轴向 CT 输入; 4 层 encoder 和 decoder; skip connection; GroupNorm; base channels 为 16; 总参数量约 194 万。 输出也从“这个 patch 是不是肝脏”,变成了“这一张切片上每个像素属于肝脏的概率”。
阅读全文 »nnU-Net 模型学习笔记 0. 背景 这篇主要是对论文 nnU-Net: Self-adapting Framework for U-Net-Based Medical Image Segmentation 的学习记录。 一开始看到 nnU-Net 的时候,其实很容易把它理解成“一个更厉害的 U-Net 变体”。但是读完论文之后发现,它真正重要的地方好像并不是提出了什么特别炫的新网络结构,而是把医学图像分割里一堆容易被忽略的工程细节系统化了。 论文里也直接把 nnU-Net 解释成 no-new-Net,这个名字还挺有意思的。它大概想表达的是: 不一定非要发明一个新的网络结构,先把 U-Net 该做好的事情全部做好。 这点对我还挺有提醒意义的。因为之前做 DICOM 预处理、期相配准、nnU-Net 输入格式的时候,我也经常会下意识觉得“模型才是核心”,但实际上数据怎么裁剪、怎么重采样、spacing 怎么统一、标签怎么插值、patch 怎么取,这些东西如果没处理好,后面的模型可能只是看起来很努力。 1. nnU-Net 想解决什么问题 医学图像分割里面,U-Net 本身已经是一个很经典的结构了。 但是问题在于,不同数据集之间差异非常大: 有的是 CT,有的是 MRI; 有的是 2D 图像,有的是 3D 体数据; 有的 spacing 比较均匀,有的 z 轴很厚; 有的图像范围很大,比如肝脏、肺; 有的目标很小,比如海马体、胰腺病灶; 有的数据集病例很多,有的数据集病例很少。 所以同样是 U-Net,直接拿来用并不一定能跑出好结果。 如果每来一个新任务,都靠人工去调网络深度、patch size、batch size、归一化方式、数据增强方式、推理策略,那整个流程就会非常依赖经验,也很难保证可复现。 nnU-Net 的思路就是: 给定一个新的医学图像分割任务,尽量自动分析数据集特征,然后自动生成一套适合这个任务的 U-Net 训练和推理方案。 所以它不是单纯的一个模型,而更像是一个完整的自动化分割框架。 2. 核心理解:它不追求“新”,而追求“配好” 论文里有一个观点我觉得挺关键: 很多所谓的新结构,在没有把 baseline 充分调好的情况下,看起来确实能提升性能。但如果基础 U-Net 已经被认真配置过,很多花哨结构带来的提升可能就没有那么明显了。 所以 nnU-Net 没有采用 residual connection、dense connection、attention mechanism 这些当时常见的改造,而是只在原始 U-Net 上做了很小的改动: 使用 leaky ReLU; 使用 instance normalization;
阅读全文 »多期相 CT 批量配准和 QC 记录 0. 背景 之前已经写过单个 fixed 和 moving 的配准脚本,但真正开始处理病例的时候才发现,单个脚本只能证明“这件事情理论上可以做”,不能证明“这件事情可以批量稳定地做”。 因为一个病例里面往往不止两个序列,可能有平扫、动脉期、门静脉期、延迟期,也可能夹杂 localizer、lung、std、薄层、厚层之类的序列。更麻烦的是,即使它们都来自同一个病例,也不能默认它们的切片编号就是一一对应的。 所以这次想做的是: 批量读取同一个病例下的多个 DICOM series,自动筛选、配准、评价配准效果,并输出可以作为 nnU-Net 多通道输入的 NIfTI 文件。 完整代码见 GitHub:medical-image-registration-scripts 1. 一开始的想法 最初的思路其实很朴素: 读取一个病例下面所有 seriesXXX 文件夹; 自动选一个序列作为 fixed image; 其余序列作为 moving image; 先通过 DICOM header 做第一轮筛选; 对筛选通过的 moving 做刚性配准和重采样; 配准后计算一组无标签 QC 指标; 根据 QC 结果打 PASS、WARNING、FAIL; 只把合格的序列放入最终多通道; 最后对保留下来的通道做共同区域裁剪; 输出 nnU-Net 的 imagesTr 格式。 看起来非常顺理成章,甚至有点过于顺理成章。 但是医学图像处理很多时候就是这样:流程图画出来的时候像一条笔直的路,真正跑起来的时候才发现中间到处是坑。 2. fixed image 的选择 脚本里暂时采用了一个比较直接的规则: 选择切片数最多的 seriesXXX 作为 fixed image。 这样做的原因是,切片数最多的序列通常扫描范围更完整,更适合作为统一空间的参考。 当然这不是一个绝对正确的规则。更严谨的做法应该结合: 序列描述; 扫描范围; 图像质量; 老师指定的标准期相; 是否为薄层或厚层重建; 是否包含目标器官区域。 但在当前阶段,为了先把批量流程跑通,先用“切片数最多”作为自动选择规则。 3. 配准前 header 筛选 在真正做配准之前,脚本会先比较 fixed 和 moving 的空间信息。 主要检查:
阅读全文 »如何处理乱码DICOM文件夹 0. 背景 其实本来没想做这个的,只是发现每次去找series_number的时候,我都要一个一个用ITK打开dicom文件,笔者属实是懒于应付这种重复机械的工作,所以写了个批量重命名的脚本虽然是生成的并且实际上花费的时间早就够我一个一个点进去看了。 每一个乱码文件夹里面可能是一组DICOM序列,比如 CT 的某个扫描序列、localizer 定位像、增强期图像等。问题就是这些文件夹名称没有任何意义,所以写了个python脚本用来批量读取每个DICOM序列的header信息,并且根据series_number和series_description自动重命名文件夹。 完整代码见 Github:allocate-dicom-renamer 1. 脚本目标 这个脚本主要完成三件事: 批量遍历所有病例文件夹; 读取每个 DICOM 序列的 header 信息; 根据 SeriesNumber 和是否为 localizer 自动重命名文件夹。 最终效果类似: 乱码文件夹1 -> series001 乱码文件夹2 -> series002 乱码文件夹3 -> localizer003 2. 用到的库 import os import json import pydicom import SimpleITK as sitk 其中: os:遍历文件夹、重命名文件夹; json:保存重命名日志; pydicom:读取 DICOM header; SimpleITK:作为备用方式读取 DICOM 信息。 安装依赖: pip install pydicom SimpleITK 3. 主要参数 DATA_DIR = "/你的/data/路径" DRY_RUN = False MIN_SERIES_IMAGES = 10 DATA_DIR 表示数据总目录,下面应该是多个病例文件夹。 data/ ├── case1/ ├── case2/ └── case3/ DRY_RUN 这是一个安全开关。 DRY_RUN = True 表示只预览,不真正改名。
阅读全文 »如何具体实现registration 0. 心得 一开始其实根本没理解配准是具体需要做什么,只是一味的vibe coding,什么重采样,刚性配准什么的,最后生成出来的图像全是噪声,第二周感觉把这些放一起就没问题了,结果惊奇的发现人体直接错位了,锁骨来到了人体的中间地方,经过重新认真严谨思考,才意识到配准的真正含义,接下来是对具体配准过程的学习笔记。 完整代码见 GitHub:registration-scripts 1. 任务目标 当前任务是多期相 CT 图像配准。 对于同一个病例,可能存在多个期相或序列,例如: 平扫期 动脉期 门静脉期 延迟期 std5mm lung5mm 这些序列的原始 DICOM 可能存在: 切片数量不同 扫描起始位置不同 spacing 不同 origin 不同 扫描范围不同 呼吸状态导致器官位置轻微变化 因此,不能直接认为不同期相中相同编号的 DICOM 切片对应同一个身体位置。 当前目标是: 以一个期相作为 fixed image,将其他期相作为 moving image,通过配准和重采样,使不同期相在统一空间中具有一一对应的切片关系。 最终输出为 nnU-Net 多通道输入格式。 2. 大体流程 当前采用的整体流程为: 读取同一病例下的 DICOM 文件; 根据 SeriesInstanceUID 区分不同序列; 排除 localizer、scout、切片数过少、不同部位的序列; 选择一个序列作为 fixed image; 其他序列作为 moving image; 对 moving image 进行配准; 将 moving image 重采样到 fixed image 的空间; 根据共同有效扫描范围进行统一裁剪; 检查同编号切片是否对应同一部位; 保存为 nnU-Net 多通道 NIfTI 格式。 简化表示为: fixed image ↓ 提供统一空间标准 moving image ↓ 配准到 fixed image ↓ 重采样到 fixed image 空间
阅读全文 »医学图像配准学习笔记 这周继续研究期相配准。。 1.图像配准定义 根据笔者粗浅的理解,配准就是通过空间变换将 moving image 变换到fixed image上,使两个图像上中的公共点一一对应。 目前我的任务就是将同一个病例中不同 DICOM 序列的图像统一到同一个空间坐标系中,使它们具有一致的: spacing:体素间距 origin:原点坐标 direction:方向矩阵 size:图像尺寸 最终目标是让不同序列可以作为 nnU-Net 的多通道输入,因为这些通道必须在空间上对应,否则模型学到的多序列信息就会错位。 2.图像配准应用场景 这一部分就不必多言,因为笔者本身做的项目也就只和图像识别有关。 3. 当前任务的配准类型定位 虽然图像配准有很多分类方式,例如按模态、对象、变换类型等分类,但对当前任务来说,不需要展开全部分类。 当前任务已经比较明确: 应用领域:医学图像配准 图像类型:CT 图像 配准对象:同一病例内部不同期相 配准类型:多期相 CT 配准 图像维度:3D 到 3D 配准 目标用途:统一不同期相图像空间,使其适配 nnU-Net 多通道输入 因此,当前任务可以概括为: 同一病例多期相 CT 图像之间的 3D 医学图像配准。 在这个任务中,重点不是讨论各种配准分类,而是解决以下问题: 如何选择 fixed image 和 moving image; 如何让不同期相的图像在空间上对齐; 如何保证配准后同编号切片对应同一解剖位置; 如何处理不同期相扫描范围不一致的问题; 如何将处理结果转换为 nnU-Net 可用的多通道输入格式。 从方法选择上看,当前任务主要关注: 重采样 Resampling 刚性配准 Rigid Registration 仿射配准 Affine Registration 必要时考虑非刚性配准 Deformable Registration 其中,当前阶段优先考虑: 刚性配准 + 重采样 + 共同有效区域裁剪 原因是刚性配准可以校正不同期相之间的整体平移和旋转,同时不会改变器官和病灶的真实形态。对于医学图像分割前处理来说,这种方法相对稳定、安全,也更适合作为批量化处理流程的基础。 如果刚性配准后仍然存在整体尺度差异,可以进一步尝试仿射配准;如果存在明显局部形变,再谨慎考虑非刚性配准。 4.图像配准过程 4.1 配准中的核心概念 4.1.1 Fixed Image fixed image 是基准图像,提供统一的空间标准,包括: spacing origin
阅读全文 »这是博客的第一篇文章。 现在这个仓库已经变成 Hugo 项目:以后你只需要在 content/posts/ 目录里新增 Markdown 文件,推送到 GitHub 后,GitHub Actions 会自动构建并发布到 GitHub Pages。 本地新建文章可以运行: hugo new content/posts/my-new-post.md 本地预览可以运行: hugo server -D 然后打开 http://localhost:1313/。 题外话: 哎也真是一种感慨吧,好像就是去年的这个时间点,看到高中同学做了个个人网站,一时心血来潮也想整一个,但是由于笔者的技术属实过于低下,搞了半天也只弄了个半成品,当时记得连个git都整了半天,想尽一切办法,不过也忘了最后是怎么弄的。记得那个时候还是o4mini?不过现在在gpt的辅佐下估计制作这个个人网页应该问题也不大。 确实感慨,去年这个时候还在努力打oi也只是假努力,每周还坚持打cf和atcoder,不过现在和这些东西也越来越远了,虽然说自己也不知道接下来的路该往哪里走,但日子总得一天一天过吧,to be postive。
阅读全文 »