☰
图像切割与特征提取实战:从大图到有效特征的关键步骤
2026/10/17 16:45:55 网站建设 项目流程

说句实在话,图像类项目的成败,往往不是死在模型结构上,而是死在这些最基础的数据环节上。这第10篇,我终于把主题从表格数据转到图像来了。今天要聊的“图像切割与特征提取”,属于数据预处理和特征工程里最容易翻车、也最值得抠细节的板块。很多人拿到的图像不是网上现成的小尺寸图库,而是相机采集的高分辨率大图——工业零件、遥感影像、医学切片、街景监控,几千万像素是常态。这时候你要是把原图直接喂给模型,显存第一个不同意;你要是暴力缩放成224×224,那些细小目标和关键纹理基本就废了。所以怎么做切割、切完之后怎么提取特征,是每个视觉项目都躲不开的第一道坎。

这篇文章适合两类人:一类是刚接触计算机视觉、想知道图像数据该怎么下手的初学者;另一类是模型已经能跑通、但准确率和鲁棒性一直上不去的工程师。我会结合实际项目经验,把切割策略、特征设计、常见坑一次讲透。

1. 图像切割:为什么要动刀,怎么动刀才不伤数据

1.1 切割在图像项目里的三个真实价值

你可能会想,切图不就是把大图裁小吗?有什么好讲的。实际项目里,这一步的核心价值至少有三个,而且每个都直接关系到你的模型能不能训练起来、能不能训好。

第一个价值是适配算力。工业相机的图像动不动就是6000×4000像素,一张单通道灰度图按uint8算就要24MB左右,三通道RGB直接翻三倍。你以batch size 32往里塞,光是输入数据就是几个GB,GPU显存直接爆炸。就算你勉强塞进去了,训练时的数据读取和预处理也会变成瓶颈,GPU吃不满,整个训练流程慢得像蜗牛。切块之后,比如切成512×512,一张大图变成几十个小块,显存压力瞬间缓解。

第二个价值是保留小目标信息。这个更致命。很多任务里,关键目标在整幅图中只占很小一块面积。比如一张零件图上有一条几十像素宽的划痕,你把整张图缩放到224×224,这条划痕可能就变成了一两个像素的模糊噪声,什么特征都提取不出来。切块相当于让模型用原始分辨率去观察局部区域,“一整张图看不清”变成了“每一块都能看清细节”,这是切割最本质的收益。

第三个价值是样本量扩充。一个数据集中如果有1000张大图,切成16块就是16000个样本,切成49块就是49000个样本。对深度学习或者对数据量敏感的模型来说,这等于免费多了一批训练数据。当然,样本量变大不代表样本独立,这里面的坑我后面会专门讲。

1.2 三种常用切割策略与适用场景

我平时用到的切分方式主要有三种,它们不是互斥的,很多时候要组合着用。

第一种是固定网格切分。就是把大图按行列均匀分成固定大小的块,比如2048×2048的图按512切成4×4共16块。优点是实现最简单、覆盖完整,不会漏掉任何区域;缺点是如果目标恰好落在切分边界上,目标会被硬生生劈成两半,两块图里各留一半,特征都不完整。这种切法适合目标分布相对均匀、图幅尺寸规整的场景,比如把整幅遥感影像分成标准瓦片。

第二种是滑动窗口切分。设定窗口大小和滑动步长,窗口按步长在图上移动。关键参数就是步长与窗口大小的比值。举个例子,2048×2048图像,窗口512,步长256,那x方向和y方向各能得到7个起点位置,一共49个patch。相比网格切法的16块,重叠率50%让样本量直接翻了3倍多。重叠的意义在于:目标如果恰好跨在边界上,相邻窗口会把它完整包含进去,至少有一张patch能看清全貌。步长越小重叠越大,目标被切碎的概率越低,但样本量膨胀也越厉害,训练耗时同步上涨。

第三种是随机裁剪。在训练阶段,从原图像上随机取一个子区域,配合随机翻转、旋转、亮度扰动一起用,这是深度学习中非常常见的数据增强手段。它的优势是每次epoch看到的样本都在变化,模型泛化能力更好;缺点是非确定性的覆盖,推理时需要用全图或多窗口预测来保证不漏检。

三种策略的取舍我整理成了表:

