likes
comments
collection
share

【Got it】用NodeJs实现krpano多分辨率切图

作者站长头像
站长
· 阅读数 23

【Got it】用NodeJs实现krpano多分辨率切图

前言:最近发现公司的krpano切图服务非常慢,严重影响了用户使用,而且我们使用的功能比较单一只有多分辨率切图一项,所以思考用其他方案代替切图服务,自己开发的好处就是可控性强,个性化开发更新迭代方便。

krpano 简述

krpano是一款用于创建全景虚拟场景的软件,它支持HTML5、Flash和iOS等多种平台,具有高度的自定义性和交互性,可以创建各种类型的虚拟场景,例如全景图片、全景视频、立体图像等。

krpano常用的切图类型有:

  • 普通切图
    • 优点:切图速度快,占用内存少。缺点:启动不够快,放大模糊。
  • 多分辨率切图
    • 多分辨率切图,跟瓦片地图原理类似。优点:启动速度快,图片清晰。缺点:占用内存较多,切图时间较久,一般用于航拍、风景等大范围场景的需求。

多分辨率瓦图

在 krpano 中,多分辨率瓦图(multi-resolution tiled image)是一种用于展示全景图的技术。其中,瓦图指的是将全景图切分成多个小图块,以便在不同的缩放级别下加载和显示。 在 krpano 中,多分辨率瓦图主要有两种类型:cube 和 sphere。 它们的区别如下:

  • Cube 多分辨率瓦图

    • 将全景图分为 6 个面(前、后、左、右、上、下),每个面都是一个矩形。在显示时,将 6 个面拼接在一起,形成一个立方体,用户可以在立方体的内部自由地观察全景图。Cube 瓦图适用于展示室内场景或建筑物内部,可以在立方体内部自由浏览。
  • Sphere 多分辨率瓦图

    • 将全景图分为多个圆柱形面,每个面都是一个圆柱形切片。在显示时,将多个圆柱形切片拼接在一起,形成一个球体,用户可以在球体的表面上自由地观察全景图。Sphere 瓦图适用于展示室外场景或自然风景,可以在球体表面上自由浏览。 需要注意的是,Cube 和 Sphere 瓦图的切分方式不同,因此它们的加载和显示效果也略有差异。在实际使用中,应根据具体的场景和需求选择合适的多分辨率瓦图类型。

一般全景设备拍摄的全景图是球体结构,球体结构切Sphere瓦图会更快,少了转换成立方体的一个步骤,球体转立方体很耗时,以下都用Sphere瓦图讲解整个算法流程

技术方案选择

NodeJs中可用于图片操作的库有:node-canvasgmnode-sharpJimp,基于同等的图片处理逻辑及源图20000x10000像素,下面选取其中的3个做比较:

图片框架处理时间
node-canvas17s
node-graphicsmagick太慢了一直没反应
node-sharp3分33秒

以下均以node-canvas为图片处理库展开

前置知识ImageData

ImageData用于表示一幅图片的像素数据。它可以用来读取和修改一张图片的像素信息,支持灰度图像、彩色图像和透明度信息等。

ImageData对象的主要属性和方法如下:

  1. width和height:表示图片的宽度和高度。
  2. data:表示一个二进制数组,存储了图片的像素数据,每个像素由4个字节表示,分别代表红、绿、蓝和透明度。

使用ImageData对象可以实现一些复杂的图像处理和特效,例如模糊、锐化、色彩调整、边缘检测、图像识别等。但同时也需要注意ImageData对象的像素数据量较大,需要合理使用,避免对性能造成影响。

多分辨率算法

1. 切图结构

krpano可以根据不同的屏幕分辨率和网络速度自动调整图片的分辨率和清晰度,提高用户的观感和体验效果。多分辨率瓦片图是通过将一张大图分割成多个小图块,每个小图块都有不同的分辨率和清晰度,然后根据需要动态加载和显示不同的图块,从而实现多分辨率和快速加载的效果。

  1. 多分辨率瓦图目录结构

【Got it】用NodeJs实现krpano多分辨率切图 2. 常规目录结构

【Got it】用NodeJs实现krpano多分辨率切图

总结:

  • l1/l2/l3指的是不同分辨率下的瓦片图,即缩小放大显示对应分辨率下的瓦图,如果由小分辨率放大之后,会将可视区域内的同位置大分辨率的瓦图加载,就会给人越放大越清晰的感觉
  • 01/02/03指的是当前分辨率下的图按照512*512为及基准来计算图片可分为多少行列,例如2048*1024可分为2行4列

2. 瓦片图规律

将各个分辨率的全景图使用krpano官方的工具切图得到如下数据:

【Got it】用NodeJs实现krpano多分辨率切图

由此得出规律: 所有数字都是64的倍数,除了2个标红的,那就自动忽视它

3. 多分辨率算法

