1. 为什么我会写一个叫 PanWatch 的项目
装修验收那天,我在没有灯光的毛坯房里拍了两百多张照片,回来在电脑上翻到半夜,愣是没看出来墙角那条裂缝到底是从哪儿延伸出去的。第二天带着充电宝和手机又跑了一趟,站在同一面墙前面,把手机贴着墙角慢慢转了一圈,才拼出了完整的走向。说实话,那时候就特别想有一个工具,能把一组空间照片整理成一张可以随便拖来拖去看的全景图,顺手还能标记问题。
PanWatch 就是从这个念头生长出来的。它不是某个大厂出品的产品,而是我按个人项目维护的一套工作流:用普通手机拍一组带重叠率的照片,拿 OpenCV 做全景拼接,输出成等距柱状投影图,再丢到 Web 端一个轻量查看器里,支持拖拽、陀螺仪、热点标记,甚至能把同一个位置前后两次拍摄的结果做结构相似度对比,用颜色把变化区域标出来。这套东西在装修验收、工地巡检、租房房源 VR 看房、门店陈列记录这些场景里都好用,尤其是当你发现自己手里只有一堆照片、却需要一个“空间答案”的时候。
我在开头把这些说清楚,是免得读者抱着“这是一个手机 App”的预期来看文章。它更像是一套开源风格的实用工具链,前端后端都有,踩坑也不少。本文就按我推进这个项目时的顺序来写:先讲为什么自建而不是用现成方案,再拆采集和拼接,然后是查看器与热点,接着是前后两次全景的时序对比,最后是攒下来的实战踩坑记录。后面提到的代码片段都是可以直接拿走的,但更值钱的可能是那些“为什么这样做”的思考,毕竟工具替人干活,方案节省时间。
1.1 装修验收到崩溃:全景查看到底解决了什么
单张照片的问题是它没有“上下文”。一面墙上的裂纹,你靠墙贴拍,看到的是局部,根本不知道裂缝是沿着踢脚线水平走、还是在某根管线位置突然拐弯。把镜头拉远拍整面墙,往往又看不清细节。全景图解决的是这种“既要细节又要整体”的矛盾:当你把一组照片拼接成完整空间之后,就能像站在房间中央转动脖子一样,把视线从墙 A 转到墙 B,让问题点和它的环境关系直接呈现。
在实际用下来,这个能力的价值远超“新鲜感”。比如查看吊顶开裂,把镜头对准天花板不同方向拍几张,拼出来之后很容易发现裂纹其实是在空调风口边缘呈放射状散开的,那说明安装阶段的结构应力可能出了问题;光看单张照片,你根本不会往这个方向想。PanWatch 把这种“现场空间推理”下沉到了日常工具层面,不需要你会发现全景并不是炫技,而是帮你少跑工地、少打电话问施工方现场的太多问题。
1.2 全景查看着的领域边界:不要拿它当短视频平台
PanWatch 不是全景相机,也不是 VR 社交平台。它更像是一个轻量级的“空间档案管理员”。全景相机看起来很美,但消费级设备在低于 1000 流明的环境光里,暗部噪声很重,缝隙拼接处容易模糊;Krpano 这类商业框架则把全景素材的包装、切瓦片、发布流程都做得很完整,不过授权费用和部署复杂度对小团队或个人来说还是偏重。Pano2VR 也是同类桌面工具,生成 HTML5 产物没问题,但它是“软件思维”,每次更新、换模板都要在 GUI 里点来点去,不好自动化。
PanWatch 的路线是给自己留足可控性。输入一套普通照片,输出一个静态目录,里面是全景图、瓦片、配置 JSON 和几个前端脚本,没有任何后台服务依赖。这意味着它可以放在本地跑,也能丢到任意静态托管服务上,分享给监理、设计师甚至客户都用不着教他们装软件。它不提供“从零做全景采集”以外的炫技功能,但凡是空间信息组织、标注、对比这一类需求,恰好是它最顺手的范围。
2. 采集与拼接:从普通照片到一张可拖动全景
全景拼接听起来像是“软件自动完成”,实际上输入端稍微偷懒,后面返工的时间会成倍增加。我在 PanWatch 第一个版本里用手机随手拍了一堆照片拼接,结果是墙角错位、光线差异把图像融合得像是打了补丁。后来总结了拍摄规则,又把 OpenCV 的拼接流程串成脚本,才稳定下来。这一节是我认为整个项目里最值得细讲的部分,因为拼接结果的质量上限在拍摄时就已经定死了,算法只是把“拍得合格”的照片还原成空间感。
2.1 拍摄规则:为什么三圈十张比随手乱拍靠谱
全景拼接的原理简单说,是找到相邻照片的重叠区域里相同的特征点,然后算出两张照片之间的透视关系,把它们“缝合”到同一张画布上。所以重叠率直接决定了匹配的可靠性。我的经验值是:相邻照片之间至少保持 35% 到 40% 的重叠,水平方向每隔 45° 拍一张,即一圈 8 张;俯仰方向分三圈,相机分别朝上约 30°、水平、朝下约 30°。这样一算,标准房间差不多要拍 24 张左右,加上天空和地面补拍,总共 26 张上下。
有人觉得室内空间四四方方,三圈有点浪费。我试过两圈,即只拍水平和中上,结果顶部墙角、吊顶边缘经常出现大面积无纹理区域,拼接器找不到足够的匹配点,最后只能手动调整。三圈十张(严格说是三圈 8 张加补拍)的好处是把天花板和地面交界区域都覆盖住,尤其是墙角这种纵深变化大、容易错位的位置。拍摄时如果能上三脚架最好,手持也可以,但每转到下一张时,要尽量保持相机光轴绕着同一竖直轴线旋转。说人话就是:以身体为轴心,身体尽量转、手肘少甩,并且每一张都不要改变站位和镜头焦段。
2.2 OpenCV 拼接的核心逻辑:特征点匹配与相机姿势估计
在代码层面,OpenCV 从 3.0 之后把 Stitcher 模块内置了,PanWatch 的核心拼接逻辑基本就是它的封装。拼接器内部干的事情大概是四步:
- 第一步:特征点检测。把每张照片上容易定位的角点、斑点、边缘线段找出来,常用的有 SIFT、ORB、AKAZE。
- 第二步:特征点匹配。在相邻照片的重叠区寻找对应的特征点对,这就像给两幅图“找相同的锚点”。
- 第三步:相机姿态估计。根据至少 8 对匹配点,算法用基础矩阵或单应性矩阵把相机旋转关系算出来,并调整到同一个坐标系。这个阶段如果输入有过大的透视畸变,估计出来的参数也会飘。
- 第四步:融合输出。把所有图投影到全景画布上,再处理重叠区的颜色过渡、曝光补偿和可能的形变。
我用 Python 写的第一版拼接脚本很简单,见下方代码。它最终输出的就是一张等距柱状投影图,这种格式的宽高比接近 2:1,也是前端球面查看器最容易接的格式。
import cv2 images = [] # images 里放的是按拍摄顺序整理好的图像路径 img = cv2.imread(path) ... stitcher = cv2.Stitcher_create(cv2.Stitcher_PANORAMA) status, pano = stitcher.stitch(images) if status == cv2.Stitcher_OK: cv2.imwrite("pano_equirect.jpg", pano) else: print("拼接失败,错误码:", status) # 错误码 1 表示特征点不够,需要提高重叠率或换纹理丰富的图片很多人在这一步会直接返回失败,但不先检查输入图像的顺序。Stitcher 对图像顺序相当敏感,乱序会导致匹配关系错乱。我的做法是先用文件名里的序号排序,再传入,同时把所有图片 resize 到统一宽度,比如 1500 像素。这能显著降低计算内存,而且对拼接结果影响不大,因为后面的前端展示另外有高分辨率瓦片。
2.3 我看重的一个小细节:锁定曝光
这是最容易踩坑、也最影响成片质量的地方。全景拼接的上限由“原始照片的重叠区间在曝光上是否一致”决定,如果同一面墙在照片 A 里测光是暗部,在照片 B 里因为手机自动曝光变成了高光,重叠区域的亮度跳跃在融合时就会留下明显的缝。更麻烦的是,手机拍摄时的自动白平衡也会随构图改变,导致左右两张图一个偏冷一个偏暖。
所以我现在拍摄前必做三件事:关闭自动 HDR,锁定曝光和对焦,并把白平衡设为固定值。iPhone 上长按画面的方法就是 AE/AF Lock,安卓很多相机的专业模式也有类似锁定选项。参数上,室内光线弱的时候我一般会把 ISO 压在 400 到 800,快门速度放慢一点,光圈由手机自动控制。控制在三脚架上可以接受低 ISO 和慢快门,手持的话要保证快门不低于 1/30 秒,否则模糊点一多,特征匹配也会出问题。
2.4 输出规格选择:分辨率、瓦片与首屏加载的平衡
拼接输出的全景图分辨率决定前端清晰度。我一开始直接输出 12000x6000 的超大图,浏览器加载时外带解码一整张 JPEG,移动端直接白屏。后来改了两级策略:拼接阶段以适中分辨率先把映射关系算准,然后输出一张约 6000x3000 的基础全景图;前端展示时,再切割成标准的 256x256 瓦片,按视场加载相邻几块。
瓦片切割这件事其实可以继续沿用 OpenCV 做,也可以交给后端脚本。我的做法是保留源全景图,每次前端请求时只加载对应的 lever(缩放层级)瓦片,用一个小工具把全景图按金字塔切好。比起一次性加载一张大图,首屏加载速度能快一倍多,而且拖拽时也不会卡顿。移动端尤其明显,这一节的收益远大于写那几十行切瓦片代码的成本。
3. 查看器实现:拖拽、陀螺仪与热点标记
工程上最容易低估的是查看器这一层,因为“把一张全景图铺到球面上”听起来简单,但涉及投影坐标、交互事件、真机兼容的时候,问题一个接一个。PanWatch 的查看器基于 Three.js 生态里的 Panolens 封装,我在上面做了主题调整和热点系统。这里有一个很重要的认知:全景查看器的本质,是把等距柱状图包裹到一个三维球体的内表面,让相机位于球心,再通过鼠标或陀螺仪改变观察方向。理解了这一点,后面所有坐标换算和交互逻辑都是顺着自然延伸出来的。
3.1 选型:Panolens 与 Three.js,比自己写球体贴图省了哪些事
我最早尝试过自己写球体贴图。Three.js 的 SphereGeometry 不支持直接构造内表面,得手动翻转面的法线方向,相机还要固定位置不能移动,再手写一个轨道控制来更新相机视角。这些基础工作并不复杂,但叠加到瓦片加载、交互兼容上以后,维护成本很快失控。Panolens 把这些都做了:它内置了球体、纹理加载器、瓦片源,以及一整套基于经纬度的观察控制,我只需要专注写业务功能。
接入它的代码量不算大,下面是 PanWatch 查看器的核心初始化片段:
const viewer = new PANOLENS.Viewer({ output: 'fullscreen', autoHideControlBar: true, // 开启陀螺仪,移动端有 deviceorientation 时生效 enableRipple: false }); const panorama = new PANOLENS.ImagePanorama('pano_equirect.jpg'); viewer.add(panorama);注意这里的 ImagePanorama 默认会一次性加载全景图,用瓦片的话要替换成 TilePanorama 并传入瓦片 URL 模板。Panolens 对瓦片路径的格式有要求,比如{level}/{x}/{y}.jpg,如果切瓦片时命名的层级顺序不对,加载会全部失败。我在第一次集成的时候就在这里耽误了大半天,后来老老实实按官方仓库里的 tile 示例路径重新切了一遍就通了。
3.2 等距柱状投影与坐标换算:把图片像素映射到三维方向
等距柱状投影的特点是:图片的水平方向代表全景的经度(yaw),垂直方向代表纬度(pitch),所以换算关系非常直接。假设全景图宽为 W、高为 H,那么图上任意一点 (px, py) 对应的方位角是:
// 像素坐标转视角方向 const yaw = (px / W) * 360 - 180; // 经度:-180 到 180 const pitch = 90 - (py / H) * 180; // 纬度:-90 到 90反过来,如果我要在图片上放一个“整改问题”的标记,需要先把经纬度转回调用的屏幕坐标,再把标记挂在球面上。Panolens 的 Marker 设计其实就是这个思路,它允许你直接传入经纬度,然后会把 Marker 放置到对应方位。底层仍然是这三行换算,只是封装了起来。搞清楚这个映射,对后面做变化对比很有帮助,因为对比时我需要在同一全景位置叠加高亮区域,本质上就是为变化区域求一个经纬度范围,再贴标记。
3.3 热点标记:整改单可以直接钉在照片上
在工地巡检或房屋验收的场景里,热点标记是最能体现“工具解决实际业务”的功能。我通常把问题描述、现场照片、责任人和整改截止日期放进一个 JSON 数组,前端遍历数组生成 Marker。Panolens 的 Marker 本质是 Sprite,它可以挂 CSS 样式,点击后弹出自定义的 Panel 展示详情。这个交互模式比传统表格直观得多:你看全景图的时候,视觉上看到的是“哪面墙、哪个高度、是什么问题”,而不是一行冷冰冰的 Excel 记录。
一个比较实用的细节是:给 Marker 设置不同颜色来区分问题严重级别,比如黄色是观察项、红色是整改项、蓝色是已完成项。这样在球面上扫一眼,空间里的问题分布密度立刻就有概念。施工方收到带这种标注的全景链接,也不需要赶到现场,就能定位到它对应的墙角和窗边。
3.4 移动端陀螺仪与桌面端兼容处理
桌面端鼠标拖拽还好说,真正的坑在移动端。Panolens 对 deviceorientation 事件的支持是做了处理的,但安卓上的 WebView 经常因为权限策略或者页面不是 secure context 而不触发这个事件。我在 debug 的时候用了个笨办法:打印window.DeviceOrientationEvent是否存在,同时监听事件里event.gamma是否在变化,这样能快速判断是权限问题还是事件根本没绑定上。
另外,移动端的浏览器会监听用户手势才能请求陀螺仪权限。iOS 13+ 之后必须调用DeviceOrientationEvent.requestPermission(),安卓 Chrome 在较新版本里也有类似策略。所以我在初始化查看器之前加了一个系统判断,如果是 iOS 就先弹一次授权按钮,等用户点击后再绑定事件,否则直接白屏。代码不复杂,但少了这一步,真机上大概率是废的。除了陀螺仪,还要注意页面底部的 CSStouch-action设置,不然在球面上拖拽时会触发整页滚动,体验很差。
4. 时间维度上的“看”:同一空间前后两次全景怎么对比变化
PanWatch 里“Watch”的意义,并不只是空间浏览,还包括“把不同时间点的空间快照放在一起看”。装修监工、室内温湿度导致的墙面变化、店铺陈列调整,都可以靠这个能力来追踪。这一部分最开始只是我给自己加的“小功能”,后来发现它的价值甚至超过纯查看:空间问题往往不是静态的,渗水痕迹、裂缝发展、施工进度,都要经过时间对比才能得出结论。
4.1 对齐问题:固定机位拍摄是先决条件
要做像素级对比,前提是两次拍摄的机位、朝向、视角高度尽量一致。我的方法是在项目第一次采集时记录拍摄点位编号,用粉笔在地面画好标记,或者用激光水平仪标记位置和高低。这样第二次拍摄时几乎可以完全复现视角,对比才有意义。
如果两次拍摄之间只能靠手持恢复大致位置,偏差是无法避免的。这时候比较稳妥的路线不是做像素级 diff,而是先把两套全景都放到查看器里,用“A/B 切换”方式让用户自己拖拽到同一视角对比。PanWatch 的对比视图里有一个滑块,拖动时透明度从全景 A 变到全景 B,这样即使机位有些偏移,人眼也能在视觉上“对齐”。真正需要自动检测变化区域时,才用固定机位的严格采集流程。
4.2 差异检测:从结构相似度到可视化叠加
在机位一致的前提下,可以做算法层的变化检测。我用的方法并不复杂,核心是结构相似性指数(SSIM)。SSIM 比起直接像素相减更稳健,不容易被窗外的光线变化误导,因为它综合比较了亮度、对比度和结构三个维度。实现时我会先把两张全景图缩到约 2000 像素宽,转成灰度图,用滑动窗口计算每个局部窗口的 SSIM 值,低于阈值的窗口就认为是“有变化”的区域。下面是核心代码:
import cv2 import numpy as np from skimage.metrics import structural_similarity as ssim a = cv2.imread("pano_before.jpg", cv2.IMREAD_GRAYSCALE) b = cv2.imread("pano_after.jpg", cv2.IMREAD_GRAYSCALE) # 统一尺寸 a = cv2.resize(a, (2000, 1000)) b = cv2.resize(b, (2000, 1000)) score, diff = ssim(a, b, full=True) diff = (diff * 255).astype("uint8") # 用二值化提取变化区域 _, thresh = cv2.threshold(diff, 50, 255, cv2.THRESH_BINARY) # 用开运算去掉噪声,再用膨胀让变化区域连成片 kernel = np.ones((5, 5), np.uint8) thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel) thresh = cv2.dilate(thresh, kernel, iterations=3) cv2.imwrite("change_mask.png", thresh)拿到变化区域的掩码之后,我会把它的 bounding box 转成经纬度范围,作为一组半透明 Marker 图层叠加到全景查看器上。这一层比直接裁剪两张图对比要容易实现很多,因为球面纹理的覆盖和对齐都很难做,但标记只需要钉在大概方位上,天然适合空间提示。实际展示时,用户看到的是“这张 3 月 12 日拍的墙面,和 2 月 26 日相比,窗台下约 20 厘米高处有一片变化”,这已经足以让人快速到现场复核了。
4.3 变化定位在工作流里的价值
这个功能在工作流里的价值,可以说很直接。例子之一,是厨房吊顶漏水。客户第一次拍全景时只是例行记录,第二次拍到积水印但不确定是否是新增问题。用变化检测把 mask 叠加后,渗水的位置、范围一目了然,连带着上方楼板对应区域也被检查了一遍,直接定位到一根 PVC 管的接口。另一个例子是施工现场的安全检查:脚手架搭设前后两次全景对比,能够自动发现“临边防护栏是否被拆除”,如果只看单张照片很可能注意不到。
对比功能用到的代码和模块,其实都不复杂,但它把“去看现场”这个高成本动作直接降维成了“看一眼全景快照对比”。在团队协作里,这种跨越时间的信息组织方式,胜于发送几十张照片在群里等对方自己领悟。工具的价值是省钱省时间,这一点 PanWatch 做得比较充分。
5. 项目推进中的踩坑记录
任何工具链从原型到顺手,中间一定有毒打过程。PanWatch 这一路积累下不少教训,有些纯粹是知识盲区,有些是版本兼容问题。我把典型的几类整理成了下面的表格,方便大家对照自查。
| 现象 | 根因 | 解决思路 |
|---|---|---|
| 拼接结果有白色接缝或错位 | 重叠区特征点不足,常见于大面积白墙、地砖 | 提高重叠率到 40%,改用 AKAZE 特征,必要时调整拼接器参数 |
| 两圈之间亮度突变,产生“阴阳面” | 自动曝光和自动白平衡随构图变化 | 拍摄锁定曝光、锁白平衡,关闭 HDR,后期做增益补偿 |
| 移动端球面拖拽卡顿,首屏黑屏 | 整张 12000 像素全景图直接解码 | 切成 256 瓦片并按视场加载,降低纹理分辨率 |
| 安卓 WebView 陀螺仪毫无反应 | 缺少设备传感器权限申请,页面不是 secure context | 使用 https 或 localhost,iOS 调 requestPermission,安卓在 WebView 授予传感器权限 |
| 热点标记在边缘视角漂移 | 我用了圆柱投影图,前端却按等距柱状图解析 | 统一输出和加载都使用等距柱状投影,代码里不做额外转换 |
| 内存峰值过高导致后台进程被清理 | 浏览器解码大图占满内存,瓦片缓存无上限 | 给瓦片缓存设置 LRU,限制纹理内存,关闭多余的三维后期效果 |
5.1 大面积白墙接缝错位的排查
装修工地很特殊,大面积的腻子白墙、浅色地砖对特征检测非常不友好。我最早是在餐厅区域拍的,天花龙骨和墙面几乎纯白,Stitcher 返回错误码 1,接缝处常常出现楼层线和墙角错开。后来我在调试中发现,ORB 在白墙区域能提取出的特征点不到几十个,即使有也只是狭长的窗框边缘。把特征算法换成 AKAZE、同时保证相邻照片有足够的纹理重叠之后,问题缓解了很多。如果墙确实一片形式感很强的壁纸,那也不需要担心,纹理丰富时 ORB 完全够用。
另一个有效的补救是二次重叠法:转完一圈后,第二圈的第一张与第一圈的最后一张保持一半的重叠区,相当于为拼接器提供额外的约束。这个方法本质是给算法多“喂”特征点,在弱纹理环境里往往比改变算法参数更直接。
5.2 曝光参数不一致导致的“阴阳面”问题
第一次完整拼接,我把客厅和阳台连续拍完后,生成的图上有一道清晰的光影分割线,看起来整个空间像是被纵切了一刀。追到原始照片才发现问题出在“自动曝光”上:阳台一侧光照强,手机在拍它附近时降低了曝光,然后转回客厅时亮度又恢复,导致全景图里有一段明亮一段暗淡,融合算法再怎么补偿,明暗过渡的不自然还是存留了下来。
解决它其实遵循拍摄纪律:迈过这一步之前,必须先做 AE/AF Lock。我后来还把相机 App 切到专业模式,快门固定 1/60 秒,ISO 设成自动上限 200,这样既能保证弱光不死黑,又不会让过亮区域疯狂溢出。实际经验是,宁可整体暗一点,也不要出现左右两半亮度跳跃的画面,因为前者后期可以整体提亮,后者已经毁掉缝合的基础。
5.3 移动端陀螺仪的“假死”与权限坑
移动端的陀螺仪问题,我耗费了很多时间。现象很典型:在 PC 上拖拽一切正常,但只要用 Android 手机自带的 WebView 打开页面,画面就像被钉死一样。后来排查发现有两层原因:一是 WebView 默认没有开启「传感器」权限,需要在应用侧手动设置;二是页面的 URL 如果不是 https 或 localhost,浏览器根本不会派发 DeviceOrientationEvent。iOS 侧还有一层,必须通过用户点击调用requestPermission来获得显式授权。
所以我在前端加了一个兼容函数:先判断浏览器是否支持,再判断是否需要请求权限,最后绑定事件。同时额外做了一层降级策略:如果半天收不到deviceorientation,就自动关闭陀螺仪按钮,提示用户返回手动拖拽模式。这保证了在权限环境复杂的情况下,查看器依然可用。
5.4 超大全景图的内存与加载优化
全景图做到 12000 像素宽以后,Chrome 桌面端还能抗住,但手机上的浏览器直接解码成满分辨率贴图,内存容易出现峰值崩溃。这是 PanWatch 里必须解决的“大图困境”。我的方案是把全景图金字塔化:先准备低分辨率缩略图,用来快速出首屏;再按缩放级别准备多层级瓦片,拖拽时只加载当前视野需要的瓦片。这里可以设置一个全局瓦片缓存池,超出数量就淘汰客户端最久未用的块,避免内存不停膨胀。
Panolens 的 TilePanorama 实际上已经支持LOD(多级细节)加载,但你需要提供符合它规则的瓦片路径。有清晰源码后换瓦片并不复杂,难的是那些“看不见”的细节:瓦片缺块时的占位处理、加载失败后的重试策略、以及为了减少请求次数而做的缩略图预取。我在这上面花了大概一个小周末,之后移动端的加载体验才算真正稳定下来。
6. 可以直接复用的代码与扩展方向
到这个阶段,PanWatch 已经从我脑子里的一时兴起,变成了一个真正能回答“空间里发生了什么”的工具链。如果你也想把它捡起来用,我给你梳理一份模块清单,顺便说说后续值得扩展的方向。我不打算把全部代码贴上——那会显得本文像仓库文档——只把模块划分和扩展思路讲清楚,剩下的照着做其实不难。
6.1 可复用模块清单
- 拍摄向导模块:一个简单的 checklist 页面或纸质卡,标注每圈角度、张数、设备位置和曝光锁定要求,拍前过一遍,能省后面大量返工时间。
- 采集与拼接脚本:Python + OpenCV,输入有序图像序列,输出等距柱状全景图;弱纹理场景下用 AKAZE,墙面色差大时切 ORB。
- 瓦片金字塔生成器:把全景图按 512x512 或 256x256 切块,生成各级 level 目录,配一个
tiles.json描述层级和文件路径。 - 查看器前端:HTML + Three.js/Panolens,包含桌面拖拽、移动端陀螺仪、热点 Marker、双击放大、全屏切换。
- 对比视图模块:A/B 透明度滑块,加上 4.2 节的变化检测脚本生成的 mask 叠加,适合巡检和进度留档。
- 报告导出脚本:把标好的热点 JSON 和全景截图整合成 PDF,或者直接生成带天气、区位、时间戳的分享页。
这套模块不是强耦合的,你可以只取查看器,也可以只用对比脚本。我最开始写的时候,完全没想到它能拆得这么开,后来重新整理了接口,每个模块都尽量“一个入口,一个输出”,这样维护和复用都轻松很多。
6.2 适合二次开发的几条路线
PanWatch 如果想继续长完整功能,方向上我优先推荐三条。
第一条是往“轻量 VR 看房”方向延伸。在现有全景图基础上,加入多个房间之间热点按钮,点击跳转到另一个全景点位,就凑成了一个能“走动”的看房链路。对中介和民宿房东来说,这比拍短视频更结构化,客户可以先在线上把户型动线和空间感判断清楚,再到实地重点看细节,效率很高。技术上的门坎主要在地图点位关系建模,前端加一个简单的图结构配置就行。
第二条是往“施工进度周报”方向做。把每周同一机位的全景图自动跑一遍对比,生成变化标记和摘要图,然后输出一页进度页。我见过一些项目经理每周在群里发几十张工地照片,很难跟进进度好坏,而带变化高亮的一张全景图说清楚“这周多做了哪几面墙、哪个区域动了管线”,会议效率完全是另一回事。
第三条是往“空间档案数据库”方向做。长期积累后,每个空间有多个时间戳版本,可以用 SQLite 或 JSON 文件建立索引,按项目、日期、问题类型检索历史全景。这一条听起来没那么酷,但它其实是把工具沉淀成资产的关键:单个房间的全景是即时价值,前后对比和版本历史则是长期价值。档案数据量不大,纯静态目录就能支撑,不需要上重型数据库。
我目前的做法,是让所有模块跑在本地仓库加一个可选的静态服务器选项上,每次采集完自动生成一个时间戳目录,里面放全景、瓦片、热点 JSON 和对比结果。把这套结构整理出来后,无论是日常搬砖还是临时给亲戚家做个验房记录,都变得非常顺手。说到底,PanWatch 不是要替代谁,它只是那条“把散乱空间照片变成可回答问题的空间档案”的路,走通之后,你也会开始相信这个方向是对的。