图像处理入门026 | 分段线性变换:灰度窗与阶梯变换——CT 窗宽窗位的数学本质

第 026 期 | 图像处理算法入门系列 | 2026-09-04

关键词:分段线性变换、灰度窗、窗宽窗位、阶梯变换、密度分层、LUT、CT 影像、中间调拉伸

承接 025 期 → 026 期 :024 期是一条直线(g = α·f + β),025 期把直线弯成曲线(伽马/Log);今晚把控制权彻底交给你------直线折成几段,每段斜率自己定。医学影像 CT 的"窗宽窗位"正是这套数学的巅峰应用:同一张 CT,医生切换一下窗口,就能分别看肺、看骨、看软组织。


一、引言:曲线也有管不到的地方

025 期结尾我们验证了一个结论:暗图的"第一步粗调"优先用伽马。但伽马有一个先天盲区------它只有一个参数 γ,无法指定"拉伸哪一段、压缩哪一段"

用一个实验说话(第六节详述):一张内容集中在 90~165 灰度区间的"中间调挤压"图(mean=128,std=18),我们分别用三种方法增强:

text 复制代码
实测对比(std = 标准差,越大对比越强):
  原图:                mean=128  std=18
  自动伽马:            mean=127  std=18   ← 自动算出 γ = 1.011,等于什么都没做!
  min-max 线性拉伸:    mean=99   std=41   ← 有一点效果,但被极值"绑架"
  三段线性(90,30/165,235):
                        mean=135  std=49   ← 中间调被猛拉,两端直接冻结

自动伽马失效的原因很直白:它的目标是"把均值调到 128",而这张图均值本来就是 128------伽马是亮度工具,不是对比度工具。min-max 拉伸能干活,但它只能"整体拉",一个亮斑离群点就能把拉伸倍数压垮。

想要"只拉 90~165 这一段,其余冻结"?只有折线做得到。


二、核心原理:三段线性、灰度窗与阶梯

2.1 三段线性变换

设两个断点 (x₁, y₁) 和 (x₂, y₂),两端锚定 (0, 0) 和 (255, 255):

text 复制代码
三段线性:
  f ≤ x₁:        g = (y₁ / x₁) · f                          ← 段1:斜率小,压缩暗部
  x₁ < f ≤ x₂:   g = y₁ + (f − x₁) · (y₂ − y₁)/(x₂ − x₁)    ← 段2:斜率大,拉伸中间调
  f > x₂:        g = y₂ + (f − x₂) · (255 − y₂)/(255 − x₂)  ← 段3:斜率小,压缩高光
斜率 作用 直觉
段 1(0 → x₁) < 1 暗部压缩 反正看不清,挤一挤
段 2(x₁ → x₂) > 1 中间调拉伸 主体内容在这里,拉开
段 3(x₂ → 255) < 1 高光压缩 已经够亮,不用动
text 复制代码
和 024 期直线的关系:
  直线 = 三段线性的退化形式(所有断点落在同一条直线上)
  折线 = 直线的推广(每个断点都是自由度)
  断点数越多 → 曲线越自由 → 但交互/调参也越复杂(PS 曲线工具就是 N 断点折线)

2.2 灰度窗:窗宽窗位(CT 的核心显示技术)

灰度窗是三段线性的一个极端特例------段 1 和段 3 的斜率直接压到 0:

text 复制代码
灰度窗(窗宽 W,窗位 L):
  f < L − W/2:            g = 0        (窗下 → 全黑)
  L − W/2 ≤ f ≤ L + W/2:  g = 255 · (f − (L − W/2)) / W   (窗内 → 线性拉满)
  f > L + W/2:            g = 255      (窗上 → 全白)
参数 别名 控制 直觉
窗宽 W Window Width 窗内对比度 W 越小,窗内拉伸越猛,但覆盖的灰度范围越窄
窗位 L Window Level 观察哪一段 L 平移窗口中心,对准感兴趣的灰度段
text 复制代码
CT 的物理背景(帮助理解,不影响代码):
  CT 值用 Hounsfield 单位(HU):水 = 0,空气 = −1000,骨 ≈ +1000,
  动态范围横跨 2000 HU,而显示器只有 256 级灰度。
  所以 CT 必须"开窗":一次只显示 2000 HU 中的一小段,其余全部黑/白饱和。
  医生调窗宽窗位,就是在调今晚这两个公式参数。

