ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

超声相控阵R-Θ线性插值扫描转换与双线性插值实现

超声相控阵R-Θ线性插值扫描转换与双线性插值实现 简介这份PPT面向医学影像、超声成像与信号处理方向的学生和研究人员围绕R-Θ线性插值方法讲解超声波图像重建的完整思路适合用于课题汇报、课程展示或实验复盘。压缩包内共1个ppt文件体积约1.17MB内容以原理图、公式推导和实验参数表格为主便于直接引用到汇报材料中。已有208人学习下载。内容覆盖实验B超成像的波束形成、图像存储、坐标变换与DSC部件流程并给出16倍数据抽取、探头35C50HA的3.5MHz标准频率、40MHz采样频率、50mm半径、128阵元与0.498mm阵元间距等关键参数以及240条扫描线、68度扇扫角度下二进制裸数据的提取方法。RF信号部分涉及时域TGC补偿效果、频域中心频率识别以及移频、滤波、抽取构成的数字下变频解调过程读者可据此理解坐标变换与插值如何提升图像连续性和分辨率为超声图像质量优化提供参考。1. 从扇形扫描说起为什么超声重建离不开 R-Θ 线性插值超声相控阵采回来的原始数据长得像一张按角度排列的表格第 i 行是探头在偏转角 θ_i 上的一条 A 扫第 j 列对应深度 r_j整块数据躺在 R-Θ 极坐标里。人眼要的是屏幕上那张扇形切片图把极坐标网格换成直角坐标网格的这一步业内叫扫描转换R-Θ 线性插值就是其中最常用的一把刀。反直觉的地方在于重建图像里那些放射状的亮暗条纹多半不是探头坏了也不是噪声大而是 θ 方向采样太稀、插值核选得太糙甚至是在射频域而不是包络域做的插值。这份内容适合三类人刚拿到极坐标数据不知道怎么显示的入门者、要把超声波重建图象塞进实时流水线的工程师以及要做课题汇报、需要把整个重建流程讲清楚的人。下面从采集链路一直讲到映射表固化每一步都给可跑的命令和参数。2. R-Θ 极坐标数据是怎么来的采集链路与重建网格的对应关系2.1 相控阵与环阵的极坐标采样以及 (nθ, nR) 数据矩阵不管是线阵做虚拟阵元偏转还是凸阵、环阵本身带弧度采集端形成的都是一组「角度 深度」的样本。每一次发射接收事件对应一个偏转角 θ_i沿声轴方向按采样率 fs 采一条 A 扫得到 nR 个点扫完 nθ 个角度就得到形状为 (nθ, nR) 的矩阵。这个矩阵的 dtype 值得讲究原始射频信号通常是 int16做包络检波Hilbert 变换取模和对数压缩之后用 float32 参与插值最省心一旦中途落到 uint8双线性插值的四个权重相乘会把量化误差放大成可见的台阶。这里有个决定成败的顺序问题插值必须在包络检波之后做不能在射频域做。射频信号是双极性的过零点附近数值剧烈翻转双线性插值本质上是四个点的加权平均在正负交替的过零点上平均会得到接近零的值重建出来就是一排排伪条纹看起来像干扰实际是数学造成的。2.2 为什么用反向映射而不是把极坐标样本打到直角网格上正向映射是把每个极坐标样本按坐标变换打到直角像素上近场角分辨率富余一个像素被反复覆盖远场则出现大量空洞除非再做一轮散点填充或者形态学补洞工程上极少这么干。反向映射反过来遍历输出直角网格上的每个像素反算它在极坐标里的位置再做插值。好处是每个输出像素一定有一组确定的 (i, j, a, b)不会出现空洞代码可以用纯向量化写出来没有循环分支。代价是极坐标数据里有些区域永远不会被访问到比如扇形之外的部分这属于可接受的浪费。另一个常见误用是先把极坐标数据沿 r 方向重采样到和输出像素一样密再直接取整索引——这等于放弃了插值径向会出现明显的阶梯状条纹。2.3 重建网格参数表与最小可跑的参数计算参数符号典型取值改动后的影响角度步长Δθ0.5°~1.0°越小远场越细腻数据量与采集时间线性上升角度范围[θ0, θ1]-45°~45°直接决定扇形开角径向步长Δrc/(2·fs)由采样率定死重采样只能加密不能变细扇形最大深度Rmax80~150 mm决定视场半径与输出图像尺寸输出像素间距Δx0.2~0.5 mm越小越平滑插值访存代价越高扇形顶点(cx, cy)图像底边中点错一个像素整幅图跟着旋转import numpy as np c 1.54 # 声速单位 mm/μs配合 MHz 采样率使用 fs 40.0 # 采样率单位 MHz即 samples/μs dr c / (2 * fs) # 径向步长约 0.0193 mm r0 0.0 n_r 1024 n_th 128 th0, th1 np.deg2rad(-45.0), np.deg2rad(45.0) dth (th1 - th0) / (n_th - 1) # 复现极坐标采集网格 r_axis r0 np.arange(n_r) * dr # (n_r,) 深度轴 th_axis th0 np.arange(n_th) * dth # (n_th,) 角度轴 # 输出直角网格 dxy 0.3 # mm/pixel Rmax r0 (n_r - 1) * dr # 视场半径取数据满量程 W int(2 * Rmax / dxy) 1 H int(Rmax / dxy) 2 apex (H - 1, W // 2) # 顶点放底边中点 print(fdr{dr:.4f} mm, W{W}, H{H}, apex{apex})这段代码解决的是「两套网格怎么对齐」的问题。dr由声速和采样率决定是不能随手改的物理量想提高径向分辨率只能提高采样率或者做插值。dxy是显示参数可以自由选选得比dr小意味着输出像素比原始样本还密插值会变得平滑但不会凭空多出信息。apex必须和真实探头阵元的等效相位中心对上凸阵和线阵偏转扫描的顶点位置差别很大拿不准的时候用点目标标定一下。3. 用 NumPy 写出第一版 R-Θ 双线性插值扫描转换3.1 反向映射的坐标变换与索引公式设输出像素坐标为 (u, v)u 向右v 向下物理尺寸换算到毫米x (u - cx) · Δx y (cy - v) · Δx r √(x² y²) θ atan2(x, y)角度用 atan2(x, y) 而不是 atan2(y, x)是把声轴深度方向当成 0 角横向偏移决定正负。索引换算成f_i (θ - θ0) / Δθ f_j (r - r0) / Δr再取 i ⌊f_i⌋、j ⌊f_j⌋小数部分 a f_i - i、b f_j - j双线性输出为 (1-a)(1-b)·P[i,j] a(1-b)·P[i1,j] (1-a)b·P[i,j1] ab·P[i1,j1]。约定正确写法用错时的现象深度轴方向y (cy - v)·Δx图像上下颠倒角度定义θ atan2(x, y)图像绕顶点旋转 90°数据行顺序第 0 行对应 θ0画面左右镜像数组下标顺序polar[iθ, ir]整幅图被转置成横条3.2 向量化双线性插值的完整实现import numpy as np def scan_convert(polar, r0, dr, th0, dth, out_hw, dxy, apexNone): polar : (n_th, n_r) 已做包络检波与对数压缩的极坐标数据 r0/dr : 径向起点与步长mm th0/dth: 角度起点与步长rad out_hw : 输出图像 (高, 宽) dxy : 输出像素间距mm/pixel apex : 扇形顶点在图像中的 (行, 列)默认底边中点 n_th, n_r polar.shape H, W out_hw cy, cx apex if apex is not None else (H - 1, W // 2) # 1) 输出像素 - 物理坐标mm u np.arange(W, dtypenp.float32) v np.arange(H, dtypenp.float32) xx, yy np.meshgrid((u - cx) * dxy, (cy - v) * dxy) # 2) 直角坐标 - 极坐标 r np.hypot(xx, yy) th np.arctan2(xx, yy) # 3) 物理坐标 - 极坐标索引浮点 fi (th - th0) / dth fj (r - r0) / dr # 4) 取整、取小数部分 i0 np.floor(fi).astype(np.int32) j0 np.floor(fj).astype(np.int32) a (fi - i0).astype(np.float32) b (fj - j0).astype(np.float32) # 5) 越界掩膜同时把索引夹到合法范围避免读越界 valid (i0 0) (i0 n_th - 1) (j0 0) (j0 n_r - 1) i0c np.clip(i0, 0, n_th - 2) j0c np.clip(j0, 0, n_r - 2) f00 polar[i0c, j0c] f10 polar[i0c 1, j0c] f01 polar[i0c, j0c 1] f11 polar[i0c 1, j0c 1] out (f00 * (1 - a) * (1 - b) f10 * a * (1 - b) f01 * (1 - a) * b f11 * a * b) out[~valid] 0.0 return out.astype(np.float32)四个数组f00/f10/f01/f11用花式索引一次性取出形状为 (H, W) 的四张图再做逐元素加权整段没有 Python 循环1280×800 的图在普通笔记本上几十毫秒能跑完。参数上的关键点有三个np.clip必须在索引之前做否则i0c 1会越过数组边界valid掩膜用逻辑与串起来一定要包含1之后的边界只判i0 0是不够的输出的 0 值代表扇形之外如果后面要做对数显示记得先加一个小量再取对数不然 log(0) 会给出 -inf。3.3 同一件事交给 cv2.remap 和 LUT 做import cv2 # 复用 3.2 中的 fi/fj注意 remap 的 map1 是列索引、map2 是行索引 map_x fj.astype(np.float32) # 对应 polar 的列即 r 方向 map_y fi.astype(np.float32) # 对应 polar 的行即 θ 方向 img cv2.remap(polar.astype(np.float32), map_x, map_y, interpolationcv2.INTER_LINEAR, borderModecv2.BORDER_CONSTANT, borderValue0.0)cv2.remap的参数顺序容易踩坑map_x是极坐标矩阵的列索引半径方向map_y是行索引角度方向和我们平时说的 x/y 是两回事写反了图像会被转置。borderMode选常量填充 0越界像素自动变黑等于自带扇形掩膜不用再手写valid。interpolation换成cv2.INTER_CUBIC就是三次插值代价大约是双线性的四倍访存。3.4 扇形掩膜与径向截止除了越界还有两个区域需要显式处理Rmax 之外没有数据即使索引在数组范围内也要置零扇形开角之外的像素同理。最省事的做法是在映射阶段就把r r_max的像素写进掩膜。mask (r r0 (n_r - 1) * dr) (np.abs(th - (th0 (n_th - 1) * dth / 2)) (n_th - 1) * dth / 2) out np.where(mask, out, 0.0)如果后续要做图像拼接或者定量测量建议把掩膜单独存一份布尔数组而不是依赖 0 值来判断背景因为真实的超声回波在小深度处也可能接近 0。4. 参数调不好就出伪影R-Θ 插值的误差来源与排错清单4.1 远场横向欠采样是扇形条纹的唯一主因相邻两条扫描线在深度 R 处的弧长是 R·Δθ这就是该深度处的横向采样间隔。R 100 mm、Δθ 0.7° 0.0122 rad 时弧长约 1.22 mm而输出像素间距 Δx 取 0.3 mmNyquist 要求采样间隔不大于 0.6 mm。差了两倍混叠就出现了表现为从顶点向外发散的放射状明暗条纹越远越明显。解决方向只有两个把 Δθ 采小或者在 θ 方向先插值加密。前者受脉冲重复频率和帧率约束后者是纯计算实际项目里多数选后者。4.2 插值核怎么挑一张对照表核每像素采样数表现适用场景最近邻1块状锯齿索引正确性一眼可见调试坐标系时用绝不出图双线性4略糊无振铃单调性好实时扫描转换的默认选择三次16更锐点目标旁瓣附近有轻微振铃离线精修、科研出图Lanczos-336最锐振铃最重极坐标域上采样选择逻辑很直白如果下游是给医生看的实时画面双线性足够糊一点比振铃好看如果是量点目标的主瓣宽度、做分辨率标定就得用三次否则量出来的宽度被双线性抹平会偏大百分之十几。4.3 先在 R-Θ 域上采样再做扫描转换这是抗混叠的正确顺序。先把 (nθ, nR) 沿 θ 方向加密再做反向映射等效于把 Δθ 降下来。from scipy.ndimage import zoom # 沿角度方向加密 4 倍径向保持原样 polar_up zoom(polar, (4, 1), order3, modenearest, prefilterTrue) dth_up dth / 4 # 后续 scan_convert 用这个新步长 # 如果担心三次插值的振铃可以先在 θ 方向做一次短窗低通 kernel np.array([0.25, 0.5, 0.25], dtypenp.float32) polar_lp np.apply_along_axis(lambda m: np.convolve(m, kernel, modesame), 1, polar)zoom的第二个参数是各维缩放因子(4, 1)表示只加密角度维order3用三次样条prefilterTrue是样条预滤波关掉它结果会明显偏软modenearest处理边界用零边界会把扇形最外侧两条线拉黑。代价是内存涨 4 倍、插值耗时涨若干倍但相比把硬件角度步长降到四分之一帧率直接掉到四分之一这笔账怎么算都划算。4.4 重建出问题时的排查顺序先用最近邻跑一遍。如果最近邻下扇形轮廓正确、只是有马赛克说明坐标变换对了问题在插值如果轮廓就是歪的先回去查顶点和角度定义。检查图像是否左右镜像。把数据矩阵的行序倒过来跑一次形状对上了就是行序约定不符。检查是否有斜向的直线状伪影。这通常是在射频域插值造成的把插值挪到包络检波之后。检查是否有大片 NaN 或 -inf。对数压缩在 0 值上取对数先加 1e-6 的底噪再压缩。检查远场条纹。按 4.1 的公式算一下 R·Δθ 和 2Δx超了就是欠采样不是 bug。5. 从点目标验证到课题汇报R-Θ 重建结果的自检与演示技巧5.1 用点目标 PSF 定量验证重建质量仿真一个点散射子按 2.3 的网格生成极坐标数据走完包络检波和扫描转换然后过点取剖面量主瓣宽度。import numpy as np def profile_width(img, row, col, thr0.5, axis1): 量 -6dB 主瓣宽度像素。img 为线性幅度图thr0.5 line img[row, :] if axis 1 else img[:, col] peak line.max() idx np.where(line peak * thr)[0] return (idx[-1] - idx[0] 1) if len(idx) else 0阈值取 0.5 是因为线性幅度图的 -6dB 点正好等于峰值的一半20·log10(0.5) ≈ -6 dB。如果图已经做过对数压缩阈值要改成peak - 6再反算回线性直接套 0.5 量出来的宽度会明显偏大。除了主瓣宽度还可以沿 θ 方向对图像做一次一维 FFT如果谱上出现与 Δθ 对应的尖峰就是角度欠采样的直接证据这一招在汇报里比嘴上说「感觉有伪影」有说服力得多。5.2 映射表固化与逐帧复用映射表只跟网格参数有关跟数据无关所以只要探头开角、深度、输出尺寸不变算一次就够。# 把 float32 映射表压缩成 16 位整数 小数部分访存减半 map1, map2 cv2.convertMaps(map_x, map_y, cv2.CV_16SC2) for frame in stream: # 逐帧只做一次 remap img cv2.remap(frame, map1, map2, cv2.INTER_LINEAR, borderModecv2.BORDER_CONSTANT, borderValue0.0) yield imgconvertMaps把映射量化到 1/32 像素对 8 位灰度显示来说完全看不出来但 remap 的访存量减半实测通常能快 1.5 到 2 倍。注意映射表一旦固化成 16 位换开角或改深度就必须重算否则整幅图会错位到无法解释。汇报页图生成方式网格对应R-Θ 网格与直角网格叠加示意图画等 r 弧和等 θ 射线插值核对比同一帧的最近邻/双线性/三次三联图改interpolation重跑点目标 PSF重建图 横向剖面曲线5.1 的profile_width分辨率指标不同深度的 -6dB 宽度表多深度批量量实时性固化映射表前后的帧率对比同一段数据计时一个实用的小技巧把convertMaps之后的映射表连同 Δθ、Δr、顶点坐标、开角一起序列化存盘np.save或cv2.FileStorage都行换机器、换语言重写重建模块的时候直接读表对齐能省掉一整轮坐标约定的反复对拍。本文还有配套的精品资源点击获取
返回列表