每层的分辨率是当前层基数最接近64的倍数的一个数, 比如12000x6000,基数为L4=12000;L3=6000;L2=3000;L1=1500,基数的规则为:

L最小层基数 <= 2048

满足规则后停止计算,具体算法如下:

/**
 * 计算层级算法
 * @param panoWidth 每个层级的基数
 * @param levelConfig 计算好的层级数据
 * @returns ILevelConfig[]
 */
export function analyzeImagesLevel(
    panoWidth: number,
    levelConfig: ILevelConfig[] = []
) {
    const levelSize = nearestNumber(panoWidth);
    levelConfig.push({
        row: Math.ceil(levelSize / 2 / 512),
        col: Math.ceil(levelSize / 512),
        size: levelSize,
    });
    if (levelSize / 2 <= 1024) {
        return levelConfig;
    }
    return analyzeImagesLevel(panoWidth / 2, levelConfig);
}

然后找到最接近基数的64倍数数字:

/**
 * 离某数字最近的64倍数数字
 * @param n
 * @returns number
 */
export function nearestNumber(n: number) {
    return Math.round(n / 64) * 64;
}

开始生成瓦图

/**
 * 生成瓦图
 * @param level 通过analyzeImagesLevel计算的levelConfig
 * @param imageData 读取源图得到的ImageData
 * @param imageName 读取源图得到的图片名称
 * @param maxTileSize 最大的瓦图尺寸 默认512
 * @param imageSavePath 瓦图保存路径
 * @returns void
 */
const generateTiles = async (
    level: ILevelConfig[] = [],
    imageData: ImageData,
    imageName: string,
    maxTileSize: number,
    imageSavePath: string
) => {
    
    const sourceCanvas = new Canvas(imageData.width, imageData.height);
    const sourceCtx = sourceCanvas.getContext("2d");
    sourceCtx.putImageData(imageData, 0, 0);

    const tempCanvas = new Canvas(imageData.width, imageData.height);
    const tempCtx = tempCanvas.getContext("2d");

    const tilesCanvas = new Canvas(0, 0);
    const tilesCtx = tilesCanvas.getContext("2d");

    for (let i = 0; i < level.length; i++) {
        const nowLevel = level[i];

        tempCanvas.width = nowLevel.size;
        tempCanvas.height = nowLevel.size / 2;
        tempCtx.drawImage(sourceCanvas, 0, 0, tempCanvas.width, tempCanvas.height);

        // 最后一列宽
        const lastRowWidth = nowLevel.size % maxTileSize;
        // 最后一行高
        const lastColHeight = (nowLevel.size / 2) % maxTileSize;

        for (let j = 0; j < nowLevel.col; j++) {
            for (let k = 0; k < nowLevel.row; k++) {
                const nowIsLastX = lastRowWidth && j === nowLevel.col - 1;
                const nowIsLastY = lastColHeight && k === nowLevel.row - 1;
                const sx = j * maxTileSize;
                const sy = k * maxTileSize;
                const sw = nowIsLastX ? lastRowWidth : maxTileSize;
                const sh = nowIsLastY ? lastColHeight : maxTileSize;
                const tilesImageData = tempCtx.getImageData(sx, sy, sw, sh);
                tilesCanvas.width = sw;
                tilesCanvas.height = sh;
                tilesCtx.putImageData(tilesImageData, 0, 0, 0, 0, sw, sh);

                const buf = tilesCanvas.toBuffer("image/jpeg", { quality: 0.92 });

                createLimitFile({
                    path: `${imageSavePath}/tiles/${imageName}/l${
                        level.length - i
                        }/${
                        formatNumToStr(k + 1)
                        }/l${level.length - i}_${formatNumToStr(k + 1)}_${
                        formatNumToStr( j + 1 )}.jpg`,
                    buf
                })

            }
        }
    }

}
    
function createLimitFile(limitFile: ILimitTask) {
    // 判断文件夹是否存在,文件夹不存在,创建文件夹
    existsMkdirFolder(limitFile.path)

    const ws = createWriteStream(limitFile.path);
    ws.on("error", (e: Error) => {
        console.error(e);
    });
    ws.write(limitFile.buf);
    ws.end();
}

注意,这样生成会有个问题,就是频繁操作会造成内存报警导致宕机,所以考虑采用async库的queue来控制createLimitFile生成文件的频率,对应的使用文档在此,使用最高并发为5来控制

总结

技术概要:

  • canvas ImageData
  • 推理 krpano sphere 多分辨率算法
  • async queue

自己实现逻辑虽然有点小麻烦,但是完成之后可控性大大提升。还是得工作驱动才更有动力

后续的一些想法和优化:

  1. 多进程
  2. 任务队列
  3. 瓦片图合并为全景图
  4. 立方体 cube 多分辨率瓦图
  5. 作品离线包功能

最后,有需要的直接👉源码传送口,有用的话,记得给作者点个star😉