2.3 阶梯变换:密度分层

把连续灰度量化成 n 级台阶,是分段线性"斜率取 0 或 ∞"的极限玩法:

text 复制代码
阶梯变换(n 级):
  g = floor( f · n / 256 ) · 255 / (n − 1)

  n = 2   →  二值化(020 期的老朋友)
  n = 4~16 →  海报化效果(美术/设计)
  灰度级映射到颜色 → 伪彩色/密度分层(101 期彩色处理的铺垫)

图 1:一张图看懂分段线性。左图为三段折线与 4 级阶梯的映射曲线------段 2 斜率 > 3,中间调被猛拉;右图为同一渐变条的实际效果,灰度窗窗内对比显著增强、窗外直接黑/白。


三、代码实践

3.1 核心函数:一切点运算都是 256 项 LUT

分段线性、灰度窗、阶梯变换,全部可以预计算成 256 项查找表(027 期专题预告):

python 复制代码
import cv2
import numpy as np

def lut_piecewise(x1, y1, x2, y2):
    """
    三段线性 LUT:断点 (x1, y1)、(x2, y2),两端锚定 (0,0)、(255,255)。
    返回 (lut_uint8, ys_float)------ys 用于绘制映射曲线。
    """
    xs = np.arange(256, dtype=np.float32)
    ys = np.empty(256, dtype=np.float32)

    s1 = y1 / max(x1, 1e-6)                       # 段1斜率:暗部压缩
    m1 = xs <= x1
    ys[m1] = xs[m1] * s1

    s2 = (y2 - y1) / max(x2 - x1, 1e-6)           # 段2斜率:中间调拉伸
    m2 = (xs > x1) & (xs <= x2)
    ys[m2] = y1 + (xs[m2] - x1) * s2

    s3 = (255.0 - y2) / max(255.0 - x2, 1e-6)     # 段3斜率:高光压缩
    m3 = xs > x2
    ys[m3] = y2 + (xs[m3] - x2) * s3

    return np.clip(np.round(ys), 0, 255).astype(np.uint8), ys

def lut_window(width, level):
    """灰度窗 LUT(窗宽 W、窗位 L)。"""
    lo = level - width / 2.0
    xs = np.arange(256, dtype=np.float32)
    vals = (xs - lo) * (255.0 / max(float(width), 1e-6))
    return np.clip(np.round(vals), 0, 255).astype(np.uint8)

def lut_staircase(n):
    """阶梯变换 LUT:把 [0,255] 量化成 n 级台阶。"""
    xs = np.arange(256, dtype=np.float32)
    ys = np.floor(xs * n / 256.0) * (255.0 / (n - 1))
    return np.clip(np.round(ys), 0, 255).astype(np.uint8)
text 复制代码
关键细节:
  1. 斜率分母要 max(·, 1e-6) 防除零(比如 x1=0、x2=255 的退化情况)。
  2. 整个函数没有任何像素级运算------只在 256 项上预计算,
     像素处理交给 cv2.LUT 一次查表完成(实测比 NumPy 逐像素快 32 倍,见第七节)。
  3. lut_window 用 float 中间量计算再 clip,不存在 uint8 回绕问题。

应用只需一行:

python 复制代码
out = cv2.LUT(img, lut_window(width=110, level=115))   # CT 软组织窗

3.2 测试图:合成"CT 断层"

真实 DICOM 数据不方便获取,我们合成一张类比图------灰度模拟组织密度:空气(≈8) → 肺(≈45) → 软组织(≈110) → 肝(≈135) → 骨(≈225):

python 复制代码
def make_ct_phantom(size=(420, 560), seed=3):
    h, w = size
    rng = np.random.default_rng(seed)
    yy, xx = np.mgrid[0:h, 0:w].astype(np.float32)
    ny, nx = yy / h, xx / w

    img = np.full(size, 8.0, dtype=np.float32)          # 背景 = 空气
    body = ((nx - 0.50)/0.42)**2 + ((ny - 0.52)/0.44)**2 < 1
    img[body] = 110.0                                    # 身体 = 软组织
    for cx in (0.33, 0.67):                              # 双肺
        lung = ((nx - cx)/0.13)**2 + ((ny - 0.42)/0.22)**2 < 1
        img[lung] = 45.0
    # ......肝 135 / 脊椎 230 / 肋骨环 225,加噪声与高斯模糊(完整代码见文末脚本)

