尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Python从零实现LBP纹理特征提取与图像分类实战
做图像处理这一行跟纹理打交道是躲不开的。不管是布匹质检、木材表面缺陷检测、医学影像分析还是遥感图像分类纹理特征都是最常用的底层视觉信息之一。而在各种纹理描述子里LBPLocal Binary Pattern局部二值模式是我个人用得最多、性价比最高的一类——实现简单、计算快、对光照变化不敏感最关键的是不依赖深度学习也能在中小型样本上拿到还不错的分类效果。这篇笔记我整理了自己用 Python 从零实现 LBP 特征的全过程从最原始的算子定义讲起逐步补上圆形邻域、旋转不变、等价模式这些扩展版本然后给出完整的代码实现和一套可以直接改来用的纹理分类 demo。适合刚入坑 CV、准备做传统图像特征提取或者想在没 GPU 的环境下快速搭一个纹理识别小系统的同学参考。1. 纹理特征为什么需要 LBP从“看图片”到“读纹理”1.1 什么是纹理为什么问题会落在“特征”上纹理这个词听起来很玄其实你盯着一块木板、一块织物、一片草地看那种由重复出现的局部模式拼成的视觉感受就是纹理。和人眼直接“看”不同计算机拿到的只是一堆像素值它需要把“这块区域是粗糙的还是细腻的、是横纹的还是竖纹的”翻译成一组数值这种数值就是纹理特征。很多新手容易犯一个错以为纹理特征就是把灰度直方图画出来。灰度直方图只能告诉你整张图亮暗分布却完全丢掉了空间位置信息——一张棋盘格和一张随机噪声图灰度直方图可能几乎一样但视觉上差了十万八千里。纹理特征要解决的恰恰是“像素之间的空间关系怎么量化”这个核心问题。1.2 主流纹理特征方案对比LBP 凭什么实用传统纹理特征方案不少我实际对比过几类各有各的脾气特征方法核心思想优点明显短板GLCM 灰度共生矩阵统计特定方向上灰度对出现的频率方向信息丰富物理含义直观计算量大参数多特征维度容易膨胀Gabor 滤波器组用不同尺度和方向的正弦波卷积图像接近哺乳动物视觉皮层响应尺度方向可控滤波器参数难调特征维度高LBP比较中心像素与邻域像素的大小关系并编码计算极快光照单调变化不敏感无需训练原始版本对噪声敏感单尺度描述力有限深度特征用 CNN 自动学特征表示泛化能力强识别上限高需要大量样本和 GPU可解释性差我为什么在传统方案里优先推荐 LBP核心原因是它的“性价比”一个 LBP 特征提取在普通 CPU 上一张 512×512 的灰度图只有几毫秒到十几毫秒的耗时而 GLCM 和 Gabor 要慢一个量级。深度学习虽然强但如果你手头只有几百张纹理样本深模型反而容易过拟合此时 LBP 这样手工特征是再合适不过的起步方案。1.3 LBP 的核心思想一句话讲明白LBP 的思想用一句话概括就是看一个像素和它周围几个邻居谁大谁小把“谁大谁小”这个关系编码成一个二进制数。这听起来太简单了但正是这种简单带来了两个重要特性。第一它只关心相对大小所以整体光照变亮或变暗只要单调变化结果几乎不变——这对实际拍摄场景太有用了。第二二进制编码天然就是一组紧凑的数值可以直接做直方图统计喂给分类器时维度可控。2. LBP 计算原理从原始算子到扩展版本2.1 原始 LBP 算子3×3 邻域的二进制编码最原始的 LBP 是在 2002 年之前就出现的版本操作很简单取一个 3×3 的窗口用中心像素作为阈值周围 8 个像素如果大于等于中心值就记为 1小于就记为 0然后按固定顺序读成一个 8 位二进制数再转成十进制。举个例子假设一个 3×3 窗口的灰度值是56 48 70 42 58 62 85 79 71中心值是 58。按顺时针从左上角开始比较56 58 → 048 58 → 070 ≥ 58 → 162 ≥ 58 → 171 ≥ 58 → 179 ≥ 58 → 185 ≥ 58 → 142 58 → 0于是得到二进制串 00111110十进制就是 62。这个 62 就是该中心像素的 LBP 编码值。这样做一遍之后整张图就变成了一个 LBP 编码图。每个像素值都不再是亮度而是它周围微结构的编号。不同编号对应不同的局部模式比如全 0 或全 1 是平缓区域有连续跳变的位置往往对应边缘或角点。这种编码本身就是一种对图像微小模式的量化。2.2 圆形邻域从 3×3 到任意半径和采样点数3×3 窗口有个先天缺陷半径固定为 1只能用 8 个邻居很多尺度信息抓不到。后来研究者把邻域扩展成半径为 R 的圆形在圆周上均匀取 P 个采样点这就是扩展 LBP。这里的 R 不一定是整数采样点也不一定落在像素中心所以需要用双线性插值去估计该位置的灰度值。采样点坐标的计算公式是x_p x R * cos(2π * p / P) y_p y - R * sin(2π * p / P)其中 p 从 0 到 P-1。注意这里用 sin 的负号是为了让角度按图像坐标系的 y 轴向下方向正确旋转实际实现时前后一致就行。双线性插值就是在采样点周围的 4 个像素之间按距离加权取值这个操作在图像缩放里很常见代码实现也不复杂。引入圆形邻域后LBP 从一个固定的 3×3 算子变成了可控尺度的特征提取器P 越大方向分辨率越高R 越大感受野越大。这也让 LBP 有了“多尺度”的可能——你完全可以在同一个图像上提取 R1、R2、R3 三种 LBP再把特征拼起来用。2.3 旋转不变性让特征不随图像转身而变同一块纹理拍照时转了 90 度LBP 编码会完全不同因为二进制串的起始点固定了。解决办法叫旋转不变 LBPRotation Invariant LBP简称 riLBP把二进制串循环右移 P 次取所有移位结果中最小的那个十进制值作为最终编码。这个操作的直观理解是不管纹理朝哪个方向我始终从“看起来最特别的方向”开始读起方向信息被归一化了。举个例子二进制串 11110000 的循环移位中最小值是 00001111十进制 15那么所有能通过旋转变成这个模式的编码都会归到同一个值。代价也很明显方向信息没了。如果任务本身很依赖方向比如区分横纹和竖纹的布料那旋转不变反而有害。所以使用的时候要自己权衡这也是后面调参部分我会再强调的点。2.4 等价模式把 256 种模式压缩成 59 种8 个邻域的原始 LBP 有 2 的 8 次方即 256 种编码做直方图时维度偏高而且很多编码在实际图像中几乎不出现。研究者统计发现自然纹理中大部分 LBP 模式属于“等价模式”Uniform Pattern也就是二进制串中 0→1 或 1→0 的跳变次数不超过 2 的模式。比如 00000000跳变 0 次、01110000跳变 2 次都是等价模式而 01010101跳变 7 次就不是。等价模式把所有非等价模式统一归成一个桶。这样 P8 时等价模式有 58 种加上 1 个非等价桶总共 59 维维度直接降到了原来的 1/4 左右而且统计上等价模式往往占所有模式数的 90% 以上信息损失很小。2.5 分块直方图把逐像素编码变成全局特征逐像素的 LBP 图没法直接当特征用——两张纹理图错开几个像素逐像素比较就全对不上了。正确的做法是对 LBP 编码图做统计把图像分成若干小格比如 4×4 个 block每个小格单独统计直方图再把所有小格的直方图拼接成一个大向量。分块直方图的妙处在于它既有直方图的统计稳定性又保留了局部空间位置信息。左上角块和右下角块的纹理如果不同最终特征里会体现在不同位置的分量上。块数越多空间信息越精细但特征维度和对对齐的敏感性也越高一般 4×4 到 8×8 是个常用区间。3. Python 实现从零手写再到调库3.1 环境准备我用的环境是 Python 3.9 以上版本主要依赖这几个库pip install numpy opencv-python scikit-image scikit-learn matplotlibnumpy 负责数组运算opencv 负责图像读写和灰度转换scikit-image 里面有现成的 LBP 实现scikit-learn 用来做分类。如果你只是学习原理其实 numpy 和 matplotlib 就够用了。3.2 基础版 LBP先搞懂循环怎么写从零手写的第一版我没做任何优化就是最直白的双重循环。代码如下import numpy as np import cv2 def basic_lbp(gray_img): h, w gray_img.shape lbp_img np.zeros((h, w), dtypenp.uint8) # 从左上角开始按顺时针排列的邻域偏移 offsets [(-1, -1), (-1, 0), (-1, 1), (0, 1), (1, 1), (1, 0), (1, -1), (0, -1)] for y in range(1, h - 1): for x in range(1, w - 1): center gray_img[y, x] code 0 for i, (dy, dx) in enumerate(offsets): neighbor gray_img[y dy, x dx] if neighbor center: code | (1 i) lbp_img[y, x] code return lbp_img这里有两个细节要注意。第一code | (1 i)表示把第 i 位设为 1通过移位和按位或运算逐位拼出二进制数比字符串拼接高效得多。第二边缘像素没有完整邻域这个实现里我直接把边界留成 0如果任务对边缘敏感可以用 OpenCV 的copyMakeBorder先把图扩一圈或者改用 reflect 模式填充。3.3 圆形邻域与双线性插值实现圆形邻域版的难点在插值。我先写一个针对单个点的插值函数def bilinear_interp(img, y, x): h, w img.shape if y 0 or y h or x 0 or x w: return 0 y0 int(np.floor(y)) x0 int(np.floor(x)) y1 min(y0 1, h - 1) x1 min(x0 1, w - 1) wy y - y0 wx x - x0 top img[y0, x0] * (1 - wx) img[y0, x1] * wx bottom img[y1, x0] * (1 - wx) img[y1, x1] * wx return top * (1 - wy) bottom * wy def circular_neighbor(img, y, x, r, p): vals [] for i in range(p): theta 2 * np.pi * i / p nx x r * np.cos(theta) ny y - r * np.sin(theta) vals.append(bilinear_interp(img, ny, nx)) return np.array(vals)然后聚合出完整的圆形邻域 LBPdef circular_lbp(img, r1, p8): h, w img.shape lbp_img np.zeros((h, w), dtypenp.uint8) for y in range(r, h - r): for x in range(r, w - r): center img[y, x] code 0 for i, val in enumerate(circular_neighbor(img, y, x, r, p)): if val center: code | (1 i) lbp_img[y, x] code return lbp_img从实现可以看出来圆形邻域版最大的成本就是插值计算。P 取 8、R 取 1 时采样点基本落在整数像素上插值退化成直接取值所以速度还算快一旦 R 取 1.5、2 这种非整数值插值的开销会明显上涨。实际项目中我一般直接用 scikit-image 的实现手写版本主要用来验证和理解原理。3.4 旋转不变与等价模式的实现旋转不变的核心是循环移位取最小值def rotate_right(bin_val, n_bits): return ((bin_val 1) | (bin_val (n_bits - 1))) ((1 n_bits) - 1) def rotation_invariant_code(bin_val, n_bits8): min_val bin_val tmp bin_val for _ in range(n_bits): tmp rotate_right(tmp, n_bits) if tmp min_val: min_val tmp return min_val等价模式需要先建立一张查找表把 256 种编码映射到 59 个桶里def build_uniform_table(p): table np.zeros(1 p, dtypenp.int32) label 0 for i in range(1 p): bits [(i k) 1 for k in range(p)] trans sum(bits[k] ! bits[(k 1) % p] for k in range(p)) if trans 2: table[i] label label 1 nonuniform_label label # 所有非等价模式共用这个桶 for i in range(1 p): bits [(i k) 1 for k in range(p)] trans sum(bits[k] ! bits[(k 1) % p] for k in range(p)) if trans 2: table[i] nonuniform_label return table, nonuniform_label 1注意跳变次数的计算是把二进制串首尾相接来统计的也就是最后一个 bit 和第一个 bit 也要比较一次。这一点特别容易漏漏了之后等价模式的统计会和标准结果对不上。3.5 直接调用 scikit-image 的现成实现如果你不想重复造轮子scikit-image 已经封装好了所有扩展版本from skimage.feature import local_binary_pattern lbp_default local_binary_pattern(img_gray, P8, R1, methoddefault) lbp_ror local_binary_pattern(img_gray, P8, R1, methodror) lbp_uniform local_binary_pattern(img_gray, P8, R1, methoduniform) lbp_nri_uniform local_binary_pattern(img_gray, P8, R1, methodnri_uniform)method参数有几种选择default原始的旋转可变模式取值 0~255ror旋转不变但非等价取值 0~35uniform旋转不变加等价取值 0~58非等价模式统一为 59nri_uniform不旋转但与等价取值 0~58非等价统一为 59var用邻域方差替代二值编码得到的不是 LBP 而是局部对比度输出是浮点型数组这一点后面做直方图时容易踩坑我会在常见问题里专门说。4. 实战用 LBP 特征搭一个纹理分类器4.1 数据准备我这边用的是自己整理的一个 5 类纹理样本集每类 200 张 128×128 灰度图包括麻布纹、横条纹、竖条纹、木纹理和网格纹。数据组织成这样的目录结构texture_dataset/ ├── fabric/ ├── horizontal/ ├── vertical/ ├── wood/ └── grid/每类下面直接放图片即可后面用路径批量读取。如果手头没数据也可以把一张大纹理图切块切成几百张小图效果类似。4.2 特征提取封装我习惯把特征提取封装成一个函数输入是灰度图输出是一维特征向量def extract_lbp_feature(img, p8, r1, methoduniform, block(4, 4)): lbp local_binary_pattern(img, Pp, Rr, methodmethod) n_bins p * (p - 1) 3 if method uniform else 1 p feats [] h, w lbp.shape bh, bw h // block[0], w // block[1] for i in range(block[0]): for j in range(block[1]): cell lbp[i * bh:(i 1) * bh, j * bw:(j 1) * bw] hist, _ np.histogram(cell, binsn_bins, range(0, n_bins)) hist hist.astype(np.float32) hist / (hist.sum() 1e-6) feats.append(hist) return np.concatenate(feats)几个关键点说一下。第一n_bins的算法要和方法对应uniform 模式下是 59default 模式下是 256。第二np.histogram的range必须显式给成(0, n_bins)否则数组里的最大值接近 59 但类型是浮点直方图分桶很可能出错。第三直方图要做 L1 归一化消除图像尺寸差异带来的影响。4.3 分类器与整体流程特征提取完后训练分类器就非常简单了。我用 kNN 和线性 SVM 都试过小样本场景下两者都能用但 kNN 更省事import numpy as np from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.svm import SVC X [] y [] class_names [fabric, horizontal, vertical, wood, grid] for label, name in enumerate(class_names): for img_path in sorted(glob.glob(ftexture_dataset/{name}/*.png)): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) feats extract_lbp_feature(img) X.append(feats) y.append(label) X np.array(X) y np.array(y) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy) clf KNeighborsClassifier(n_neighbors5) clf.fit(X_train, y_train) print(kNN accuracy:, clf.score(X_test, y_test)) svm SVC(kernellinear, C1.0) svm.fit(X_train, y_train) print(SVM accuracy:, svm.score(X_test, y_test))我在自己的这个 5 类样本集上uniform 模式加 4×4 分块kNN 准确率在 93% 左右线性 SVM 能到 95% 上下。这个结果已经能说明 LBP 的实用性了——没经过任何调参纯靠特征本身就能把五类纹理基本分开。5. 调参与避坑心得5.1 关键参数到底怎么选LBP 需要重点调的是 P、R、method 和 block 四个参数。我给个自己常用的参考参数常用区间我的经验P8、16样本小或图像小用 8方向细节要求高用 16R1、2、3R1 抓微观纹理R3 抓宏观结构可多尺度拼接methoduniform / ror / default默认优先 uniform维度低且稳方向敏感任务用 nri_uniformblock4×4、8×8、1×1图像对齐好用小 block对齐差用大 block 或全图直方图一个很容易忽略的原则block 的尺寸和图像比有关不是越多越好。128×128 的图用 16×16 block每个小格只有 8×8 像素直方图统计极不稳定反而把性能拉低。我见过不少人一上来就追求精细分块结果准确率暴跌。5.2 我踩过的几个坑踩坑一local_binary_pattern的输出是浮点数组直接拿去做np.uint8强转会在 59 这个边界处出问题。正确做法是直方图之前保留浮点分桶时用浮点 range。踩坑二等价模式表的跳变次数要首尾相接统计很多网上代码只统计了相邻位、漏掉最后一位和第一位导致模式映射错误。踩坑三手写版本和 scikit-image 版本的方向定义可能不一样同一个图一个顺时针一个逆时针特征对不上。如果一边手写、一边调库务必统一采样方向。还有一个坑是直方图归一化。样本量不一致时不归一化分类结果会被图像面积大的样本主导。但如果后续分类器用的是决策树这类对尺度不敏感的模型归一化倒无所谓所以也别无条件照搬。5.3 性能优化经验纯 Python 双重循环实现一张 512×512 图要一两秒根本没法上生产。想提速我试过比较有效的几个方向用 numpy 向量化替代像素循环计算所有邻域的差值矩阵耗时能降一个数量级以上图像先缩放到 256×256 或更小纹理特征对分辨率不像深度学习那么敏感提取一次特征后存成文件缓存训练和测试阶段直接读特征避免反复跑分块直方图可以用积分直方图加速每个 block 的直方图通过四次查表得到速度提升明显如果只是学习或做原型上面这些优化都不是必须的但真到了几千张图的规模还是要尽早考虑。6. 写在最后一点个人体会LBP 是我在传统图像特征里使用率最高的工具它教会我一件很重要的事特征不一定越复杂越好。很多问题尤其是有明确物理背景的纹理识别任务简单算子加合理统计往往比盲目套用深度模型更稳、更快、更容易解释。根据我个人的实际经验LBP 最适合的场景是中小规模样本、硬件条件有限、需要快速验证想法的情况。如果样本量足够大且有 GPU深度模型的上限会更高但即便是在深度学习流程里LBP 也常被用来做辅助特征或后验校验。比如我就经常把 LBP 特征和深度特征拼接起来在不少任务上都能带来一两个百分点的稳定提升。最后分享一个小技巧调参时不要一次性把所有参数都改掉。我的习惯是先固定 P8、R1、methoduniform、block4×4跑一个基线结果然后每次只调一个维度用测试集准确率画小表格对比。这样你能清楚知道每个参数到底贡献了多少而不是黑箱式地碰运气。纹理特征这条路走进去之后你会发现很多看似简单的方法背后的设计理由远比想象中讲究。
RELATED