策略覆盖完整性样本量增幅适用场景
固定网格切分完整但边界无保护适中目标分布均匀、图幅规整
滑动窗口切分完整且边界有重叠保护较大目标大小不定、对召回要求高
随机裁剪训练时不保证完整灵活控制数据增强、样本量充足时

实际项目里我常用的组合是:训练阶段用滑动窗口切块,重叠率设在50%上下;如果数据仍然不够,再叠加随机裁剪做增强。

1.3 切分时必须守住的三个底线

切图看似简单,但我见过太多项目在这里埋雷,后面怎么调都救不回来。三个底线必须守住。

底线一是标签必须跟着patch走。大图的标签不能直接复制给每一块。比如大图上有缺陷,切成49块,可能只有3块包含这个缺陷,你把缺陷标签给全部49块,模型直接学出一堆噪声。正确做法是切割时记录patch在大图中的坐标,与标注框求交叠,交叠比例超过阈值的patch才继承正样本标签,或者按实际包含目标的比例来标。

底线二是切分之后立即检查样本分布。大图上目标占比可能只有1%,切块后如果不做筛选,你的训练集里99%都是纯背景patch,正负样本比例极端不平衡,模型会倾向于把所有东西都预测成负样本。先统计一下各类别patch数量,不平就要在特征工程之前处理掉。

底线三是按原图划分数据集,而不是按patch划分。同一张大图切出来的patch之间有很强的相关性,如果随机把这些patch分进训练集和测试集,模型等于提前见过“同一场景的亲戚”,验证分数虚高,上线后立刻现原形。正确做法是先把大图按比例分成训练、验证、测试组,再对组内图像切块,保证同一个来源的大图不会同时出现在两个集合里。

2. 特征提取:从切割图里捞什么,怎么捞才划算

2.1 不训练网络也能用的三组手工特征

切完图之后,每个patch还是一张像素图,直接扔给模型不是不行,但如果你要走的是经典机器学习路线,或者想在深度学习之前先建一个baseline,手工特征是最快的路径。我在做图像分类、缺陷检测项目时,最常捞的是三组特征:颜色(灰度)特征、纹理特征、形状特征。

颜色特征最简单的做法是直方图。要注意的是,直接在RGB空间做直方图对光照变化非常敏感,同一个物体打光不同,RGB值可能差出一大截。更稳的做法是把图像转到HSV或LAB空间,对H通道或ab通道做统计,把色相和亮度剥离开。灰度图更简单,直接统计灰度直方图、均值、标准差,成本极低,信息量却不小。

纹理特征里,LBP(局部二值模式)和GLCM(灰度共生矩阵)是我最常用的两个。LBP的基本思路很朴素:把一个像素和它的邻居比较,邻居值比中心大记1,否则记0,一圈下来得到一个二进制数,这个数字就描述了局部的纹理模式。统计整张图里所有模式的出现频率,得到直方图,就是LBP特征。GLCM则统计两个相距一定距离的像素在不同灰度组合下出现的次数,再从这个矩阵抽象出对比度、能量、均匀性、相关性等标量。这两个特征擅长抓“表面质感”,比如金属拉丝、木头纹路、布料织法,这类人类觉得“手感不同”但颜色相近的区域,恰恰是它们的用武之地。

形状特征的代表是HOG(方向梯度直方图)。思路是先把图像分成小格子,在每个格子里统计梯度方向的分布,再把邻近格子的直方图拼接起来。梯度大的地方往往就是边缘和轮廓,HOG等于把“长了什么样、轮廓清不清楚”数值化。早期行人检测就是靠HOG+SVM撑起来的,到今天用在零件轮廓、字符识别这类场景依然有效。

这三组特征不是互斥的,实际项目里我通常把它们拼起来用。特征之间互补性越强,分类器的上限越高。

2.2 手工特征里最容易被忽视的维度与量纲问题

手工特征拼起来有个直接的麻烦:维度爆炸。一个patch上,LBP直方图256维,GLCM五个统计量,灰度统计四五项,HOG按常见参数轻松上千维。加在一起一个patch轻松有几千维,而你的有效样本可能只有几千到几万。高维度小样本,直接喂线性模型或者SVM,过拟合几乎是必然的。

所以流程上我是这么走的。第一步先做低方差过滤:那些几乎所有样本上取值都差不多的特征列,对区分类别没有贡献,直接删掉。第二步做标准化:颜色直方图的取值可能是几百,GLCM的对比度可能是零点几,HOG的值可能到几千,这些量纲差异会让距离类模型完全被大数值特征主导。常见做法是z-score标准化,把每列变成均值0、方差1。第三步做PCA降维:保留95%方差对应的主成分数量,把几千维压到几十上百维。这一步在去掉共线性冗余的同时,也让后续分类器更稳。