图 2:断点网格实验。原图 std=18(中间调挤压在 90~165),6 组断点组合拉伸后 std 提升到 24~51------断点收得越窄、段 2 斜率越大,对比越猛,但离群像素越容易被"牺牲"到 0/255。


四、灰度窗深入:一次调窗 = 一次重新显示

4.1 四大窗口:同一张 CT 的四种看法

图 3:合成 CT 上的四种窗口。全范围对照(std=67)所有组织混在一起;软组织窗(W=110, L=115,std=86)肝与肌肉层次清晰、骨已饱和发白;肺窗(W=170, L=60)肺叶与背景层次拉开;骨窗(W=90, L=200)只剩肋骨环与脊椎,软组织全部归黑。

实测数据:

text 复制代码
  全范围对照(W=255, L=128):  mean=77   std=67  ← 什么都能看到,什么都看不清
  软组织窗(W=110, L=115):    mean=74   std=86  ← 窗内对比 +29%,细节最清楚
  肺窗(W=170, L=60):        mean=141  std=79  ← 窗口对准肺的灰度段
  骨窗(W=90, L=200):        mean=20   std=59  ← 只有骨在窗内,其余全黑

4.2 W 与 L 的独立影响:两个旋钮各管各的

图 4:上排固定 L=128 变 W------窗宽是"对比度旋钮",W 越小窗内越猛(std 随 W 单调变化);下排固定 W=110 变 L------窗位是"平移旋钮",决定观察哪一段灰度,图像整体亮度随之变化。

text 复制代码
一句话记住:
  窗宽 W = 拉伸倍数的分母(255/W)------管"看多清楚"
  窗位 L = 窗口中心的位置------管"看哪里"
  临床调窗顺序:先转 L 对准目标组织,再收 W 增强对比。

五、三方案对比:为什么只有折线能"只拉中间调"

图 5:中间调挤压图(std=18)上三方案对决。自动伽马算出 γ=1.011 近似恒等(均值已达标,无活可干);min-max 拉伸 std=41 但被极值绑架;分段线性 std=49 且直方图两端被钉死在 0/255。

text 复制代码
选型口诀:
  · 亮度不对(整体偏暗/过曝)      → 伽马校正(025 期)
  · 对比度整体不足、内容满幅分布   → min-max / 百分位拉伸
  · 内容挤在某一段、两端是废话     → 分段线性 / 灰度窗(本期)
  · 要的是全局对比度重新分配       → HE / CLAHE(022/023 期)

分段线性直方图的独有特征:中间部分被拉开,两端出现 0 和 255 上的"冲天柱"(被冻结的像素堆在那里)------这是伽马和 HE 都不会产生的形状。


六、阶梯变换与密度分层

图 6:合成 CT 在 2/4/8/16 级阶梯量化下的效果。2 级即二值化(020 期),级数越少色块感越强------设计领域的"海报化"滤镜正是这个数学。

text 复制代码
阶梯变换的三张面孔:
  图像增强视角:密度分层------把淹没在连续灰度里的密度差异显式分开
  美术设计视角:海报化(Posterize)------PS 的"色调分离"就是逐通道阶梯变换
  压缩视角:颜色量化(099 期预告)------中位切割/八叉树是它的"智能选台阶"版本

七、性能基准:折线再复杂,查表只要 0.24 ms

图 7:480×640 灰度图三方案性能(对数坐标)。LUT=0.236ms,比 NumPy 逐像素插值(7.55ms)快 32 倍,比 Python 双层循环(70.9ms)快约 300 倍;三方案输出逐像素一致(max|Δ|=0)。

text 复制代码
本期最重要的性能结论:
  无论映射是直线(024)、曲线(025)还是折线/阶梯(026),
  只要它是"每个输入灰度对应唯一输出"的点运算,
  就一定能写成 256 项 LUT------cv2.LUT 一律 0.2~0.3ms。
  这也是为什么医学影像设备能实时调窗:调窗 = 换一张 256 项表。