相关推荐

AR人脸数据集稀疏表示实验:BMP读取、数据划分与SRC配置指南

AR人脸数据集稀疏表示实验:BMP读取、数据划分与SRC配置指南

简介:AR数据库是稀疏表示与鲁棒人脸识别研究中广泛使用的公开数据集;常见下载多为灰度图,此版本是经论文“PCA versus LDA”(IEEE TPAMI)预处理后的彩色裁剪版。压缩包共5201个文件,含2600张BMP彩色人脸图像…

📅 2026/10/14 13:13:01
Scrum敏捷开发框架:角色分工与团队协作实践

Scrum敏捷开发框架:角色分工与团队协作实践

1. Scrum框架概述 Scrum作为一种敏捷开发框架,最早由Jeff Sutherland和Ken Schwaber在1990年代提出。它通过固定周期的迭代(Sprint)来应对复杂项目的需求变化,相比传统瀑布式开发更适应现代快节奏的产品研发环境。我在多个互联网产…

📅 2026/10/14 13:13:01
uni-app项目模板搭建指南:从目录结构到请求封装与多端调试

uni-app项目模板搭建指南:从目录结构到请求封装与多端调试

简介:这是一份面向具备一年以上uni-app开发经验的前端工程师的快速开发项目模板,由一位五年经验的Web前端开发者开源,旨在帮助开发者跳过重复搭建基础框架的过程,直接进入业务开发,同时也可作为进阶学习与参考的实战范…