这里有一个特别常见的错误我必须单独拎出来说:标准化和PCA的拟合只能用训练集,验证集和测试集要复用训练集拟合好的转换器来transform。有人图省事,把全部数据一起fit,测试集的信息就在这一步泄漏进了训练过程,验证分数虚高没参考意义。

2.3 什么时候别折腾手工特征,直接上深度特征

手工特征虽然稳定,但天花板确实存在。复杂的语义信息——比如“这个缺陷是焊接时留下的气孔”还是“是磕碰出来的凹痕”——靠直方图和梯度很难描述清楚。

这种情况下,深度特征才是正解。做法也很简单:拿一个在ImageNet上预训练过的CNN模型,比如ResNet18或ResNet50,把最后用于分类的全连接层去掉,只保留前面的卷积特征提取部分。输入一张224×224的图,输出一个几百维的向量,这个向量就是模型理解的“图像语义压缩包”。你不再需要手工设计任何特征,需要做的只是把每张patch套进模型过一遍,拿到特征向量,存成npy,后面该训练什么分类器就训练什么分类器。

我在项目里会先看数据量和任务复杂度。数据量只有几千张、类别差异又比较表象(比如不同颜色的零件),手工特征往往更快更稳。数据量达到几万张、类别差异很抽象(比如不同类型的产品缺陷),或者场景里有大量光照和角度变化,深度特征的优势就非常明显了。两者也不冲突,深度特征为主,再拼接上灰度均值方差这类基础统计特征,偶尔会有惊喜。

3. 实操:在一块表面缺陷检测数据上走通切割+特征提取

3.1 案例背景与整体流程

我拿一个做过的表面缺陷检测项目举例,细节做了脱敏,但流程完全真实。背景是某工厂要对零件表面做自动质检,相机采集到的灰度大图尺寸为2048×2048,标签分三类:正常、划痕、表面污渍。缺陷在整图中占比很小,直接缩小整图训练不现实,所以整个流程定为:大图切割生成patch,对每个patch提取特征,用特征矩阵训练分类器,完成检测。

流程拆开就是“读图→切块→特征提取→标准化→降维→分类训练→评估”,看着平平无奇,但每一步都在前面讲的坑里蹚过一遍。下面直接上代码。

3.2 图像切割实现与边界处理

切割我选了滑动窗口,窗口大小512,步长256,重叠率50%。这个组合怎么定的?一是512×512在显卡上可以开一个不算小的batch,训练效率能接受;二是步长256给了足够重叠,细小划痕跨块被切碎的概率大幅降低。基础切分用OpenCV就能写:

import cv2 import numpy as np def split_into_patches(image, patch_size=512, stride=256): h, w = image.shape[:2] patches = [] coords = [] for y in range(0, h - patch_size + 1, stride): for x in range(0, w - patch_size + 1, stride): patches.append(image[y:y + patch_size, x:x + patch_size]) coords.append((x, y)) return patches, coords img = cv2.imread("part_surface.png", cv2.IMREAD_GRAYSCALE) patches, coords = split_into_patches(img) print(f"共生成 {len(patches)} 个patch") # 2048x2048 图像,窗口512步长256,理论上是 7x7 = 49 个

这个写法在图像尺寸刚好匹配时没问题,但真实场景里图像尺寸经常是2047、2049这种“差一点”的怪数字。for range到不了最后一行/列,尾部像素就被丢弃了。我实际生产里会换成while循环,保证最后一个窗口紧贴边界收尾,宁可尾部多一次重叠,也不能丢失图像内容。

还有一个细节:2048×2048、窗口512、步长256,x方向能取到的起始点是0,256,512,768,1024,1280,1536,一共7个位置,7×7得到49个patch。如果你用固定网格512无重叠,只能得到16个patch。重叠切法让样本量翻了三倍还多,对于小样本项目这是一个非常可观的增幅。

3.3 特征提取与特征矩阵构建

特征提取我选了LBP直方图、GLCM统计特征、灰度与梯度统计三组。LBP参数上,我直接用24个采样点、半径3,uniform模式。这里有个细节要提醒:uniform模式下LBP的取值只有0到P+1,也就是0到25,共26个可能值,所以直方图bins要设成n_points + 2,而不是想当然的16或256,否则高段位的模式会被直接丢掉。