八、参数网格:等效双滑杆预览

图 8:窗宽 × 窗位 3×3 网格。横向看 W:越往左窗内越猛;纵向看 L:越往下观察的灰度段越亮。本地可跑 Trackbar 交互版,双滑杆实时调窗(代码见文末)。

Trackbar 交互版(本地运行,按 q 退出):

python 复制代码
import cv2
import numpy as np

img = cv2.imread('ct.png', 0)   # 换成你的图
cv2.namedWindow('windowing')

def on_change(_):
    w = cv2.getTrackbarPos('W', 'windowing') or 1
    l = cv2.getTrackbarPos('L', 'windowing')
    lo = l - w / 2.0
    xs = np.arange(256, dtype=np.float32)
    lut = np.clip(np.round((xs - lo) * (255.0 / w)), 0, 255).astype(np.uint8)
    cv2.imshow('windowing', cv2.LUT(img, lut))

cv2.createTrackbar('W', 'windowing', 110, 255, on_change)
cv2.createTrackbar('L', 'windowing', 115, 255, on_change)
on_change(0)
while cv2.waitKey(30) != ord('q'):
    pass

九、可视化实验总览

本次共 8 个实验,对应 8 张可视化图:

# 实验 图文件 验证目标
1 折线/阶梯原理 + 渐变条 026_curves_principle.png 一张图看懂折线形状
2 断点网格 026_piecewise_grid.png 断点收窄 → 对比增强
3 灰度窗四大窗口 026_window_basics.png 同图四看法
4 W/L 独立影响 026_window_wl_effect.png W 管对比、L 管位置
5 阶梯变换 026_staircase.png 密度分层/海报化
6 三方案对比 026_compare_three.png 折线是中间调拉伸唯一解
7 性能与一致性 026_benchmark.png LUT 快 32 倍
8 W×L 参数网格 026_wl_grid.png 等效双滑杆

代码运行后,上述 PNG 全部自动生成到 code/output/ 目录。


十、产品推荐

层级 移动端 App 专业级工具 开源/免费 适用场景
医学影像调窗 OsiriX HD(iOS DICOM 查看器)、MIM Mobile RadiAnt DICOM Viewer(Windows)、OsiriX MD(macOS) Horos(macOS)、3D Slicer、MicroDicom(Windows 免费版) DICOM 影像窗宽窗位浏览
曲线工具(N 断点折线) Snapseed(曲线)、Lightroom Mobile(色调曲线) Adobe Photoshop(曲线)、Capture One、Affinity Photo GIMP(曲线)、Darktable、RawTherapee 修图:中间调精修
海报化/色调分离 PicsArt(海报效果)、Prisma Photoshop(色调分离)、Affinity Photo GIMP(Posterize) 设计/美术风格化
科研分析 --- ImageJ/Fiji(Brightness/Contrast 调窗)、MATLAB ImageJ(免费) 显微/科学图像
text 复制代码
选择建议:
  · 想亲手体验"调窗":手机装 MicroDicom / OsiriX HD 打开任意 DICOM 文件,
    拖动窗宽窗位滑杆------就是今晚实验 3/4 的交互版。
  · 通用修图:Snapseed "曲线"工具点两个控制点拉折线,等价于今晚的 lut_piecewise。
  · 影像科/科研:Horos 与 3D Slicer 免费专业,窗宽窗位、CT 值测量全套支持。

十一、常见问题 FAQ

Q1:分段线性、灰度窗、阶梯变换,三者是什么关系?

A:灰度窗是三段线性的特例(段 1/段 3 斜率 = 0,段 2 端点对齐 0/255);阶梯变换是"斜率在台阶处为 0、台阶沿垂直"的极端分段函数。三者都是"逐灰度映射"的点运算,都能写成 256 项 LUT。

Q2:窗宽窗位里的 L(窗位)和 024 期的 β(亮度)是一回事吗?

A:形似神不似。β 是整条映射直线平移;L 平移的是"窗口"(哪一段灰度被拉伸),窗内像素的输出不受 L 直接加减,而是被重新映射到 0~255。L 增大时图像看起来"变暗",本质是窗口移向了更亮的输入段。

