全景图高清下载这件事,说白了就一句话:平台从来不会一次性把一整张大图发给你,它只会一小块一小块地喂给你的浏览器,你要做的就是把喂过来的小块按原样拼回去。听起来简单,真正动手的人十有八九会卡在三个地方——不知道图被切成了什么形状、不知道切了多少层、拼的时候行列顺序搞反了。这篇就把这三件事从头到尾捋一遍,顺带把等距柱状投影、立方体贴图、瓦片金字塔这几套底层逻辑讲清楚。标题里提到的那几个平台,我不逐个点名,按实现方式归成四类讲,因为它们的底层套路高度重合,你把这四类吃透,剩下的基本都是换汤不换药。适合两类人看:一类是想把自己账号里的项目素材导出来做备份的设计师、摄影师;另一类是单纯好奇这玩意儿怎么运作、想拿公开可访问的素材练手的技术爱好者。前提说在前头——只动你自己有授权的、或者明确公开可获取的内容,别人花钱买的服务和版权作品,别碰。
1. 先把"全景图"这件事拆开看:三种投影与两套切图逻辑
1.1 等距柱状投影和立方体贴图,决定了你最后能拼出什么形状
很多人第一次下载全景图,拼完之后发现图片是个歪歪扭扭的长条或者六宫格,以为是拼错了,其实是投影方式没搞明白。全景图的原始数据本质上是"球面上的一圈颜色信息",要摊平成二维图片,就得选一种投影方式,行业里主流就两种。
第一种叫等距柱状投影(Equirectangular),你直接理解成"把地球仪剪开摊平"就行。它的特征是宽高比严格 2:1,横向代表 360° 经度,纵向代表 180° 纬度,越靠近上下两端拉伸越严重——所以你在这种图的最顶上会看到天空被拉成一条横带,最底下地面糊成一片。这是绝大多数全景相机(消费级和准专业级)直接输出的格式,也是后期做 VR 视频、导入三维软件的标准输入格式。
第二种叫立方体贴图(Cubemap),思路是拿一个正方体把观察点罩住,往六个面各投影一次,得到 6 张 90° 视场角的正方形图。它的好处是每个方向的分辨率分布均匀,渲染时 GPU 采样效率高,所以大量 WebGL 全景播放器用的都是这套。缺点是它不像 2:1 那样一眼能看懂,六个面的朝向、旋转、拼接顺序全靠约定,约定错了就镜像、就错位。
还有第三种相对少见的球面投影/鱼眼原始图,那是相机传感器的原始输出,一般不会直接出现在网页端,这里不展开。你只要记住一条判断法则:拿到手的图如果是 2:1 比例的长条,就是等距柱状;如果是若干个正方形、且数量是 6 或者 6 的倍数,就是立方体面。这条法则后面会反复用到。
1.2 瓦片金字塔:平台为什么不肯一次给你一张大图
假设一张全景原图是 12000×6000 像素,存成 JPEG 高质量大概 8 到 15MB。听起来不算大,但一个页面里可能挂十几个场景,手机端一进页面就要拉一百多兆,谁都受不了。所以平台的做法是切片 + 分级,术语叫瓦片金字塔(Tile Pyramid)。
原理特别朴素:把大图按固定边长(常见 256、512、1024 像素)切成棋盘格,这是最底下的第 N 层;然后把这一层 4 张拼成 1 张、降采样一半,得到第 N-1 层;如此反复,直到整张图缩成 1 张或者 2 张小图为止。加载的时候,播放器先请求最顶层那张模糊图垫底,等你转动视角,再按当前视场角算出来需要哪几块,逐块请求高清瓦片盖上去。这就是你转动画面时"先糊后清"的原因。
这里有个关键点很多人忽略:每一层的瓦片数量是 4 的幂次增长。第 0 层 1 张,第 1 层 4 张,第 2 层 16 张,第 3 层 64 张,第 4 层 256 张,第 5 层 1024 张。也就是说,你从第 3 层换成第 4 层,请求量直接翻 4 倍。这就是为什么很多平台的"最高清"其实只切到第 4 层或者第 5 层——再往上切,存储和带宽成本是指数级爆炸的,而用户肉眼在常规屏幕尺寸下根本分辨不出差别。
反过来说,这也给了你一个判断依据:如果你在请求列表里看到的最高层级只有 3 或 4,那这平台给你的"高清"其实是有天花板的,别指望拼出 1 万像素宽的图。
1.3 先判断类型再动手:一张对照表
动手之前先花两分钟做类型判断,能省掉后面半小时的瞎折腾。下面这张表是我自己总结的分类法,覆盖了标题里提到的几类平台:
| 平台族 | 典型投影 | 常见瓦片边长 | 最高层级 | 垂直方向是否裁切 | 鉴权方式 |
|---|---|---|---|---|---|
| 云设计出图类 | 立方体面为主 | 512 | 3 到 4 层 | 通常裁掉顶底各约 15° | 会话 Cookie |
| 720°漫游托管类 | 等距柱状为主 | 512 | 4 到 5 层 | 一般不裁 | 部分带时效签名 |
| 街景类服务 | 立方体面 | 512 或 1024 | 4 到 5 层 | 上下裁切明显 | 短期 token |
| VR 看房类 | 立方体面 + 深度图 | 1024 | 5 到 6 层 | 裁切较少 | 会话 + 设备指纹 |
表里最右边那列要特别注意。带时效签名或者短期 token 的平台,瓦片地址几分钟就失效,不适合大批量抓取,你就算拿到地址也下不全。这种情况我的建议很直接:走官方导出、找项目负责人要源文件,或者用平台自带的"下载全景图"按钮(不少平台其实藏了这个功能,只是入口很深)。技术上能不能绕是一回事,该不该绕是另一回事,后面第 6 章会专门聊这个。
至于垂直方向裁切这一列,很多人踩坑就在这里。街景类服务因为拍摄时云台角度限制,天顶和地面往往没有有效像素,平台索性把上下各裁掉一块,所以你拼出来的立方体面不是完整的 90° 视场,转成等距柱状之后上下会有黑边。这不是你操作错了,是数据本身就没有。
2. 动手前的判断与准备:从页面到请求的三步走
2.1 打开面板,先看图片请求的"体量特征"
不管你用哪个浏览器,第一步永远是打开开发者工具切到网络面板,然后刷新页面、拖动视角转一圈。这一步的目的不是马上找地址,而是看请求的体量分布。
正常的瓦片加载长这样:页面初始化阶段有一批小请求(几 KB 到几十 KB),那是低层级垫底图;你开始拖动之后,会出现一批尺寸相近、大小相近的请求(几十 KB 到两百 KB),那是当前视场角内的高层级瓦片。如果你看到的是几个巨大的请求(1MB 以上),恭喜,这平台可能压根没切片,直接给整图,那就简单了,右键保存完事。
如果看到的是几百个小请求、而且命名高度规律,那就是标准瓦片流,进入下一节。这里有个实操技巧:在网络面板里按类型筛成"图片",再按大小排序,通常能一眼看出哪些是瓦片、哪些是图标的干扰项。图标一般是 16×16 到 64×64 的 PNG,瓦片是 512×512 的 JPG,尺寸列一看就分开了。
提示:别在页面刚加载完就急着复制地址。一定要拖动视角转一整圈,让播放器把各个方向的瓦片都请求一遍,这样你才能收集到完整的地址样本,也才能看出层级的切换规律。
2.2 识别 URL 命名规律:四种常见套路
瓦片的 URL 命名看起来五花八门,归纳下来基本逃不出四种模式。认出来模式,你就能自己造地址批量下载,不用一个个手点。
第一种,三维坐标式:/tiles/{level}/{y}/{x}.jpg或者/tiles/{level}/{x}/{y}.jpg。这是最标准的,关键是要判断哪个参数是行、哪个是列。判断方法很简单:拖动视角水平转,观察变化的是哪个数字;再上下看,观察另一个。水平转时变化的是列(x),上下看时变化的是行(y)。
第二种,扁平拼接式:/l3/2_5.jpg或/pano_3_2_5.jpg。参数用下划线连在一起,顺序通常是层级_行_列。这种要小心,有些平台是层级_列_行,拼错了就是整体转置的图。
第三种,哈希分片式:/a3/f9/a3f9c2...jpg。这种是内容寻址,路径跟行列没关系,只能靠 Manch 抓全量列表,没法自己造地址。遇到这种基本只能靠录制 HAR 然后批量提取。
第四种,立方体面式:/cube/{face}/{level}/{y}_{x}.jpg,其中 face 是px、nx、py、ny、pz、nz这类命名,或者0到5、f、r、b、u、d这类简写。这种要下六遍,每面单独成一套。
识别的时候还有个小窍门:把某个瓦片地址单独在新标签页打开,如果能正常显示,说明没有强 Referer 校验,后面脚本下载会顺利很多;如果返回 403 或者一张"禁止盗链"的占位图,那就得补上 Referer 头,或者干脆放弃批量方案。
2.3 层级和瓦片总数的估算方法
这一节讲计算,是为了让你在下之前就知道"应该下多少、下到第几层够用",避免下完了发现还差一半。
先算理论最高层级。设原图宽度为W(等距柱状)或单面边长为S(立方体),瓦片边长为T,则最高层级L满足:
T × 2^L ≥ W → L = ceil(log2(W / T))举个例子:原图 12000 像素宽,瓦片 512,则 12000 / 512 = 23.4,log2(23.4) ≈ 4.55,向上取整得L = 5。也就是最高层是第 5 层,这一层的瓦片边长合计 512 × 32 = 16384 像素,略大于原图宽度,属于正常的填充留白。
再算瓦片总数。第k层的网格边长是2^k,瓦片数是4^k;从第 0 层加到第L层:
Total = (4^(L+1) - 1) / 3还是上面那个例子,L = 5,Total = (4^6 - 1) / 3 = (4096 - 1) / 3 = 1365张。按每张平均 80KB 算,总共约 109MB。这个量级你心里得有数——如果你只想拼一张 8000 像素宽的图,其实下到第 4 层就够了(网格 16×16,合计 8192 像素),第 5 层的 1024 张纯属浪费。
立方体面要乘 6。设单面边长S = 4096,瓦片 512,则L = ceil(log2(4096/512)) = 3,单面瓦片数(4^4-1)/3 = 85,六个面共 510 张,最终能拼出 4096×4096 的六个面,转成等距柱状后宽度约 16384 像素——这个尺寸打印 2 米宽的展板都没问题。
3. 瓦片下载与拼接的完整实操
3.1 抓取清单:从 HAR 到纯文本 URL 列表
手动复制几百个地址是不现实的,第一步是把网络请求导出成文件。开发者工具的网络面板一般都有"导出 HAR"的功能(右键请求列表就能看到),导出来的是一份 JSON,里面记录了所有请求的 URL、响应头、状态码。
拿到 HAR 之后,我一般用一段小脚本把瓦片地址提出来、去重、排序,生成一份干净的清单:
import json, re with open("pano.har", encoding="utf-8") as f: har = json.load(f) urls = set() for entry in har["log"]["entries"]: u = entry["request"]["url"] # 只保留瓦片特征明显的地址,按需改正则 if re.search(r"/tiles?/\d+/\d+/\d+\.(jpg|jpeg|png|webp)", u, re.I): urls.add(u) urls = sorted(urls) with open("tiles.txt", "w", encoding="utf-8") as f: f.write("\n".join(urls)) print("收集到", len(urls), "条")跑完看一眼条数,对照上一节算出来的理论总数,能对上六七成就算正常——因为用户通常只会把视角转一圈,不会把全部瓦片都触发。缺的部分靠地址规律自己补齐,补齐的方式就是下一节的循环。
注意:WEBP 格式现在很常见,体积只有 JPG 的三到五成,但拼接时要注意
PIL需要额外装支持库,不然打开会报错。遇到这种情况我一般先转成 PNG 再拼,稳妥。
3.2 批量下载脚本与并发控制
地址规律一旦确认,就可以自己造地址循环下载了。下面这个是双循环的骨架,改一下 URL 模板和层级范围就能用:
#!/bin/bash BASE="https://example.com/pano/tiles" LEVELS=(3 4) for L in "${LEVELS[@]}"; do GRID=$((1 << L)) mkdir -p "tiles/l${L}" for ((y=0; y<GRID; y++)); do for ((x=0; x<GRID; x++)); do OUT="tiles/l${L}/${y}_${x}.jpg" [ -s "$OUT" ] && continue # 断点续传:已有且非空就跳过 curl -s --max-time 20 \ -H "Referer: https://example.com/" \ -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)" \ -o "$OUT" \ "${BASE}/${L}/${y}/${x}.jpg" sleep 0.05 done done done几个参数值得说一下为什么这么设。--max-time 20是防止某个瓦片卡住导致整个脚本挂死,超过 20 秒直接放弃,后面补下。sleep 0.05是给自己留个体面,纯串行下载 1000 多张小图其实也就几分钟,没必要开 50 并发把人家服务器打满——真被打满了轻则限流返回占位图,重则整个 IP 段被临时拒绝,得不偿失。[ -s "$OUT" ] && continue这行是断点续传的关键,下载中断后重跑脚本不会重复拉已经下好的文件,这个技巧在下载 1000 张以上时能省大量时间。
如果你的平台瓦片特别多(比如 4000 张以上),可以把串行改成xargs -P 4的四并发,实测速度提升明显而且不容易触发限流。但并发数别超过 8,超过之后收益递减、风险陡增。
3.3 拼接还原:两种算法与常见错位修正
下载完之后,目录结构大概是这样:tiles/l4/下面有 256 张行_列.jpg。拼接的逻辑就是开一张网格边长 × 瓦片边长的画布,按行列往对应位置贴。
from PIL import Image import os LEVEL = 4 GRID = 1 << LEVEL # 16 TILE = 512 OUT_W = GRID * TILE # 8192 canvas = Image.new("RGB", (OUT_W, OUT_W), (0, 0, 0)) missing = [] for y in range(GRID): for x in range(GRID): p = f"tiles/l{LEVEL}/{y}_{x}.jpg" if not os.path.exists(p) or os.path.getsize(p) < 1024: missing.append((y, x)) continue canvas.paste(Image.open(p).convert("RGB"), (x * TILE, y * TILE)) canvas.save(f"pano_L{LEVEL}.jpg", quality=95, subsampling=0) print("拼完,尺寸", canvas.size, "缺块", len(missing)) for m in missing[:20]: print("缺:", m)这里有个容易忽略的细节:quality=95, subsampling=0。PIL 默认的 JPEG 保存参数是quality=75加上色度二次采样,对于全景图这种大面积渐变(尤其是天空)来说,二次采样会产生明显的色块和边缘溢出。我试过同一个源拼出来,subsampling=0保存的文件大 20% 左右,但天空的过渡干净太多,值。
拼接最容易出的问题是行列转置和Y 轴翻转。判断方法很直接:如果拼出来的图看起来"内容对但方向不对",比如地平线变成竖的,那就是行列搞反了,把paste的坐标换成(y * TILE, x * TILE)再试;如果拼出来是上下颠倒的,那是 Y 轴方向相反,把行号取反(GRID - 1 - y)即可。
还有一种是瓦片本身带重叠边,个别平台为了做无缝衔接,每张瓦片右侧和下侧多带了 8 到 16 像素的重叠区。这种情况你直接贴会出现规律的接缝带,处理方式是crop掉重叠部分再贴:
OVERLAP = 8 # 先试 8,接缝消失就对了 img = Image.open(p).convert("RGB") if x < GRID - 1: img = img.crop((0, 0, TILE - OVERLAP, img.height)) canvas.paste(img, (x * (TILE - OVERLAP), y * TILE))重叠量怎么确定?放大看接缝处,数一下重复的像素宽度就知道了。多数情况下是 0(无重叠)或者瓦片边长的 1/32、1/64。
3.4 立方体展开图转等距柱状:remap 的写法
如果你拼出来的是六张正方形,而你需要的是 2:1 的标准等距柱状图(导入三维软件、做 VR 视频都得用这个格式),就需要做一次投影变换。思路是:遍历输出图的每一个像素,算出它在球面上的方向向量,再判断这个方向落在立方体的哪个面上,取出对应面的像素颜色。
import cv2 import numpy as np FACE = 2048 OUT_W = FACE * 4 OUT_H = OUT_W // 2 # 六个面的读取顺序:px, nx, py, ny, pz, nz names = ["px", "nx", "py", "ny", "pz", "nz"] tex = {n: cv2.imread(f"cube/{n}.jpg") for n in names} lon = np.linspace(-np.pi, np.pi, OUT_W, endpoint=False) lat = np.linspace(np.pi / 2, -np.pi / 2, OUT_H, endpoint=False) LON, LAT = np.meshgrid(lon, lat) dx = np.cos(LAT) * np.sin(LON) dy = np.sin(LAT) dz = np.cos(LAT) * np.cos(LON) ax, ay, az = np.abs(dx), np.abs(dy), np.abs(dz) use_x = (ax >= ay) & (ax >= az) use_y = (ay > ax) & (ay >= az) use_z = (az > ax) & (az > ay) map_x = np.zeros((OUT_H, OUT_W), np.float32) map_y = np.zeros((OUT_H, OUT_W), np.float32) # X 轴方向的两个面 sel = use_x & (dx > 0) map_x[sel] = (-dz[sel] / ax[sel] * 0.5 + 0.5) * FACE map_y[sel] = (-dy[sel] / ax[sel] * 0.5 + 0.5) * FACE # ... nx、py、ny、pz、nz 同理,符号按约定调整 # 分面 remap 再合并,或直接按面索引一次性做这段代码我给的是骨架,符号约定(哪些分量取正、哪些取负)必须按你实际拿到的六面图来调,不同平台的立方体面朝向约定能差出好几种。验证的办法很土但有效:先在场景里找一个明显的地标(比如门口、招牌、墙角),记住它在原始浏览画面里的位置,转成等距柱状之后看它是不是还在同一个方位、有没有左右镜像。镜像了就翻一个符号,上下倒了就翻另一个,试个三四次肯定对。
如果嫌自己写麻烦,其实有不少开源工具和命令行程序能直接做 cube 转 equirect,搜"cubemap to equirectangular converter"能找到一堆。但自己写一遍的好处是你能精确控制输出分辨率和插值方式,不会因为工具默认参数把边缘糊掉。
3.5 出图后的二次处理:降噪、补天、扩边
拼出来的原图通常不能直接用,有三件事要做。
第一,补天补地。街景类和部分 VR 看房类因为拍摄限制,天顶和地面是纯黑或者纯色块。如果只是自己看,无所谓;如果要导出给别人看,就得补。最省事的办法是从低层级瓦片里取那部分模糊的颜色,放大后高斯模糊一下盖上去;讲究一点的做法是在三维软件里贴一张天空 HDRI,渲染出对应视角再合成。我一般用前者,因为全景图本身在手机上看的时候,天顶区域视角畸变很大,用户根本不会细看。
第二,色阶校正。有些平台会对瓦片做统一的色彩压缩,拼出来的整体发灰、对比度偏低。用ffmpeg一行就能拉回来:
ffmpeg -i pano_L4.jpg -vf "eq=contrast=1.06:saturation=1.05:brightness=0.01" -q:v 2 pano_fix.jpg参数别调太狠,contrast超过 1.12 天空就开始断层了。这个值是试出来的,不同平台的压缩强度不一样。
第三,尺寸统一。如果你要批量处理几十张图,一定要先把尺寸统一到 2:1,不然之后做 VR 播放的时候球面接缝会露出来。ImageOps.fit或者直接resize都行,注意用LANCZOS插值,双线性会让细节糊成一团。
4. 不同平台类型的差异与对应策略
4.1 云设计出图类平台:分辨率天花板与"预览图"陷阱
这类平台的全景图基本是设计软件渲染出来的,不是相机拍的,所以它的"原图"这个概念跟传统摄影不一样——它渲染出来的时候就是某个固定分辨率,比如 4096×2048,然后再切片。你下载到最高层,拼出来也就是这个尺寸,不会更大。
这里有个陷阱要提醒:很多平台在列表页展示的缩略图和实际渲染图是两套资源,缩略图可能是 1024 宽的预览版,而且路径跟正式瓦片完全不同。我曾经花了半小时拼出一张 2048 宽的图,怎么都觉得不对劲,回头一查发现下的是预览图那一套。判断方法是看瓦片边长——正经渲染图的瓦片一般是 512 或 1024,预览图那套往往是 256 甚至 128。瓦片边长小于 256 的,基本可以判定是预览资源,别浪费时间。
另一件事是这类平台经常带"水印层"。水印不是在瓦片里烤死的,而是在播放器里叠了一层 canvas 或者一张 SVG。所以你拼出来的图往往是干净的,这是好事;但也意味着你拼出来的图和你在页面上看到的不完全一样,别以为是哪里搞错了。
4.2 720°漫游托管类平台:热点、多场景与切片组织
这类平台的典型特征是"一个项目多个场景",每个场景一套瓦片,URL 里带场景 ID。你要下载一整套项目,就得先把所有场景 ID 摸出来。
场景 ID 的抓法很简单:在页面上把所有场景都点一遍,然后在网络面板里按 URL 前缀分组,看sceneId=xxx或者/pano/{id}/这样的路径段,去重就是全部场景。有的平台会把场景列表直接塞在一个 JSON 配置里,搜一下页面源码里的"scenes"或者"panolist"能找到,那个更省事,直接从配置里读每个场景的瓦片根路径和层级数量,连猜都不用猜。
这类平台的瓦片组织一般是等距柱状的,所以拼出来就是 2:1,不用做投影变换,省一道工序。但要注意它们的层级号可能不是从 0 开始的,有的平台把最大层标成l0,越小越清晰。判断方法是看单张瓦片的文件大小——同场景下文件最大的那一层就是最高清层,别管它编号是几。
还有一点,这类平台经常做按视场角的动态加载,也就是你不转到某个方向,那个方向的瓦片永远不请求。所以如果你只靠 HAR 抓,一定抓不全,必须靠地址规律自己循环补齐,或者用脚本模拟拖拽把视角转满。后者麻烦但保险,前者快但有风险,看你手上项目的具体情况选。
4.3 街景类服务:立方体面、垂直裁切与拍摄点差异
街景类是最麻烦的一类,原因有三个。
第一,立方体面 + 垂直裁切。前面提过,这类服务的原始采集车云台角度有限,天顶和地面本来就没有有效像素。它们通常的做法是把立方体面上下各裁掉一部分,所以你拿到的是一个"缺顶少底"的立方体。转成等距柱状之后,上下各有一条黑边或者色块。这个没法补,只能接受,或者在后期用天空/地面纹理盖掉。
第二,拍摄点会漂移。街景是沿路连续拍摄的,相邻两个拍摄点的位置差几米,所以你在不同位置"截图"得到的是不同视角。这意味着你没法像处理单张全景那样,把所有瓦片当成一套完整数据来拼——你得先确定你要的是哪个拍摄点的图。判断方法是看 URL 里的panoid或者类似的唯一标识段,一个 ID 对应一个拍摄点,换 ID 就是换位置。
第三,多级缩放很激进。街景类为了在低带宽下也能快速出图,层级往往切得很细,从 L0 到 L5 都可能有。这时候没必要全下,按你要的最终尺寸挑一层就行。要 4000 像素宽就下 L3,要 8000 就下 L4,别贪。
这类服务的瓦片鉴权相对严格,短期 token 过期很快。我的实际做法是:如果要的量不大,直接在高倍缩放下截图,一张 3000 多像素的图也够用了;要的量大,就老老实实看平台有没有官方导出或者开放接口。硬啃 token 这种事,投入产出比太低。
4.4 VR看房类:深度图与平面图是额外福利
这类平台除了全景图本身,往往还会传两样东西:深度图和户型平面图。这两样对做二次开发的人价值很高。
深度图一般是跟全景图同尺寸的一张灰度图,灰度值代表该像素到相机的距离。有了它,你可以自己做简单的三维重建、做"点击地面走到该位置"的交互,甚至是做点云导出。判断有没有深度图:在网络面板里找跟全景同尺寸、但颜色明显是灰阶的图片请求,或者找后缀带depth、d的资源。
户型图通常是 SVG 或者一张 PNG,路径里带floorplan。这个直接拿就行,很多时候比全景图本身还有用。
这两样东西的下载逻辑跟全景瓦片一样,但深度图往往不切片,直接给整图,所以反而简单——找到地址,右键另存。我遇到过几次,深度图比全景图还好拿,因为它没做防盗链。
5. 常见问题排查实录
5.1 拼出来错位、断裂、镜像的四种原因
这一类问题占了所有求助的七成,我把遇到过的情况整理成速查表:
| 现象 | 最可能原因 | 修法 |
|---|---|---|
| 画面整体转置(地平线变竖线) | 行列参数顺序反了 | 交换 x、y 的 paste 坐标 |
| 上下颠倒 | Y 轴方向相反 | 行号取GRID-1-y |
| 左右镜像 | 立方体面朝向约定不同 | 翻转对应面的符号,或水平flip |
| 规律的错位条带 | 瓦片带重叠边未裁 | 试crop掉 8、16、32 像素 |
| 边缘一圈黑 | 瓦片存在留白填充 | 裁掉留白后重算网格 |
| 某一块明显比周围糊 | 那一块是低层级兜底图 | 补下该位置的高层级瓦片 |
表里最后一行特别典型。播放器在加载时如果某块高清瓦片超时了,会用低层级的模糊块顶上,而且它就这么留在那儿了。你抓 HAR 的时候抓到的是高清地址,但实际下载下来那张可能是模糊的——因为服务器给你返回了兜底图。判断方法是打开单张瓦片看分辨率,如果尺寸比TILE小,那就是兜底图,得重新下。
还有一种最坑的情况:拼出来的图看起来完全正常,但某一条竖线两侧接不上。这是瓦片在切的时候按了不同的量化表,或者某几张瓦片被重新压缩过。这种情况基本无解,只能接受,或者换一个层级重下。
5.2 下载到一半 403 或空图
403 最常见的三个原因:缺 Referer、Cookie 过期、请求频率过高。
缺 Referer 好办,curl -H "Referer: 页面地址"补上就行。Cookie 过期的话,从浏览器里把当前会话的 Cookie 复制出来,加到请求头里。注意 Cookie 一般有有效期,下大批量的时候要预留重新取 Cookie 的时间。
频率过高这个最麻烦,因为它的表现有时候不是 403,而是返回一张尺寸正常的占位图——你以为下成功了,拼的时候才发现那一块是个灰色的问号。这也是为什么我在 3.2 的脚本里加了sleep。判断方法是随机抽几张瓦片打开看,如果内容跟周围对不上,就是被限流了。
还有一种"空图"是 0 字节文件,这种是请求直接失败。脚本里用-s加os.path.getsize() < 1024双重判断能过滤掉大部分,剩下的靠重跑补。
提示:遇到大面积 403,先别急着改脚本。换一个浏览器窗口重新登录一次,把新的 Cookie 和 Referer 全套复制过来,九成能解决。剩下的那一成,通常是平台在服务端做了更细的判定,这种就别硬来了。
5.3 颜色发灰、对比度不对:色彩空间那条线
拼出来的图整体发灰、饱和度低,很多人第一反应是 JPEG 压缩太狠。实际上大概率是色彩空间不一致。
有些平台在服务端会把图片转成特定的色彩空间,比如为了省带宽把 sRGB 转成更窄的色域,或者干脆丢了 ICC 配置文件。你拼的时候用的是 PIL 默认的 sRGB 解释,两边对不上,就发灰。
处理方式分两步。第一步,检查单张瓦片是不是带 ICC 配置文件(用exiftool或者 PIL 读info),如果带,拼接时要保留;如果所有瓦片都不带,那就是平台在压的时候丢了,没法找回。第二步,拼完之后用一次轻微的曲线拉伸补回来,ffmpeg的eq滤镜或者 ImageMagick 的-sigmoidal-contrast都行,参数控制在 5% 以内,别过。
还有一种情况是瓦片本身是 16 位色深打包进 JPEG 的,这种极少见,但如果遇到了,拼出来会有明显的色带。处理方式是用cv2.imread加上IMREAD_ANYDEPTH读成 16 位,拼完之后再转 8 位输出。
5.4 尺寸对不上:算一下理论像素
最后一种常见的困惑是"为什么我拼出来的图只有 4096 宽,感觉不够清"。
先算一下理论上限。如果最高层是 L3,瓦片 512,那理论宽度就是512 × 2^3 = 4096。这就是天花板,你再怎么努力也拼不出 8192。想要更大,只能去找更高层级的瓦片,而更高层级存不存在,取决于平台有没有切。
判断平台切到第几层,方法很直接:在播放器里把画面放到最大,然后看请求列表里出现的最大层级号。如果你看到最大只到 L3,那就是 4096 封顶,接受现实。
另一个容易搞错的是等距柱状的宽度跟立方体面的换算。一个 4096 的立方体面,转成等距柱状之后宽度是4096 × 4 = 16384,高度 8192。注意这个换算关系:立方体单面边长 × 4 = 等距柱状宽度。很多人按 ×6 算(因为有六个面),算出来对不上,就是因为立方体展开不是简单地把六张拼一起,投影变换之后有效像素是 4 倍关系。
6. 版权、授权与实操中踩过的坑
6.1 什么场景下这样操作是合理的
技术本身是中性的,但用在哪里有明确边界。我自己的原则是三条。
第一,自己账号下的素材,导出来做备份、做离线演示、做二次剪辑,没问题。你付了钱买的是使用权,把渲染结果导出来存在本地,这是正常的使用行为。但导出之后不能反过来再分发、再售卖,那是另一码事。
第二,明确标注可公开获取、且没有版权限制的内容,练手没问题。有些开放数据集、有些教学示例,本来就是给大家研究的。但你要先确认它的授权条款,别看都不看就下。
第三,别人花钱买的服务、别人拍的作品,别碰。这条没有例外。你下载下来自己看,跟下载下来给别人看,跟下载下来改一改卖钱,性质完全不同。
实际操作里还有一个边界容易被忽略:平台的用户协议。绝大多数平台在协议里明确写了禁止批量抓取、禁止绕过技术措施。你技术上能做到,不代表协议上允许。我个人的做法是,如果只是零星几张、自己用,就走正常路径;如果是批量需求,先看看平台有没有官方的导出功能或者商务渠道,很多平台其实愿意给你源文件,只是入口不明显。
6.2 几个不写进文档的经验
最后分享几条只有真下过几十套图才会知道的细节。
关于层级选择。新手总想下最高层,觉得越大越好。实际上下到"够用"就停。我做过对比,同一张图 L4 拼出来 8192 宽、L5 拼出来 16384 宽,在 27 寸 4K 屏上全屏看,肉眼看不出差别,但文件体积差 4 倍,处理时间差 5 倍。先确定最终用途的尺寸,再倒推层级,这是我现在的固定流程。
关于缺块的补法。拼完发现缺了几块,别急着重下全部。缺块的位置信息是明确的,写个小脚本只补那几块就行。如果补不到(比如那个位置的瓦片服务端就是不返回),可以从低一层级取对应区域,放大后贴上去——反正那一块可能是天空或者地面,糊一点看不出来。
关于批量处理。做第二套、第三套图的时候,一定要把脚本参数化。层级、瓦片边长、URL 模板、重叠量、行列顺序,这五个参数抽出来做成配置,下一套图改配置就行,不用改代码。我前两套图是硬编码的,第三套换了个平台,改代码改了四十分钟,血的教训。
关于验证。每拼完一张,不要直接就看。先看一眼文件大小,正常情况下应该是宽度 × 高度 × 0.15字节左右(JPEG 高质量的经验值),比如 8192×4096 的图大概 4 到 6MB。如果只有几百 KB,那肯定有大片纯色区域(多半是天顶补的),得回头查。这个经验判断法帮我抓到过好几次"某一块全是黑"的问题。
最后一个小技巧。如果平台的全景播放器支持"截图"或者"导出当前视角",而你又只需要某一个方向的高清图,直接用那个功能比下全套瓦片快得多。很多平台的截图是按照当前渲染分辨率出的,你把窗口拉到最大、画质调到最高,截出来的单视角图往往比拼全套再加裁剪还清晰。这个路子我在赶时间的时候用得最多。