在iOS图像开发中,有时会拿到一类特殊画面:原本应该是直线街道的航拍图被处理成了以某点为中心的漩涡,或者老式滚筒扫描仪输出的文档呈现一圈圈同心圆变形。这类问题的统一解法是极坐标逆变换,也就是把图像从极坐标空间重新采样回直角坐标空间。Core Image框架虽然提供了部分极坐标相关滤镜,但默认多是正向变换,真正要把漩涡还原、同心圆拉直,还得自己写内核。

极坐标与直角坐标的转换原理
极坐标用半径r和角度θ描述一个点,直角坐标用x和y。两者关系为:x = cx + r * cos(θ),y = cy + r * sin(θ),其中(cx, cy)是极坐标圆心在图像中的像素位置。正向极坐标变换是把直角坐标映射到极坐标,常用于生成漩涡;逆变换则反过来,对于输出图像每一个直角坐标(x, y),先计算它相对圆心的r和θ,再去原极坐标图中对应位置取色。
在Core Image的自定义滤镜里,我们操作的其实是输出像素坐标。也就是说,内核函数收到的是目标直角坐标,需要反推源图极坐标位置。假设原图是一张已经漩涡化过的图,它本身存储的是某个极坐标展开后的样子,那么逆变换就是要让r对应原图的横向或纵向采样坐标,θ对应另一轴。这一步如果圆心设错,整张图就会像被撕裂一样错位。
插值方式也决定最终质量。最近邻采样速度快但边缘锯齿明显;双线性插值更平滑,在CIKernel中可通过手动读取四周像素并加权平均实现。很多初学者直接用默认采样导致同心圆校正后留有黑边,其实就是因为逆变换算出的源坐标超出原图范围,没有做边界钳制。
使用CIKernel编写逆变换滤镜
Core Image允许用Core Image Kernel Language(类似GLSL)编写CIKernel来处理像素。下面示例展示一个基础极坐标逆变换内核,把以图像中心为圆心的漩涡图还原。代码中sampler代表原图,destCoord是输出坐标。
kernel vec4 polarInverse(sampler src, vec2 center, float maxR) {
vec2 d = destCoord() - center;
float r = length(d);
float theta = atan(d.y, d.x);
// 将r映射到原图横向,theta映射到纵向
float u = r / maxR;
float v = (theta + 3.14159265) / 6.2831853;
vec2 srcCoord = vec2(u, v) * vec2(640.0, 480.0);
return sample(src, srcCoord);
}
上面代码把半径归一化到宽度,角度归一化到高度,再从源图采样。实际使用时要用CIColorKernel或CIKernel配合CIImage的imageByApplyingKernel:arguments:方法。若原图是正方形且圆心在正中央,maxR可取边长一半。
要注意的是,Core Image内核里所有坐标都是像素浮点值,且原点在左下角。这和UIKit的左上角原点不同,写圆心参数时记得转换。另外,若源坐标超出[0, 宽]或[0, 高],sample默认会按边缘像素填充,有时反而比黑边好看,但也可能模糊,需要按业务决定。
漩涡还原与同心圆校正实战对比
漩涡还原通常指图像曾被CIPolarParallax或自定义漩涡内核处理,像素沿圆心旋转且半径拉伸。逆变换时,除了r和θ映射,还要确认原漩涡内核的映射公式。如果原漩涡是把θ加上了r*k的偏移,逆变换就要先减掉再采样,否则出来的图仍是弯的。下面代码演示带旋转补偿的还原:
kernel vec4 swirlRestore(sampler src, vec2 center, float k, float maxR) {
vec2 d = destCoord() - center;
float r = length(d);
float theta = atan(d.y, d.x);
// 抵消原漩涡:原图theta' = theta + r*k
float origTheta = theta - r * k;
float u = r / maxR;
float v = (origTheta + 3.14159265) / 6.2831853;
vec2 srcCoord = vec2(u, v) * vec2(640.0, 480.0);
return sample(src, srcCoord);
}
同心圆校正多出现在文档扫描。纸张卷曲让横线变成圆弧,用逆变换把圆弧映射回直线即可。和漩涡不同,同心圆往往只需在垂直方向做r映射,水平方向保持原样。此时可把内核改成只调y坐标,x直接等于destCoord的x,性能更好。
从性能看,在iPhone上用CIKernel做单张1200万像素图逆变换约耗时30到50毫秒,而用CPU遍历写Accelerate的vImage约需200毫秒以上。若批量处理千张图,GPU内核优势明显,但需要注意内核里不要写复杂循环,否则编译慢且易超时。另一个坑是模拟器上CIKernel可能走CPU后端,测出的时间和真机差异极大,必须以真机为准。
常见错误与边界处理
第一类错误是圆心传错。比如UI层拿到的是左上角坐标(100, 80),直接丢进内核,但内核原点在左下,实际圆心变成(100, 高-80)。结果整图向对角偏移。正确做法是在OC层用CIImage的extent高度减去y再传参。
第二类错误是忽略长宽比。示例里把u、v直接乘固定640和480,若原图不是这个尺寸,采样会压扁。应从sampler的size动态获取,或用uniform传入原图宽高。第三类是黑边,逆变换后四个角r可能大于maxR,此时u大于1,采样越界。可在内核开头判断if (r > maxR) return vec4(0,0,0,0);透明处理,或者钳制u到1.0让边缘延伸,视场景选择。
最后提醒,Core Image的内核字符串在iOS版本间偶有兼容变动,建议把内核代码封装在一个CIFilter子类里,输入参数通过setValue:forKey:传递,避免每次拼接字符串。这样也方便在漩涡强度k变化时只改参数不动内核,提升维护效率。
Core_Image极坐标逆变换图像校正修改时间:2026-08-16 21:42:39