from skimage.feature import local_binary_pattern def lbp_histogram(patch, n_points=24, radius=3): lbp = local_binary_pattern(patch, P=n_points, R=radius, method="uniform") n_bins = n_points + 2 hist, _ = np.histogram(lbp.ravel(), bins=n_bins, range=(0, n_bins)) hist = hist.astype(np.float32) hist /= (hist.sum() + 1e-6) return hist

GLCM部分,灰度共生矩阵对灰度级数量很敏感,直接拿0到255的原始灰度做矩阵,矩阵会非常大而且稀疏,统计量反而失稳。我的做法是先量化到32个灰度级,距离取1个像素,方向取0度、45度、90度、135度四个方向,然后对每个方向的统计量取均值。

from skimage.feature import graycomatrix, graycoprops def glcm_features(patch, levels=32): patch_q = (patch / 256.0 * levels).astype(np.uint8) glcm = graycomatrix(patch_q, distances=[1], angles=[0, np.pi/4, np.pi/2, 3*np.pi/4], levels=levels, symmetric=True, normed=True) props = [] for prop in ["contrast", "dissimilarity", "homogeneity", "energy", "correlation"]: vals = graycoprops(glcm, prop) props.append(vals.mean()) return np.array(props, dtype=np.float32)

最后是一组最朴素的灰度与梯度统计特征。灰度均值和标准差捕捉整体明暗,Sobel梯度幅值的均值与标准差捕捉边缘强度。别小看这几个数字,它们经常能在分类器里贡献意想不到的区分度。

def gray_stats(patch): mean = patch.mean() std = patch.std() gx = cv2.Sobel(patch, cv2.CV_64F, 1, 0, ksize=3) gy = cv2.Sobel(patch, cv2.CV_64F, 0, 1, ksize=3) grad = np.sqrt(gx ** 2 + gy ** 2) return np.array([mean, std, grad.mean(), grad.std()], dtype=np.float32)

三个函数提取的结果拼成一行特征:

def build_feature_matrix(patches): rows = [] for p in patches: lbp = lbp_histogram(p) glcm = glcm_features(p) stats = gray_stats(p) row = np.concatenate([lbp, glcm, stats]) rows.append(row) return np.vstack(rows) X = build_feature_matrix(patches) print("特征矩阵形状:", X.shape) # 26 + 5 + 4 = 35 维

这里你可能会说:不是说特征几千维吗?这个例子我刻意选了较轻量的特征组合,35维对于小样本也足够了。实战中如果叠加HOG,维度会明显变大,到时候就必须加上PCA。

3.4 标准化、降维与分类评估

特征矩阵建好,后面的处理链是标准三步:标准化、PCA、分类器。为了展示信息泄漏的坑,代码里特意把scaler和pca只fit在训练集上:

from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.svm import LinearSVC from sklearn.model_selection import train_test_split # 假设 X 是特征矩阵,y 是对应的patch标签 X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42) scaler = StandardScaler().fit(X_train) X_train_s = scaler.transform(X_train) X_val_s = scaler.transform(X_val) # 关键:只准transform,不准fit pca = PCA(n_components=0.95).fit(X_train_s) X_train_pca = pca.transform(X_train_s) X_val_pca = pca.transform(X_val_s) clf = LinearSVC(max_iter=5000) clf.fit(X_train_pca, y_train) acc = clf.score(X_val_pca, y_val) print(f"验证集准确率: {acc:.4f}")

分类器选了LinearSVC,原因是特征维度不高、样本量有限时,线性SVM在小样本分类上非常稳,而且训练速度快。如果你的数据量更大、特征和类别关系更复杂,换成随机森林或梯度提升树也可以,但前处理逻辑完全不变。跑完之后我还会看一眼混淆矩阵,特别是划痕和污渍这两类是否互相混淆,这往往能指引下一步该怎么补特征。

4. 图像特征工程翻车实录:高频问题与排查清单

4.1 切完片后样本不平衡反而更严重

切块后最典型的问题就是正负样本比例急剧恶化。原始大图里缺陷面积可能只占1%,按512窗口、50%重叠切完,含缺陷的patch可能只有几个,剩下全是背景。模型训练出来永远只预测“正常”,准确率看着还有99%——因为正常类本身就占99%,一点意义都没有。

