- 数据增强
- 计算机视觉
- 图像处理
- 机器学习
【免费下载链接】imgaug
Image augmentation for machine learning experiments.
本篇围绕 imgaug 0.4.0 周期内的一份缺陷修复记录展开:Affine增强器的translate_px参数在传入连续型随机参数(stochastic parameter)时,其实际行为会退化为按图像宽高百分比(即translate_percent的语义)平移;反之,translate_percent传入离散型随机参数时又会按像素平移。读完本文,你能理解该缺陷的类型判定根因、imgaug 0.4.0 的修复方案(按参数名而非参数类型决定平移模式),并结合仓库源码与回归测试验证正确的调用方式。
一、缺陷记录原文与问题本质
该修复记录位于 20191128_fixed_affine_translate.md,原文(对应 issue #508)如下:
Fixed `Affine` parameter `translate_px` behaving like `translate_percent` if a continuous stochastic parameter was provided. Analogously `translate_percent` would behave like `translate_px` if a discrete stochastic parameter was provided.翻译成开发语言就是:平移模式(px 还是 percent)的判定结果,错误地取决于用户传入的随机参数的“类型”,而不是用户显式指定的参数名。
- 用户写
translate_px=iap.Uniform(10, 20)(连续参数),期望每幅图像在 x、y 轴上随机平移 10~20像素; - 旧实现却把它识别为
percent模式,实际变成平移图像宽/高的 1000%~2000%,平移量被放大成千上万倍; - 反过来,
translate_percent=iap.DiscreteUniform(1, 10)(离散参数)本应按百分比解释,旧实现却按像素处理,导致平移量过小、几乎不可见。
这种 bug 的隐蔽之处在于:确定性数值和常见元组写法(如translate_px=(-16, 16))不受影响,只有显式传入StochasticParameter对象(Uniform、Deterministic、Binomial等)时才触发,因此极易漏测。
二、imgaug 中 translate_px 与 translate_percent 的语义差异
在讨论修复之前,先明确这两个参数的正确语义,这也是 Affine 类文档中定义的行为:
translate_px(像素平移),见 imgaug/augmenters/geometric.py:
None:等价于0(除非translate_percent有值);- 单个整数:所有图像使用同一平移量;
- 元组
(a, b):从离散区间[a..b]每图均匀采样一个整数,x、y 轴共用该样本; - 列表:每图从列表中随机取一个值;
StochasticParameter:每图从该参数采样一个值,x、y 轴共用;- 字典
{"x": ..., "y": ...}:两轴各自独立采样,允许两轴取不同值。
translate_percent(比例平移),见 imgaug/augmenters/geometric.py:语义与上面对应,但数值表示“轴尺寸的分数”,0表示不变,0.5表示平移半个轴长;其采样走连续分布。
典型用法(均出自 Affine 类文档示例):
import imgaug.augmenters as iaa import imgaug.parameters as iap # 固定平移:x、y 各 16 像素,新像素填 0(黑) aug = iaa.Affine(translate_px=16) # 比例平移:x、y 各平移 10% 的宽/高 aug = iaa.Affine(translate_percent=0.1) # 离散随机:每图从 [-16..16] 整数区间采样一次,两轴共用 aug = iaa.Affine(translate_px=(-16, 16)) # 字典形式:两轴独立采样 aug = iaa.Affine(translate_px={"x": (-16, 16), "y": (-4, 4)}) # 显式随机参数:每图从连续区间 [10, 20) 采样,单位是像素而非百分比 aug = iaa.Affine(translate_px=iap.Uniform(10, 20))需要特别指出的是,imgaug 的参数系统存在两个“平行宇宙”:
- imgaug/parameters.py 中的
handle_continuous_param:单值 →Deterministic,元组 →Uniform(连续),列表 →Choice,StochasticParameter原样保留; - imgaug/parameters.py 中的
handle_discrete_param:单值 →Deterministic(int(param)),元组 →DiscreteUniform(离散),列表 →Choice且元素取整,并可通过allow_floats拒绝浮点输入。
这两套解析器产出的参数对象类型不同(UniformvsDiscreteUniform),正是旧 bug 得以发生的“类型指纹”来源。
三、修复后的实现:模式由参数名决定,与参数类型无关
当前仓库中,Affine.__init__在 imgaug/augmenters/geometric.py 将两个平移参数统一交给类方法_handle_translate_arg处理:
self.translate = self._handle_translate_arg( translate_px, translate_percent)_handle_translate_arg的完整逻辑见 imgaug/augmenters/geometric.py,其判定流程如下:
- 若
translate_percent与translate_px均为None,则回退为translate_px = 0; - 断言二者只能有一个非
None,否则会抛出AssertionError: Expected either translate_percent or translate_px to be provided...; translate_percent非None:走比例分支。- 字典输入时断言必须含
x或y键,缺失的轴取0,两轴分别经iap.handle_continuous_param(..., tuple_to_uniform=True, list_to_choice=True)解析,返回(x_param, y_param, "percent"); - 非字典输入时,整个参数经
handle_continuous_param解析,返回(param, None, "percent")——第二个元素None表示 y 轴与 x 轴共用同一参数与样本;
- 字典输入时断言必须含
- 否则(
translate_px非None):走像素分支,结构与上面对称,但解析器换成iap.handle_discrete_param(..., allow_floats=False),并标记allow_floats=False以拒绝浮点输入,返回模式"px"。
关键在于第 3、4 步的分叉条件:if translate_percent is not None: ... else: ...。也就是说,模式字符串"percent"/"px"只由“用户把值填给了哪个参数名”决定,与填入的是整数、元组、列表还是任何StochasticParameter无关。这恰好消除了缺陷记录中描述的两种错配:
translate_px=iap.Uniform(10, 20):无论Uniform是连续型参数,模式都稳定为"px";translate_percent=iap.DiscreteUniform(1, 10):无论其是离散型,模式都稳定为"percent"。
修复后self.translate的结构是三元组(x, y, mode)。这一结构变化本身也有一份独立的 0.4.0 变更记录佐证:20191128_affine_translate.md 指出Affine.get_parameters()从此始终返回(x, y, mode)元组,其中mode为px或percent,当两轴共用同一参数时y可为None。从源码结构看,三元组的第三个元素mode就是 bug 修复的落点——旧实现中该值可由参数类型推导,修复后它成为由参数名锁定的不可变标记,随增强器一起参与序列化(get_parameters/restore_from_parameters)。
四、样本如何被消耗:从 draw_samples 到仿射矩阵
修复只完成了“语义归类”的一半,另一半是采样与矩阵生成阶段确实按标记执行。完整调用链如下:
采样:
Affine._draw_samples(imgaug/augmenters/geometric.py)对每个图像样本抽取平移量:- 若
self.translate[1](y 轴参数)非None,x、y 各自独立调用draw_samples,得到两个独立数组; - 否则仅对 x 参数采样一次,然后把同一份样本复制给两轴(
translate_samples = (translate_samples, translate_samples)),保证“元组/单值输入两轴共用样本”的文档承诺; - 结果连同
translate_mode=self.translate[2]一起封装进_AffineSamplingResult(imgaug/augmenters/geometric.py)。
- 若
矩阵生成:
_AffineSamplingResult.get_affine_parameters(imgaug/augmenters/geometric.py)在每张图构造仿射参数时按translate_mode分叉:
if self.translate_mode == "percent": translate_y_px = translate_y * arr_shape[0] # 分数 × 输出图高 translate_x_px = translate_x * arr_shape[1] # 分数 × 输出图宽 else: # px 模式:对输入/输出尺寸不一致(如热图、低分辨率分割图)做比例换算 translate_y_px = (translate_y / image_shape[0]) * arr_shape[0] translate_x_px = (translate_x / image_shape[1]) * arr_shape[1]随后to_matrix(imgaug/augmenters/geometric.py)将平移像素值喂给_AffineMatrixGenerator.translate,与旋转、缩放、错切、中心化偏移等组合成最终的 3×3 仿射矩阵,再交给skimage/cv2后端执行 warp。旧 bug 的影响点正是在第一处分支:translate_px=iap.Uniform(10, 20)被误标为"percent"后,采样值 10~20 会被乘以图像宽/高,1000×1000 的图像上等价于平移 10000~20000 像素——图像几乎整体移出画面。
- 可复现性:
_draw_samples使用random_state.duplicate(12)把随机源切分为各参数专属的流(平移占rngs[3]~rngs[5]),因此同一seed下翻译样本序列完全可复现,这也是测试能稳定断言平移区间的底层保障。
五、配套回归测试:把“像素即像素”钉死
该缺陷在仓库测试中有直接对应的回归用例,见 test/augmenters/test_geometric.py:
def test_keypoints_with_continuous_param_results_in_absolute_shift(self): # This test ensures that t ~ uniform(a, b) results in a translation # by t pixels and not t% # see issue #505 # use iap.Uniform() here to ensure that is really a float value that # is sampled and not accidentally DisceteUniform aug = iaa.Affine(translate_px=iap.Uniform(10, 20)) kps = [ia.Keypoint(x=10, y=10)] kpsoi = ia.KeypointsOnImage(kps, shape=(1000, 1000)) for _ in np.arange(5): kpsoi_aug = aug.augment_keypoints(kpoii) kp_aug = kpsoi_aug.keypoints[0] assert 10 + 10 <= kp_aug.x <= 10 + 20 assert 10 + 10 <= kp_aug.y <= 10 + 20(注:上面代码块中aug.augment_keypoints(kpoii)一处为笔误,仓库源码中实际是kpsoi_aug = aug.augment_keypoints(kpsoi)。)
这个用例的设计很有针对性:
- 用 1000×1000 的大图,使“误当百分比”的放大效应达到 100 倍量级,断言区间
[20, 30]一旦被 percent 模式污染必然失败; - 刻意选
iap.Uniform而非DiscreteUniform,注释明确说明是为了确保采样到的真的是浮点连续值,排除“碰巧走了离散分支”的假阴性; - 对关键点(keypoint)而非图像像素做断言,避开插值误差,直接检验平移量的绝对值。
此外,参数解析层的测试也分别锁定了两个参数各自应保持的参数类型与模式标记(test/augmenters/test_geometric.py):
def test___init___translate_percent_is_stochastic_parameter(self): aug = iaa.Affine(translate_percent=iap.Uniform(0.7, 0.9)) ... assert aug.translate[2] == "percent" def test___init___translate_px_is_stochastic_parameter(self): aug = iaa.Affine(translate_px=iap.DiscreteUniform(1, 10)) ... assert aug.translate[2] == "px"其中aug.translate为三元组,translate[0]是 x 轴参数、translate[1]是 y 轴参数(共用时为None)、translate[2]是模式标记——与修复后get_parameters()的返回结构一一对应。
六、工程启示与使用建议
从这次修复可以提炼出几条对 imgaug 使用者和 imgaug 贡献者都有价值的经验:
- 优先用参数名表达意图,用参数系统表达分布。
translate_px=iap.Uniform(10, 20)与translate_px=(-10, 20)的区别仅在于前者是连续分布(可取小数样本,经矩阵计算后体现为亚像素级位移)、后者是整数离散区间;修复后二者的单位语义都严格是“像素”,不会再因参数类型漂移。 translate_px拒绝浮点常量,translate_percent接受任意数值。前者经handle_discrete_param(allow_floats=False)解析,传入translate_px=16.5这类浮点会被断言拒绝;若确实需要连续像素位移,应显式传iap.Uniform或iap.Deterministic(16.5)等StochasticParameter对象。- 模式标记参与序列化,pickle 兼容性依赖该结构。
get_parameters()返回的(x, y, mode)是增强器持久化/反序列化的组成部分,从 0.4.0 之前旧版本迁移的增强器对象若依赖旧的二元组结构,反序列化行为需以 0.4.0 的结构为准(见 20191128_affine_translate.md)。 - 同类处理也覆盖了 AffineCv2 等兄弟类。从源码结构看,
AffineCv2.__init__拥有与_handle_translate_arg等价的内联实现(imgaug/augmenters/geometric.py),同样区分handle_continuous_param(percent 分支)与handle_discrete_param(allow_floats=False)(px 分支),对应测试位于 test/augmenters/test_geometric.py。
适用前提与限制:本文所有结论基于当前仓库(0.4.0 修复已完成的状态,含后续 0.5.0 周期的少量重构标记),缺陷本身的旧版行为描述以 changelog 原文为依据;fit_output=True会“抵消”平移与缩放的效果(见 Affine 文档),若依赖绝对平移量请勿开启该选项,或将其包在KeepSizeByResize中统一处理输出尺寸。
综上,issue #508 的修复看似只改动了模式判定一处,实则为 imgaug 的随机参数系统树立了一条清晰边界:用户接口(参数名)决定语义单位,参数对象只决定概率分布。理解了这条边界,再读 Affine 的参数文档 与 _AffineSamplingResult 的换算逻辑,就能准确预测任意translate_px/translate_percent写法在每幅图像上产生的真实位移。
- 数据增强
- 计算机视觉
- 图像处理
- 机器学习
【免费下载链接】imgaug
Image augmentation for machine learning experiments.
相关推荐
imgaug 0.4.0 改造解析:`Affine.get_parameters()` 与 `translate_px`/`translate_percent` 平移参数的标准化
imgaug 0.4.0 改造解析: Affine.get_parameters 与 translate_px / translate_percent 平移参数
数据增强计算机视觉图像处理机器学习imgaug 0.4.0 图像归一化崩溃修复解析:当 augment() 输出从 ndarray 变成 list
imgaug 0.4.0 图像归一化崩溃修复解析:当 augment 输出从 ndarray 变成 list 在 imgaug 0.4.0 版本中, augme
数据增强计算机视觉图像处理机器学习imgaug 0.4.0 弃用 AffineCv2:用 Affine(backend='cv2') 统一仿射变换后端
imgaug 0.4.0 弃用 AffineCv2:用 Affine backend='cv2' 统一仿射变换后端 imgaug 在 0.4.0 版本中正式弃用
数据增强计算机视觉图像处理机器学习
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考