PAYATHON 2026

按 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 已经丢失的细节,两倍画布也只能保留布局精度,不保证增加图像细节。

如果仍然无法还原完整页面

需要回到页面实际的图像渲染方式,核对以下信息:

  1. 这些 <a> 是否只是热区。查看报纸是否由一张完整的 <img>、CSS background-image 或其他图像层显示。如果有完整页图,优先使用该资源。
  2. 坐标是否属于同一个父容器。检查定位祖先、容器尺寸和 transform。不同局部坐标不能直接粘贴到同一张画布上。
  3. 切片是否覆盖整页。标题、页边、广告或其他区域可能由独立资源提供。仅凭这 12 个链接,无法确认资源是否齐全。
  4. 重叠区域按什么顺序显示。示例代码按列表顺序粘贴,后面的图片会覆盖前面的图片。如果页面依赖叠放顺序,应核对 DOM 顺序和 z-index;调整顺序本身不能解决坐标含义错误。

当前画布尺寸取自所有矩形的最大右边界和下边界,只能包住已知切片,不能证明它就是原报纸的完整尺寸。如果页面外侧还有留白或其他内容,需要另外取得整页宽高。

如果这些链接提供的是独立文章图片,而坐标只是原页面上的点击区域,按矩形缩放也无法保证还原原版面。此时需要完整页图,或真正对应版面裁切的图像资源和坐标。

备注:内容仅供参考。