【Bug已解决】`transformers.image_transforms.resize` doesnot work for negative values 解决方案
【Bug已解决】transformers.image_transforms.resizedoesnot work for negative values 解决方案
一、现象长什么样
transformers.image_transforms.resize是图像预处理里把 PIL 图 resize 到目标尺寸的工具。当你传入负值作为目标尺寸时,行为异常:
# 现象 A:负值被当成"绝对像素"喂给 PIL,直接报错 ValueError: width and height must be > 0 # 传入 size=(-1, 64) 或 size=(64, -1),PIL.Image.resize 不接受负尺寸 # 现象 B:负值的"按比例"语义没实现,结果错 # 用户期望 size=(256, -1) 表示"宽 256、高按比例自动",但代码直接把 -1 当尺寸 # resize 失败或产出错误形状 # 现象 C:负坐标/负 crop 参数被静默接受,图像被裁错 # resize 内部先 crop 再 resize,crop 框含负坐标,PIL 行为未定义 -> 图像错位 # 典型触发 from transformers.image_transforms import resize img = resize(pil_image, size=(-1, 224)) # 期望高 224、宽按比例;实际报错最典型的指纹:用户用负值表达"某一维自动/按比例",但resize没有这套语义,把负值直接传给 PIL 导致报错或错误结果。
二、背景
图像 resize 在很多视觉 pipeline 里需要"只固定一维、另一维按比例"。常见约定(如torchvision.transforms.Resize):
size是单个 int → 短边缩放到该值,长边按比例;size是(h, w)元组,且允许其中一个为-1表示"该维自动按比例"。
但transformers.image_transforms.resize早期实现只支持"两维都是正 int"的简单情况,没有处理:
- 一维为
-1(按比例); - 负值(应当报错或解释为"相对/自动");
- resize 前的 crop 步骤里出现的负坐标。
于是负值要么被 PIL 拒绝(现象 A),要么被误当绝对尺寸(现象 B),要么在内部 crop 里产生未定义行为(现象 C)。
三、根因
根因有三类:
负值被直接传给 PIL.Image.resize。 PIL 的
resize((w, h))要求w,h > 0。image_transforms.resize没在调用 PIL 前把-1解析成"按比例",直接把(-1, 224)传进去 →ValueError: width and height must be > 0。缺少"一维 -1 自动"的语义解析。 正确做法:若
size=(256, -1),先算出当前图宽高比,把-1替换成round(256 * 原高/原宽)。原实现没做这步 → 现象 B。内部 crop 的负坐标未裁剪/校验。 resize 流程有时先
crop再 resize(如先居中裁剪再缩)。crop 框若含负坐标(如(box=[-10, 0, 100, 100])),PIL 行为依赖于版本,可能裁出错位图。应在 crop 前把负坐标 clamp 到[0, 边界]。
四、最小可运行复现
下面用纯 Python 模拟"负值直接传给 PIL 报错,而解析 -1 为按比例后正常":
from dataclasses import dataclass from typing import Tuple @dataclass class FakePIL: w: int h: int def pil_resize(img: FakePIL, size: Tuple[int, int]): w, h = size if w <= 0 or h <= 0: raise ValueError("width and height must be > 0") return FakePIL(w, h) def resize_buggy(img: FakePIL, size: Tuple[int, int]): """有 bug:直接把 size 传给 PIL。""" return pil_resize(img, size) def resolve_neg(size: Tuple[int, int], img: FakePIL) -> Tuple[int, int]: """修正:把 -1 解析成按比例。""" h, w = size if h == -1 and w != -1: h = round(img.h * w / img.w) elif w == -1 and h != -1: w = round(img.w * h / img.h) return h, w def resize_fixed(img: FakePIL, size: Tuple[int, int]): h, w = resolve_neg(size, img) return pil_resize(img, (h, w)) # 复现:size=(-1, 224) 直接传 PIL -> 报错 img = FakePIL(400, 300) try: resize_buggy(img, (-1, 224)) print("复现失败") except ValueError as e: print("复现成功(根因):", e) # 修正:解析 -1 为按比例 out = resize_fixed(img, (-1, 224)) print("按比例结果:", out.w, "x", out.h) # 宽 224,高应为 224 * 300/400 = 168 assert out.w == 224 and out.h == 168运行后,buggy 版因-1直接传给 PIL 而ValueError,fixed 版把-1解析成按比例(224×300/400=168),复现并修复了根因 1/2。
五、解决方案(第一层:最小直接修复)
最快的止血:在调用PIL.Image.resize前,先把-1解析成按比例的正尺寸,并对 crop 负坐标做 clamp:
from PIL import Image def safe_resize(image: Image.Image, size): """第一层修复:支持 (256, -1) / (-1, 256) 按比例,拒绝/修正负值。""" # size 兼容 int 与 (h, w) 元组 if isinstance(size, int): size = (size, size) h, w = size oh, ow = image.height, image.width # 解析 -1 -> 按比例 if h == -1 and w != -1: h = round(oh * w / ow) elif w == -1 and h != -1: w = round(ow * h / oh) elif h == -1 and w == -1: raise ValueError("size 两维不能同时为 -1") # 防御:其他负值直接报错(非 -1 的负值无明确定义) if h <= 0 or w <= 0: raise ValueError(f"解析后尺寸必须为正,得到 ({h}, {w})") return image.resize((w, h)) # 使用 img = Image.open("cat.jpg") resized = safe_resize(img, (-1, 224)) # 宽 224,高按比例第一层让用户立刻能用(256, -1)这类"一维自动"的写法,负值不再崩。
六、解决方案(第二层:结构性改进)
用ImageResizeNormalizer把"负尺寸解析 + crop 负坐标 clamp + 异常类型统一"收口:
from dataclasses import dataclass from typing import Tuple @dataclass class ImageResizeNormalizer: """统一的图像 resize 参数归一化:负号语义、crop 坐标校验。""" def normalize_size(self, size, img_hw: Tuple[int, int]) -> Tuple[int, int]: if isinstance(size, int): size = (size, size) h, w = size oh, ow = img_hw if h == -1 and w != -1: h = max(1, round(oh * w / ow)) elif w == -1 and h != -1: w = max(1, round(ow * h / oh)) if h <= 0 or w <= 0: raise ValueError(f"resize 目标尺寸必须为正,得到 {(h, w)}") return h, w def clamp_crop_box(self, box, img_hw: Tuple[int, int]) -> Tuple[int, int, int, int]: oh, ow = img_hw left, top, right, bottom = box # 负坐标 clamp 到 [0, 边界],避免 PIL 未定义行为 left = max(0, left); top = max(0, top) right = min(ow, right); bottom = min(oh, bottom) if right <= left or bottom <= top: raise ValueError(f"crop 框非法: {(left, top, right, bottom)}") return (left, top, right, bottom) # 使用 norm = ImageResizeNormalizer() h, w = norm.normalize_size((-1, 224), (300, 400)) box = norm.clamp_crop_box((-10, 0, 100, 100), (300, 400))ImageResizeNormalizer把"负尺寸语义 + crop 负坐标"收口,resize 调用方不再各自处理,避免遗漏。
七、解决方案(第三层:断言 / CI 守护)
用 pytest 固化"-1 按比例、非 -1 负值报错、crop 负坐标被 clamp":
import pytest def test_neg_one_means_proportional(): from img_resize_norm import ImageResizeNormalizer n = ImageResizeNormalizer() h, w = n.normalize_size((-1, 224), (300, 400)) assert w == 224 and h == 168 def test_both_neg_raises(): from img_resize_norm import ImageResizeNormalizer n = ImageResizeNormalizer() with pytest.raises(ValueError): n.normalize_size((-1, -1), (300, 400)) def test_non_neg_one_negative_raises(): from img_resize_norm import ImageResizeNormalizer n = ImageResizeNormalizer() with pytest.raises(ValueError): n.normalize_size((-5, 224), (300, 400)) # -5 不是 -1,无定义 def test_crop_negative_clamped(): from img_resize_norm import ImageResizeNormalizer n = ImageResizeNormalizer() box = n.clamp_crop_box((-10, 0, 100, 100), (300, 400)) assert box[0] == 0, "负 crop 坐标应被 clamp 到 0"CI 跑pytest tests/test_image_resize.py,以后只要有人又把负值裸传给 PIL,或忘了按比例解析,测试立刻红灯。
八、排查清单
当image_transforms.resize遇负值异常,按顺序查:
width and height must be > 0→ 负值被直接传给 PIL,用safe_resize先解析-1为按比例。- 想"只固定一维" → 用
(256, -1)/(-1, 256),确保有解析逻辑。 - 非
-1的负值(如-5)→ 无明确定义,应当报错而非静默。 - crop 后 resize 图像错位 → crop 框含负坐标,用
clamp_crop_box钳到[0, 边界]。 - 长期方案:用
ImageResizeNormalizer统一"负尺寸语义 + crop 校验"。
九、小结
"transformers.image_transforms.resizedoesnot work for negative values" 的根因是:resize 没有实现"一维-1表示按比例"的语义,把负值直接传给PIL.Image.resize(要求 >0)导致报错;同时内部 crop 的负坐标未被校验/clamp,产生未定义行为。
- 第一层:调用 PIL 前把
-1解析成按比例正尺寸,并对 crop 负坐标 clamp,立刻可用(256, -1)。 - 第二层:用
ImageResizeNormalizer统一"负尺寸语义 + crop 坐标校验",避免遗漏。 - 第三层:pytest 断言"-1 按比例、非 -1 负值报错、crop 负坐标 clamp",防止回归。
记住:图像 resize 的负值应只用于"一维自动(-1 按比例)"这一约定,且必须在调用 PIL 前解析成正数;其它负值无定义,应明确报错而非静默传给底层库。