📅 2026/10/14 13:13:01
MORE NEWS

更多资讯

📰

CUDA算力与驱动版本兼容性速查指南

简介:本资源是一份系统梳理NVIDIA显卡CUDA算力与版本兼容关系的权威参考文档,面向深度学习工程师、GPU开发者及高性能计算初学者,解决选型时‘某型号显卡支持哪些CUDA版本’‘不同CUDA版本对应哪些计算能力(Compute Capability&am…

📰

AI攻击时代,企业高防IP该升级到什么形态?高防IP给出了一份新答卷

过去两年,网络安全领域发生了一个容易被忽视却极其关键的变化:攻击的门槛正在被AI快速拉低。过去需要一定技术功底才能发起的攻击,现在借助AI工具,一个懂点基础操作的人就能批量生成。攻击资源打包售卖、自动化攻击脚本一键调用、…

📰

EP进化规划Python实现:从原理到黑盒优化实战

EP进化规划的Python实现,听起来像是一篇课程作业的标题,但真正上手之后你会发现,这套算法在工程优化里意外地好用。进化规划(Evolutionary Programming)算得上进化计算里最朴素的一支,它不搞遗传算法那套“…

📰

自主机器人开发的四层物理现实约束

1. 为什么“自主机器人基础”不是入门课,而是分水岭“自主机器人基础”这六个字,乍看像大学选修课的名称,或者某本教材的副标题。但在我带过三十多个机器人项目、从学生团队到工业落地一线摸爬滚打的十年里,它从来不是起点——而是…

📰

飞机总体设计软件核心:约束分析、重量估算与气动迭代

简介:这份PDF面向飞机设计初学者、航空专业学生与研究人员,系统介绍AAA(Advanced Aircraft Analysis)飞机设计分析软件的功能与用法,帮助读者快速建立从概念设计到性能分析的完整认知。内容围绕重量分析、空气动力计算…

📰

Oracle数据库运维案例实战:从DG同步到AWR定位与巡检脚本

简介:这是一份面向Oracle数据库运维与DBA人员的案例型PPT课件,聚焦RAC集群环境下的故障分析与处置流程。内容以真实错误日志为主线,逐一剖析IPC Send timeout、LMS/LMD锁管理进程通信异常、实例成员资格不一致、脑裂驱逐以及ORA-29740报错等典…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