Q3:分段线性会把信息压没吗?

A:窗外像素直接饱和到 0/255,窗内外的灰度差信息确实丢失(不可逆)。这正是 CT 的设计取舍:一次只看清一种组织,需要看别的组织就换窗口------所以医学影像原始数据永远保存全动态范围的 DICOM,显示层才调窗。

Q4:自动伽马为什么在这期实验里完全失效?

A:伽马的优化目标是均值(亮度),不改变直方图形状的"宽窄"(对比度)。实验图的均值恰好是 128,自动伽马算出 γ=1.011 直接躺平。教训:先诊断图像问题属于亮度还是对比度,再选工具

Q5:断点应该怎么选?有没有自动方法?

A:经验法是看直方图:把内容密集区两端设为断点(如百分位 1% 和 99%)。自动方法包括按百分位自动选断点、最小值-最大值忽略离群点等。更彻底的自适应方案是 022/023 期的 HE/CLAHE------它们相当于"直方图数据驱动的自动分段变换"。

Q6:彩色图能做分段线性吗?

A:能,而且 Photoshop 的"曲线"就是逐通道(RGB/单独通道)分段折线。工程上与 024/025 期结论一致:整图风格调整推荐转 Lab 只动 L,或 HSV 只动 V,避免色相漂移;创意调色才逐通道拉曲线。


十二、课后练习

  1. 基础练习 :给 lut_window 加一个"反窗"模式:窗内显示 0/255 两级、窗外线性(用于突出"不在目标灰度段"的像素),想想这能用于什么质检场景。
  2. 进阶练习:实现"百分位自动断点":取累计直方图 1% 和 99% 处的灰度作为 x₁、x₂,自动做三段拉伸,在 10 张不同图上对比 min-max 拉伸的鲁棒性。
  3. 挑战题 :用 OpenCV Trackbar 实现四窗口联动:一个窗口显示原图+当前窗宽窗位指示线(叠加在直方图上),另一个窗口实时显示调窗结果(提示:cv2.line 画指示线)。

十三、完整可运行脚本

bash 复制代码
# 环境要求
Python 3.8+
OpenCV 4.5+ / 5.x
NumPy 1.20+
Matplotlib 3.4+

# 运行
python code/026_piecewise_windowing.py

运行后 code/output/ 下会生成 8 张 PNG,对应本文全部实验。


下期预告(第 027 期):查找表(LUT)加速------点运算优化的统一答案。三期的直线、曲线、折线全部归位:为什么 256 项查表能通吃一切点运算?LUT 在滤镜引擎、美颜 SDK、实时调色里的工业级用法。

相关推荐
Ro Jace2 小时前
基于深度学习的图像处理方法
图像处理·人工智能·深度学习
AndrewHZ13 小时前
图像处理入门025 | 非线性灰度变换:伽马校正与 Log 变换——暗图救星的两条曲线
图像处理·伽马校正·log变换·非线性点运算·自动伽马·lut加速·暗图增强
AndrewHZ15 小时前
图像处理入门024 | 线性灰度变换:亮度与对比度调整
图像处理·性能基准·线性灰度变换·亮度 β·对比度 α·uint8溢出·hsv/lab
岁月蹉跎的一杯酒21 小时前
格科微内置 ISP 图像效果调试(格科微 YUV Sensor)—GC2145
图像处理·isp·gc2145
FPGA开源工坊1 天前
ISP图像处理--Bayer Raw格式
图像处理·人工智能·verilog·fpga·isp
江畔柳前堤2 天前
具身智能全景深度指南(2026年9月版):从“会聊天的AI“到“能干活的机器“
大数据·javascript·图像处理·人工智能·分布式·智慧城市·原型模式
安河桥。2 天前
Android视频流处理模块硬件加速器数据流转技术说明
android·图像处理·isp
牧羊人.3332 天前
计算机视觉基础 第13章 |背景建模与运动目标检测
图像处理·人工智能·目标检测·计算机视觉·目标跟踪
牧羊人.3332 天前
计算机视觉基础第15章|DNN实现图像风格迁移
图像处理·人工智能·深度学习·opencv·计算机视觉