按 x、y、width、height 拼接图像切片为何出现错位?
结论
先检查切片的实际像素尺寸是否与文件名中的 width、height 一致。现有代码只用这两个值计算画布大小,没有按它们调整切片尺寸。canvas.paste(img, (x, y)) 会按 img.size 原样粘贴,因此切片像素尺寸与目标显示尺寸不同时,图片可能覆盖相邻区域,也可能被画布边界裁掉。
仅凭给出的 HTML 和代码,还不能确认这些矩形就是完整页面的图像分块。示例中的 <a> 可能只是可点击区域,其尺寸未必等于链接图片在报纸页面中的尺寸。需要先核对这一点,再决定是否按文件名中的矩形缩放图片。
代码里有哪些容易造成错位的地方?
图片尺寸没有参与定位计算
原代码执行的是:
tiles.append((img, round(x), round(y)))
这里没有保存目标宽高。粘贴时,即使文件名写着 738 × 148,只要图片的实际尺寸不同,Pillow 就不会自动把它缩放到 738 × 148。
可以先打印每张图片的实际尺寸,计算横向和纵向比例:
print(
name,
"actual:", img.size,
"target:", (w, h),
"ratios:", (img.width / w, img.height / h),
)
根据结果继续排查:
- 两个比例接近,且各切片的比例基本一致:可能存在统一的像素与 CSS 尺寸比例。
- 两个比例明显不同:需要核查坐标含义、裁切方式或链接图片是否经过重新排版,直接缩放可能拉伸内容。
- 实际尺寸与目标尺寸一致:继续检查坐标系、重叠关系和切片是否齐全。
尺寸比例只能提供线索,不能单凭它认定文件名的含义。
小数坐标被提前取整
输入中有 181.5、38.5 等坐标。直接使用 round() 会丢失半像素位置。Python 对恰好位于中间的值采用取偶规则,例如 round(38.5) 为 38,round(181.5) 为 182。
这可能造成局部一像素偏差。如果错位范围很大,应优先排查尺寸和坐标系。
这批坐标以整数和半整数表示,可以先按两倍比例生成画布,将坐标和尺寸都乘以 2,保留给定的半像素位置。这里的 2 是输出比例,并不代表已经确认网站使用两倍像素图片。
给出的矩形本身就存在重叠
例如,01_181.5_41_738_148.jpg 对应的纵向范围是 [41, 189),01_182.5_184_739_283.jpg 对应 [184, 467)。两者横向也有交集,因此这些坐标本身包含约 5 个 CSS 像素高的重叠区域。
即使严格按照这些矩形粘贴,图片之间也会发生覆盖。需要确认重叠是页面设计的一部分、点击区域的扩展,还是这些矩形根本不适合作为图像裁切边界。
确认坐标含义后,如何修改拼接代码?
下面的代码有一个前提:文件名中的 x、y、width、height 确实表示各张图片在同一页面坐标系中的目标矩形。
沿用问题中的 urls 列表,用下面的代码替换下载和拼接部分。代码会打印尺寸,按目标矩形缩放图片,并以两倍比例保留给定的半像素坐标。
from io import BytesIO
from urllib.parse import urlsplit
import requests
from PIL import Image
# 沿用问题中的 urls 列表。
scale = 2
rects = []
with requests.Session() as session:
for url in urls:
filename = urlsplit(url).path.rsplit("/", 1)[-1]
name = filename.rsplit(".", 1)[0]
_, x, y, w, h = name.split("_")
x, y, w, h = map(float, (x, y, w, h))
if x < 0 or y < 0 or w <= 0 or h <= 0:
raise ValueError(f"Invalid rectangle: {filename}")
response = session.get(url, timeout=30)
response.raise_for_status()
with Image.open(BytesIO(response.content)) as source:
img = source.convert("RGB")
print(
filename,
"actual:", img.size,
"target:", (w, h),
"ratios:", (img.width / w, img.height / h),
)
# 按左右、上下边界计算尺寸,避免分别取整产生矛盾。
left = round(x * scale)
top = round(y * scale)
right = round((x + w) * scale)
bottom = round((y + h) * scale)
if right <= left or bottom <= top:
raise ValueError(f"Empty output rectangle: {filename}")
rects.append((img, left, top, right, bottom))
canvas_w = max(right for _, _, _, right, _ in rects)
canvas_h = max(bottom for _, _, _, _, bottom in rects)
canvas = Image.new("RGB", (canvas_w, canvas_h), "white")
for img, left, top, right, bottom in rects:
target_size = (right - left, bottom - top)
if img.size != target_size:
img = img.resize(target_size, Image.Resampling.LANCZOS)
canvas.paste(img, (left, top))
canvas.save("output.png")
代码中的 Image.Resampling.LANCZOS 要求所用 Pillow 提供该枚举;旧环境需要使用对应版本支持的重采样常量。
输出采用 PNG,方便检查接缝,也能避免保存时再次引入 JPEG 有损压缩。PNG 不会恢复源 JPG 已经丢失的细节,两倍画布也只能保留布局精度,不保证增加图像细节。
如果仍然无法还原完整页面
需要回到页面实际的图像渲染方式,核对以下信息:
- 这些
<a>是否只是热区。查看报纸是否由一张完整的<img>、CSSbackground-image或其他图像层显示。如果有完整页图,优先使用该资源。 - 坐标是否属于同一个父容器。检查定位祖先、容器尺寸和
transform。不同局部坐标不能直接粘贴到同一张画布上。 - 切片是否覆盖整页。标题、页边、广告或其他区域可能由独立资源提供。仅凭这 12 个链接,无法确认资源是否齐全。
- 重叠区域按什么顺序显示。示例代码按列表顺序粘贴,后面的图片会覆盖前面的图片。如果页面依赖叠放顺序,应核对 DOM 顺序和
z-index;调整顺序本身不能解决坐标含义错误。
当前画布尺寸取自所有矩形的最大右边界和下边界,只能包住已知切片,不能证明它就是原报纸的完整尺寸。如果页面外侧还有留白或其他内容,需要另外取得整页宽高。
如果这些链接提供的是独立文章图片,而坐标只是原页面上的点击区域,按矩形缩放也无法保证还原原版面。此时需要完整页图,或真正对应版面裁切的图像资源和坐标。
备注:内容仅供参考。