我常用的解法有两路。一路从数据层入手:对包含缺陷的patch做扩充,旋转、翻转、小幅缩放、调整对比度都行,让模型在同一个缺陷上看到更多变化;同时对纯正常的patch做下采样,把比例控制到能接受的范围。另一路从模型层入手:改用带类别权重的损失函数,或者简单一点,在SVM或树模型里把class_weight设成balanced。实践经验是,两个手段一起用,比单用一个稳得多。

4.2 目标跨块被切碎,模型怎么都认不出

滑动窗口重叠50%之后,跨块问题好了很多,但依然存在。划痕这种细长形态的目标,可能横跨四个patch,每个patch里只有一小段弧线,单看哪一块都像噪声。我踩过这个坑后的改进方式是:训练时只要该缺陷与当前patch有实质性交叠,就把这个patch标成正样本,让模型见过“半个缺陷”的样子;推理时用更小的步长再做一次滑动窗口,让同一个缺陷出现在多个窗口里,最后把多个窗口的预测结果做投票或平均。这相当于把单个patch的判断升级成“多窗口协同判断”,误检率明显下降。

4.3 特征维度太高导致过拟合

手工特征最诱惑人的地方就是可以不断往上堆,颜色直方图加完加纹理,纹理加完加关键点特征,最后一个patch上万维。但维度一旦超过样本量能支撑的范围,分类器就会开始背样本而不是学规律,训练集分数极高、验证集崩盘。

我的排查思路是分三步。第一步看每个特征列的方差,方差接近0的直接删,这种列在样本间没有区分信息。第二步看相关性,把相关性极高的特征列保留其中之一,LBP直方图的相邻bin之间有时高度冗余,GLCM的dissimilarity和contrast也常常接近线性相关。第三步PCA压到90%到95%的方差保留量。如果你用的是树模型,也可以直接看feature_importance,砍掉那些重要性长期垫底的特征。

4.4 测评指标虚高:数据泄漏藏在前处理里

这个坑最隐蔽,也最坑。前面我反复强调Scaler和PCA只能在训练集上fit,但更隐蔽的是数据集划分层面的泄漏。假如一张大图切成49个patch,随机按patch划分训练集和测试集,那么同一张大图的patch很可能同时出现在两边。这些patch共享同一光照、同一个背景、甚至同一个缺陷形态,相当于模型在测试时遇到了“训练时见过的相似场景”,指标自然漂亮。但如果换一批全新的大图,效果立刻打回原形。

我的做法是:周一上来就按大图路径做分组划分,把每张大图对应的所有patch绑定在一起,要么全在训练集,要么全在测试集。有时候更严格一点,按采集批次切分,比如前三个批次做训练、后一个批次做验证,模拟“模型上线后见到新环境”的真实场景。

4.5 切图太慢,IO卡死

数据量大时,切图环节很容易被低估。几千张2048×2048的图逐张读取、逐张切块、再逐张提取特征,单线程跑起来可能要好几个小时,GPU基本在空转。我踩过几次之后,改成离线预处理模式:先把所有大图一次性切成patch,存成npy或者小尺寸文件,训练时直接读已经切好、提取好特征的矩阵,而不是训练循环里现切现算。切分和特征提取过程用multiprocessing并行,按CPU核心数切任务。实测下来,同样的数据量,离线并行预处理比边训练边处理快一个数量级。

这里再给一个与LBP有关的冷门提示:local_binary_pattern的计算其实不便宜,radius和n_points越大越明显。如果你有几万个patch,先用小参数(如P=8、R=1)跑通整个流程,确认有效再上大参数,不然调试一次要等半天。

上面这些坑,有些是我一开始就意识到的,有些是上线后被实际效果打脸才回头补的。做图像特征工程最核心的体会就是:每一步都要问一句“这个操作有没有让信息在训练和测试之间偷偷流动”。

切块大小、重叠率、特征组合,这三个东西没有一劳永逸的黄金参数。我现在的习惯是:固定一套代码,先在小批量数据上把整个流程串起来,用结果反推参数怎么调;目标很小就缩小窗口,目标细长就加大重叠率,特征区分度不够就加一组深度向量再比一把。这个流程跑顺之后,你手里的数据会从一堆原始像素变成干净、稳定、有区分度的特征矩阵,后续做什么模型都顺手得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询