在医学影像分析领域,如何将复杂的病理图像转化为可量化、可分析的计算机数据,是连接临床诊断与人工智能模型的关键一步。对于肺腺癌这种异质性强的肿瘤,其生长模式(如贴壁型、腺泡型、乳头型、微乳头型、实体型等)的精确识别与空间定位,对预后评估和治疗方案制定至关重要。然而,传统的人工阅片耗时费力,且存在主观差异。本文探讨一种经典的计算机视觉技术——词袋模型在解决这一问题上的应用。我们将构建一个完整的流程,从病理切片图像预处理开始,到特征提取、视觉词典构建、图像编码,最终实现肺腺癌不同生长模式的空间映射。通过这篇文章,你将理解词袋模型在医学图像分析中的核心思想,并能动手实现一个基础但完整的分析管道,为后续更复杂的深度学习模型应用打下坚实基础。
1. 理解词袋模型及其在医学图像分析中的价值
词袋模型最初源于自然语言处理,用于将文档表示为词汇出现的频率向量,而忽略语法和词序。在计算机视觉中,这一思想被迁移为“视觉词袋”:将图像视为由大量局部特征(视觉单词)构成的“文档”,通过统计这些视觉单词的出现频率来表征整幅图像或图像区域的内容。
1.1 为什么选择词袋模型处理肺腺癌病理图像?
肺腺癌病理切片是高分辨率、信息密集的复杂图像。直接使用原始像素值进行分析,会面临维度灾难和语义信息缺失的问题。词袋模型提供了一种中层语义表示方法:
- 局部性:它关注图像中局部区域(如细胞核、腺体结构、间质)的特征,这与病理医生观察局部形态特征的诊断逻辑相符。
- 不变性:通过选择合适的局部特征描述子(如SIFT、SURF),可以对图像的尺度、旋转、轻微形变保持一定的鲁棒性。
- 可量化:将图像表示为固定长度的特征向量(视觉词频直方图),便于输入到标准的机器学习分类器(如SVM、随机森林)中进行训练和预测。
- 可解释性:每个视觉单词对应一种特定的局部纹理或模式,通过分析哪些视觉单词在某种生长模式中频繁出现,可以为病理诊断提供可解释的线索。
1.2 视觉词袋模型的核心工作流程
一个标准的视觉词袋模型管道包含以下关键步骤,我们将围绕肺腺癌图像展开:
- 图像预处理与分块:将整张WSI(全切片数字图像)或ROI(感兴趣区域)切割成更小的图像块(Patches),作为后续处理的基本单元。
- 局部特征提取:从每个图像块中提取密集或关键点处的局部特征描述子。
- 视觉词典构建:使用所有训练图像块提取的特征描述子,通过聚类算法(如K-Means)生成一个包含K个聚类中心的集合,每个中心即为一个“视觉单词”。
- 特征编码:对于任何一个新的图像块,将其提取的局部特征映射到视觉词典中,统计每个视觉单词出现的频率,生成一个K维的直方图向量,即该图像块的词袋表示。
- 空间映射:对整张WSI进行滑窗分块,对每个块进行编码,得到其词袋向量,然后使用预先训练好的分类器预测每个块的生长模式类别,最后将预测结果映射回原始图像位置,生成一张“生长模式空间分布图”。
2. 环境准备与数据组织
在开始编码前,需要搭建一个包含必要库的Python环境,并按照医学图像分析的规范组织数据。
2.1 依赖库安装与版本确认
建议使用Conda或venv创建独立的Python环境。核心依赖库如下:
# 创建并激活环境(以conda为例) conda create -n bow_lung_cancer python=3.8 conda activate bow_lung_cancer # 安装核心科学计算和图像处理库 pip install numpy scipy scikit-learn opencv-python-headless pillow # 安装用于更高级特征提取的可选库(如SIFT,OpenCV contrib版本) # 注意:OpenCV-Python默认不包含SIFT,需安装contrib版本 pip install opencv-contrib-python # 安装用于图像显示和进度提示的库 pip install matplotlib tqdm注意:OpenCV的SIFT、SURF等算法在较新版本中已移至
opencv-contrib-python包中,且可能受专利限制。在学术研究场景下可使用,生产部署需留意许可问题。也可考虑使用ORB(免费)作为替代特征。
2.2 病理图像数据组织
假设我们已获得一批标注好的肺腺癌WSI图像,并已由病理专家标注了不同生长模式的区域。数据应组织成如下结构:
lung_adenocarcinoma_data/ ├── wsi_images/ │ ├── case_001.tif │ ├── case_002.tif │ └── ... ├── annotations/ │ ├── case_001/ │ │ ├── lepidic_region_1.png # 二值化掩码图像,白色区域代表贴壁型 │ │ ├── acinar_region_1.png # 代表腺泡型 │ │ └── ... │ ├── case_002/ │ └── ... └── patch_dataset/ # (后续步骤生成) ├── train/ │ ├── lepidic/ │ ├── acinar/ │ ├── papillary/ │ └── ... └── val/实际操作中,我们需要一个脚本从WSI和标注掩码中提取出小的图像块。以下是一个简化的提取示例:
import os from PIL import Image import numpy as np import cv2 def extract_patches_from_mask(wsi_path, mask_dir, patch_size=256, stride=128, output_root='./patch_dataset'): """ 根据标注掩码提取对应区域的图像块。 Args: wsi_path: WSI文件路径。 mask_dir: 该病例标注掩码文件夹路径。 patch_size: 提取的图像块大小(正方形)。 stride: 滑动窗口步长。 output_root: 提取块保存的根目录。 """ # 1. 加载WSI (这里简化用OpenCV读取,真实WSI需用openslide或tifffile) # 注意:真实WSI巨大,需使用多分辨率读取,此处仅为流程演示。 wsi_img = cv2.imread(wsi_path) if wsi_img is None: print(f"无法读取WSI: {wsi_path}") return # 2. 遍历所有掩码文件 for mask_file in os.listdir(mask_dir): if not mask_file.endswith('.png'): continue # 解析生长模式类别,假设文件名格式为“{pattern}_region_{id}.png” pattern_name = mask_file.split('_')[0] # 例如 'lepidic' mask_path = os.path.join(mask_dir, mask_file) mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) if mask is None: continue # 3. 创建输出子目录 output_dir = os.path.join(output_root, 'train', pattern_name) os.makedirs(output_dir, exist_ok=True) # 4. 滑动窗口提取 height, width = mask.shape patch_id = 0 for y in range(0, height - patch_size + 1, stride): for x in range(0, width - patch_size + 1, stride): # 检查掩码对应区域是否大部分为标注区域(例如>50%) mask_patch = mask[y:y+patch_size, x:x+patch_size] if np.sum(mask_patch > 0) > (patch_size * patch_size * 0.5): # 提取对应WSI区域的图像块 img_patch = wsi_img[y:y+patch_size, x:x+patch_size, :] if img_patch.shape[:2] != (patch_size, patch_size): continue # 边界处理 # 保存图像块 save_path = os.path.join(output_dir, f'{pattern_name}_{patch_id:06d}.png') cv2.imwrite(save_path, img_patch) patch_id += 1 print(f"从 {mask_file} 提取了 {patch_id} 个 {pattern_name} 图像块。") # 示例调用 extract_patches_from_mask('./lung_adenocarcinoma_data/wsi_images/case_001.tif', './lung_adenocarcinoma_data/annotations/case_001/', patch_size=256, stride=128)3. 构建视觉词袋模型管道
数据准备就绪后,我们开始实现BoVW的核心三步:特征提取、词典构建、图像编码。
3.1 局部特征提取:从图像块到描述子集合
我们选择尺度不变特征变换(SIFT)作为局部特征描述子。SIFT能检测关键点并生成128维的描述向量,对图像缩放、旋转、亮度变化保持较好稳定性。
import cv2 import numpy as np from tqdm import tqdm import os def extract_sift_features_from_patches(data_root, max_samples_per_class=500): """ 从所有图像块中提取SIFT特征。 Args: data_root: patch_dataset/train 的路径。 max_samples_per_class: 每类最多采样多少图像块,防止内存溢出。 Returns: all_descriptors: 所有提取到的SIFT描述子列表。 patch_labels: 每个描述子对应的图像块类别标签(用于后续分析,词典构建不需要)。 """ all_descriptors = [] patch_labels = [] class_names = sorted([d for d in os.listdir(data_root) if os.path.isdir(os.path.join(data_root, d))]) sift = cv2.SIFT_create() # 创建SIFT检测器 for label_idx, class_name in enumerate(class_names): class_dir = os.path.join(data_root, class_name) image_files = [f for f in os.listdir(class_dir) if f.endswith(('.png', '.jpg'))] # 随机采样,避免数据过多 if len(image_files) > max_samples_per_class: import random image_files = random.sample(image_files, max_samples_per_class) print(f"正在处理类别: {class_name}, 图像数: {len(image_files)}") for img_file in tqdm(image_files): img_path = os.path.join(class_dir, img_file) img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # SIFT通常处理灰度图 if img is None: continue # 检测关键点并计算描述子 keypoints, descriptors = sift.detectAndCompute(img, None) if descriptors is not None: all_descriptors.append(descriptors) # 记录这个描述子集合来自哪个类别的哪个图像块(可选,用于分析) # 这里简单记录类别索引 patch_labels.extend([label_idx] * len(descriptors)) # 将所有描述子垂直堆叠成一个大的N x 128矩阵 if all_descriptors: all_descriptors = np.vstack(all_descriptors) else: all_descriptors = np.array([]) print(f"特征提取完成。总共提取到 {len(all_descriptors)} 个SIFT描述子。") return all_descriptors, np.array(patch_labels) # 使用示例 train_data_root = './lung_adenocarcinoma_data/patch_dataset/train' all_descriptors, patch_labels = extract_sift_features_from_patches(train_data_root, max_samples_per_class=300)3.2 视觉词典构建:使用K-Means聚类生成视觉单词
将从所有训练图像中提取的海量SIFT描述子进行聚类,聚类中心即为视觉单词。词典大小K是一个关键超参数,影响模型的表达能力和计算复杂度。
from sklearn.cluster import MiniBatchKMeans import joblib # 用于保存模型 def build_visual_vocabulary(descriptors, vocabulary_size=500, batch_size=1000, random_state=42): """ 使用K-Means聚类构建视觉词典。 Args: descriptors: 所有SIFT描述子,形状为 (N, 128)。 vocabulary_size: 视觉词典大小,即聚类中心数K。 batch_size: MiniBatchKMeans的批大小。 random_state: 随机种子,保证可复现。 Returns: kmeans: 训练好的KMeans模型,其聚类中心即为视觉单词。 """ if len(descriptors) < vocabulary_size: print(f"警告:描述子数量({len(descriptors)})少于词典大小({vocabulary_size}),将调整词典大小。") vocabulary_size = len(descriptors) // 2 print(f"开始构建视觉词典,使用 {len(descriptors)} 个描述子,词典大小 K={vocabulary_size}...") # 使用MiniBatchKMeans加速大规模数据聚类 kmeans = MiniBatchKMeans(n_clusters=vocabulary_size, batch_size=batch_size, init='k-means++', n_init=3, max_iter=100, random_state=random_state, verbose=1) kmeans.fit(descriptors) print("视觉词典构建完成。") # 保存词典模型 os.makedirs('./models', exist_ok=True) joblib.dump(kmeans, './models/visual_vocabulary_k500.pkl') print("视觉词典模型已保存到 ./models/visual_vocabulary_k500.pkl") return kmeans # 使用示例 visual_vocab = build_visual_vocabulary(all_descriptors, vocabulary_size=500)3.3 图像编码:将图像块表示为词频直方图
对于一个新图像块,提取其SIFT特征,然后为每个特征找到最近的视觉单词(聚类中心),最后统计所有特征所属视觉单词的频次,归一化后得到该图像块的词袋向量。
def encode_image_as_bow_vector(img_gray, sift_detector, kmeans_model): """ 将单张灰度图像编码为词袋向量。 Args: img_gray: 灰度图像,numpy数组。 sift_detector: 已初始化的SIFT检测器。 kmeans_model: 训练好的KMeans模型(视觉词典)。 Returns: bow_vector: 归一化的词频直方图,形状为 (K, )。 """ # 1. 提取SIFT特征 keypoints, descriptors = sift_detector.detectAndCompute(img_gray, None) if descriptors is None: # 如果图像没有检测到特征,返回零向量 return np.zeros(kmeans_model.n_clusters) # 2. 为每个描述子找到最近的视觉单词(聚类中心) visual_word_ids = kmeans_model.predict(descriptors) # 形状: (num_descriptors,) # 3. 统计词频 bow_vector = np.bincount(visual_word_ids, minlength=kmeans_model.n_clusters) # 4. 归一化(L1或L2归一化,消除图像块大小的影响) bow_vector = bow_vector.astype(np.float32) # L1归一化:使向量各元素之和为1 if np.sum(bow_vector) > 0: bow_vector /= np.sum(bow_vector) # 也可以使用L2归一化: bow_vector = bow_vector / np.linalg.norm(bow_vector) return bow_vector def create_bow_dataset(data_root, sift_detector, kmeans_model, max_samples=None): """ 将整个数据集(图像块)转换为词袋向量数据集。 Args: data_root: 包含按类别分文件夹的图像块根目录。 sift_detector: SIFT检测器。 kmeans_model: 视觉词典模型。 max_samples: 每类最大样本数(用于控制数据集大小)。 Returns: X: 词袋向量矩阵,形状 (n_samples, vocabulary_size)。 y: 类别标签向量,形状 (n_samples,)。 filenames: 对应的图像文件名列表。 """ X = [] y = [] filenames = [] class_names = sorted([d for d in os.listdir(data_root) if os.path.isdir(os.path.join(data_root, d))]) class_to_idx = {name: idx for idx, name in enumerate(class_names)} for class_name in class_names: class_dir = os.path.join(data_root, class_name) image_files = [f for f in os.listdir(class_dir) if f.endswith(('.png', '.jpg'))] if max_samples and len(image_files) > max_samples: import random image_files = random.sample(image_files, max_samples) print(f"编码类别: {class_name}") for img_file in tqdm(image_files): img_path = os.path.join(class_dir, img_file) img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: continue bow_vec = encode_image_as_bow_vector(img, sift_detector, kmeans_model) X.append(bow_vec) y.append(class_to_idx[class_name]) filenames.append(img_file) return np.array(X), np.array(y), filenames # 使用示例 sift = cv2.SIFT_create() visual_vocab = joblib.load('./models/visual_vocabulary_k500.pkl') # 加载已保存的词典 X_train, y_train, train_files = create_bow_dataset('./lung_adenocarcinoma_data/patch_dataset/train', sift, visual_vocab, max_samples=200) print(f"训练集词袋向量形状: {X_train.shape}, 标签形状: {y_train.shape}")4. 训练分类器与空间映射
有了词袋向量表示,我们就可以训练一个分类器来识别每个图像块的生长模式,进而对整个WSI进行空间映射。
4.1 训练生长模式分类器
这里我们使用支持向量机(SVM)作为分类器,它在高维稀疏特征上通常表现良好。
from sklearn.svm import SVC from sklearn.model_selection import cross_val_score, GridSearchCV from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline def train_svm_classifier(X_train, y_train): """ 训练一个SVM分类器,并尝试简单的超参数调优。 """ # 构建管道:标准化 + SVM # 标准化可以提升SVM性能,尤其是使用RBF核时 pipeline = Pipeline([ ('scaler', StandardScaler()), ('svm', SVC(kernel='linear', probability=True, random_state=42)) # 线性核,可计算概率 ]) # 定义超参数网格(简化示例) param_grid = { 'svm__C': [0.1, 1, 10], # 正则化参数 # 'svm__kernel': ['linear', 'rbf'], # 也可以尝试RBF核 # 'svm__gamma': ['scale', 'auto'] # RBF核参数 } print("开始网格搜索寻找最佳SVM参数...") grid_search = GridSearchCV(pipeline, param_grid, cv=3, scoring='accuracy', n_jobs=-1, verbose=1) grid_search.fit(X_train, y_train) print(f"最佳参数: {grid_search.best_params_}") print(f"最佳交叉验证准确率: {grid_search.best_score_:.4f}") best_model = grid_search.best_estimator_ # 保存训练好的分类器 joblib.dump(best_model, './models/svm_growth_pattern_classifier.pkl') print("分类器模型已保存。") return best_model # 使用示例 classifier = train_svm_classifier(X_train, y_train)4.2 实现整张WSI的空间映射
这是最终目标:输入一张新的WSI,输出一张彩色编码图,显示每个小区域预测的生长模式。
import matplotlib.pyplot as plt from matplotlib import colors from matplotlib.patches import Patch def predict_whole_slide(wsi_path, classifier, visual_vocab, sift_detector, patch_size=256, stride=128, output_map_path='./spatial_map.png'): """ 对整张WSI进行滑窗预测,生成空间映射图。 Args: wsi_path: WSI文件路径。 classifier: 训练好的分类器管道(包含标准化器)。 visual_vocab: 视觉词典模型。 sift_detector: SIFT检测器。 patch_size: 预测用的图像块大小。 stride: 滑窗步长。 output_map_path: 预测结果图保存路径。 Returns: prediction_map: 预测类别索引的矩阵。 """ # 1. 加载WSI (简化版,实际需用openslide处理金字塔层级) wsi_img = cv2.imread(wsi_path) if wsi_img is None: print("无法读取WSI。") return None # 转换为灰度图用于特征提取 wsi_gray = cv2.cvtColor(wsi_img, cv2.COLOR_BGR2GRAY) height, width = wsi_gray.shape # 2. 初始化预测图 map_height = (height - patch_size) // stride + 1 map_width = (width - patch_size) // stride + 1 prediction_map = np.full((map_height, map_width), -1, dtype=np.int8) # -1表示未预测 # 3. 滑窗预测 print(f"开始滑窗预测,地图尺寸: {map_height} x {map_width}") for i in tqdm(range(map_height)): y = i * stride for j in range(map_width): x = j * stride # 提取图像块 patch = wsi_gray[y:y+patch_size, x:x+patch_size] if patch.shape != (patch_size, patch_size): continue # 忽略边界不完整的块 # 编码为词袋向量 bow_vec = encode_image_as_bow_vector(patch, sift_detector, visual_vocab) bow_vec = bow_vec.reshape(1, -1) # 变为(1, n_features) # 预测 # classifier是管道,会自动进行标准化 pred_class = classifier.predict(bow_vec)[0] prediction_map[i, j] = pred_class # 4. 可视化预测图 class_names = ['lepidic', 'acinar', 'papillary', 'micropapillary', 'solid'] # 示例类别 # 为每个类别分配一个颜色 cmap = colors.ListedColormap(['lightgreen', 'gold', 'lightcoral', 'plum', 'skyblue']) bounds = range(len(class_names)+1) norm = colors.BoundaryNorm(bounds, cmap.N) plt.figure(figsize=(12, 10)) plt.imshow(prediction_map, cmap=cmap, norm=norm, interpolation='nearest') # 创建图例 legend_elements = [Patch(facecolor=cmap(i), edgecolor='k', label=class_names[i]) for i in range(len(class_names))] plt.legend(handles=legend_elements, bbox_to_anchor=(1.05, 1), loc='upper left') plt.title('Spatial Mapping of Lung Adenocarcinoma Growth Patterns') plt.axis('off') plt.tight_layout() plt.savefig(output_map_path, dpi=300, bbox_inches='tight') plt.show() print(f"空间映射图已保存至: {output_map_path}") return prediction_map # 使用示例(假设已加载模型) sift = cv2.SIFT_create() visual_vocab = joblib.load('./models/visual_vocabulary_k500.pkl') classifier = joblib.load('./models/svm_growth_pattern_classifier.pkl') pred_map = predict_whole_slide('./lung_adenocarcinoma_data/wsi_images/case_003.tif', classifier, visual_vocab, sift, patch_size=256, stride=128)5. 模型评估、常见问题与优化方向
完成基础流程后,必须对模型性能进行评估,并理解实践中可能遇到的问题。
5.1 模型性能评估与验证
在独立验证集上评估分类器的性能是必要步骤。
from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay # 1. 在验证集上生成词袋向量 X_val, y_val, val_files = create_bow_dataset('./lung_adenocarcinoma_data/patch_dataset/val', sift, visual_vocab, max_samples=100) # 2. 预测并评估 y_pred = classifier.predict(X_val) print("分类报告:") print(classification_report(y_val, y_pred, target_names=['lepidic', 'acinar', 'papillary', 'micropapillary', 'solid'])) # 3. 绘制混淆矩阵 cm = confusion_matrix(y_val, y_pred) disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=['lepidic', 'acinar', 'papillary', 'micropapillary', 'solid']) disp.plot(cmap=plt.cm.Blues, values_format='d') plt.title('Confusion Matrix on Validation Set') plt.tight_layout() plt.show()5.2 常见问题与排查路径
在实现和应用视觉词袋模型时,你可能会遇到以下典型问题:
| 问题现象 | 可能原因 | 检查与解决思路 |
|---|---|---|
| 特征提取数量为0 | 图像对比度太低、图像块内容过于均匀(如空白背景)、SIFT参数不匹配。 | 1. 可视化几个图像块,确认是否有丰富纹理。 2. 调整SIFT参数,如 contrastThreshold、edgeThreshold。3. 尝试其他特征,如ORB、HOG,或使用密集采样特征。 |
| 词典构建内存不足 | 提取的描述子数量过多(数百万),K-Means聚类耗内存。 | 1. 增加max_samples_per_class限制,或对描述子进行随机下采样。2. 使用 MiniBatchKMeans并减小batch_size。3. 考虑使用更小的 vocabulary_size。 |
| 分类准确率很低 | 特征区分度不够、词典大小不合适、分类器参数不佳、数据不平衡、图像块未对齐。 | 1. 检查混淆矩阵,看是否特定类别混淆。 2. 调整 vocabulary_size(通常尝试100, 500, 1000, 2000)。3. 对SVM进行更细致的网格搜索(C, gamma, kernel)。 4. 检查训练集每类样本数量,考虑使用类别权重或过采样。 5. 确认图像块提取是否准确对准了病理结构。 |
| 空间映射图斑驳、噪声大 | 步长stride太小,导致相邻块预测结果不一致;分类器置信度低。 | 1. 增大stride,或对预测结果进行后处理(如多数投票滤波)。2. 使用 classifier.predict_proba()获取预测概率,只显示高置信度(如>0.7)的区域。3. 考虑在更高层级的图像金字塔上进行预测,再上采样。 |
| 处理速度太慢 | WSI分辨率极高,滑窗数量巨大;SIFT计算耗时。 | 1. 在低倍率(如5x或10x)的WSI层级上进行预测。 2. 使用更快的特征,如ORB。 3. 增大 stride,减少预测块数量。4. 使用多进程并行处理不同的图像区域。 |
5.3 优化方向与最佳实践
基础流程跑通后,可以从以下几个方向提升系统性能与实用性:
- 特征工程优化:
- 特征选择:除了SIFT,可以尝试结合颜色特征(如颜色直方图)、纹理特征(如LBP、GLCM)或深度特征(使用预训练CNN的中间层输出)。
- 特征编码升级:词袋模型是硬分配(一个特征只属于一个词)。可以改用软分配(如VLAD、Fisher Vector),或使用稀疏编码,能获得更具判别力的表示。
- 分类器与后处理:
- 集成学习:结合多个不同特征或不同分类器(如SVM、随机森林、XGBoost)的结果。
- 空间上下文建模:在预测时,考虑相邻图像块类别的一致性,使用条件随机场(CRF)等图模型进行平滑优化。
- 多尺度分析:在不同放大倍率下提取特征并融合,同时捕获局部细胞形态和全局组织结构信息。
- 工程化与生产部署:
- 使用专业WSI库:生产环境必须使用
openslide或libvips来高效读取WSI的多分辨率金字塔数据,避免将整张数十GB的图像读入内存。 - 管道并行化:将特征提取、编码、预测等步骤设计为流水线,利用多核CPU或GPU加速。
- 结果存储与可视化:将预测结果(如每个块的坐标和类别)存储为GeoJSON或专门的病理图像标注格式(如ASAP的XML),便于在专业查看器中叠加显示。
- 使用专业WSI库:生产环境必须使用
- 与深度学习结合:
- 视觉词袋模型可视为一个浅层的、可解释的特征提取器。可以将其输出(词袋向量)与深度学习模型(如CNN)提取的深度特征进行融合,作为混合模型的一部分。
- 更现代的做法是直接使用全卷积网络(FCN)或U-Net等分割网络进行端到端的像素级分类,但这需要大量像素级标注数据。视觉词袋模型在数据量有限时仍是一个强有力的基线模型。
通过上述流程,你不仅实现了一个用于肺腺癌生长模式空间映射的视觉词袋模型,更重要的是掌握了将经典计算机视觉方法应用于复杂医学图像分析问题的完整方法论。这套流程的模块化设计(特征提取、词典学习、编码、分类)允许你灵活替换其中任何一个组件,以适应不同的数据特点和任务需求,为后续探索更先进的算法奠定了坚实的基础。