专业编程教程与实战项目分享平台

网站首页 > 技术文章 正文

前端加载超大图片(100M以上)实现秒开解决方案

ins518 2024-12-15 13:55:06 技术文章 35 ℃ 0 评论

前言

前端加载超大图片时,一般可以采取以下措施实现加速:

  1. 图片压缩:将图片进行压缩可以大幅减小图片的大小,从而缩短加载时间。压缩图片时需要注意保持图片质量,以免影响图片显示效果。
  2. 图片分割:将超大图片分割成多个小图块进行加载,可以避免一次性加载整个图片,从而加快加载速度。这种方式需要在前端实现图片拼接,需要确保拼接后的图片无缝衔接。
  3. CDN 加速:使用 CDN(内容分发网络)可以将图片缓存在离用户更近的节点上,从而加速图片加载速度。如果需要加载的图片是静态资源,可以将其存储在 CDN 上,以便快速访问。
  4. 懒加载:懒加载是一种图片延迟加载的方式,即当用户浏览到需要加载的图片时才进行加载,可以有效避免一次性加载大量图片而导致页面加载速度缓慢。
  5. WebP 格式:使用 WebP 格式可以将图片大小减小到 JPEG 和 PNG 的一半以下,从而加快图片加载速度。
  6. HTTP/2:使用 HTTP/2 协议可以并行加载多个图片,从而加快页面加载速度。
  7. 预加载:预加载是在页面加载完毕后,提前加载下一步所需要的资源。在图片加载方面,可以在页面加载完毕后提前加载下一个需要显示的图片,以便用户快速浏览。

而对于几百M或上G的大图而言,不管对图片进行怎么优化或加速处理,要实现秒开也是不太可能的事情。而上面介绍的第二条“图像分割切片”是最佳解决方案。下面介绍下如何对大图进行分割,在前端进行拼接实现秒开。

图像切片原理介绍

图像切片是指将一张大图分割成若干个小图的过程,以便于存储和处理。图像切片常用于网络地图、瓦片地图、图像拼接等应用中。

切片原理主要包括以下几个步骤:

  1. 定义切片大小:首先需要定义每个小图的大小,一般情况下是正方形或矩形。
  2. 计算切片数量:根据定义的切片大小,计算原始图像需要被切成多少个小图。计算公式为:切片数量 = 原始图像宽度 / 切片宽度 × 原始图像高度 / 切片高度。
  3. 切割图像:按照计算出的切片数量,将原始图像分割成相应数量的小图。可以使用图像处理库或自己编写代码实现。
  4. 存储切片:将切割后的小图存储到磁盘上,可以使用常见的图片格式,如JPEG、PNG等。
  5. 加载切片:在需要显示切片的地方,根据需要加载相应的小图,组合成完整的图像。

使用图像切片可以降低处理大图像的复杂度,同时也能够提高图像的加载速度,使得用户可以更快地查看图像的细节。图像切片广泛应用于需要处理大图像的场景,能够提高图像处理和显示效率,同时也能够提高用户的体验。

实现

先上效果图

上传打开图形

先上传大图,至后台进行切片处理, 上传相关代码为:

async onChangeFile(file) {
            try {
                message.info('文件上传中,请稍候...')
                this.isSelectFile = false;
                this.uploadMapResult = await svc.uploadMap(file.raw);
                if (this.uploadMapResult.error) {
                    message.error('上传图形失败!' + this.uploadMapResult.error)
                    return
                }
                this.form.mapid = this.uploadMapResult.mapid;
                this.form.uploadname = this.uploadMapResult.uploadname;
                this.maptype = this.uploadMapResult.maptype || '';
                this.dialogVisible = true;
            } catch (error) {
                console.error(error);
                message.error('上传图形失败!', error)
            }
        }
复制代码

如果需要上传后对图像进行处理,可以新建一个cmd.txt文件,把处理的命令写进文件中,然后和图像一起打包成zip上传。

如需要把1.jpg,2.jpg拼接成一个新的图片m1.png再打开,cmd.txt的写法如下:

join
1.jpg
2.jpg
m1.png
horizontal
复制代码

再把1.jpg,2.jpg,cmd.txt三个文件打包成zip文件上传即可

打开图像相关代码

async onOpenMap() {
            try {
                let mapid = this.form.mapid;
                let param = {
                    ...this.uploadMapResult,
                    // 图名称
                    mapid: this.form.mapid,
                    // 上传完返回的fileid
                    fileid: this.uploadMapResult.fileid,
                    // 上传完返回的文件名
                    uploadname: this.form.uploadname,
                    // 地图打开方式
                    mapopenway: this.form.openway === "直接打开图形" ? vjmap.MapOpenWay.Memory : vjmap.MapOpenWay.GeomRender,
                    // 如果要密码访问的话,设置秘钥值
                    secretKey: this.form.isPasswordProtection ? svc.pwdToSecretKey(this.form.password) : undefined,
                    style: vjmap.openMapDarkStyle(),// div为深色背景颜色时,这里也传深色背景样式
                    // 图像类型设置地图左上角坐标和分辨率
                    imageLeft: this.form.imageLeft ? +this.form.imageLeft : undefined,
                    imageTop: this.form.imageTop ? +this.form.imageTop : undefined,
                    imageResolution: this.form.imageResolution ? +this.form.imageResolution : undefined,
                }
                let isVectorStyle = this.form.openway === "存储后渲染矢量";
                await openMap(param, isVectorStyle);
            } catch (error) {
                console.error(error);
                message.error('打开图形失败!', error)
            }
        }
复制代码

应用案例

应用一 对图像进行拼接前端查看

原始图片为

最终效果为:

体验地址: vjmap.com/app/cloud/#…

应用二 对tiff影像进行切片并与CAD图叠加校准

对tiff影像上传时可设置地理坐标范围。

tiff/tfw, jpg/jpgw坐标文件的格式(6个参数) 0.030000 0.0000000000 0.0000000000 -0.030000 451510.875000 3358045.000000

以上每行对应的含义:

1 地图单元中的一个象素在X方向上的X分辨率尺度。 2 平移量。 3 旋转量。 4 地图单元中的一个象素在Y方向上的Y分辨率尺度的负值。 5 象素1,1(左上方)的X地坐标。 6 象素1,1(左上方)的Y地坐标。

在上传图时需要根据文件中的第一个,第五个和第六个值设置地图范围

或者上传完后,操作菜单中点击设置地图范围进行设置

影像地图切片完成后,可与CAD图进行叠加校准。效果如下


地图切片除了需要了解瓦片计算逻辑,还会涉及到数据投影、分辨率,重采样相关能力这里我们使用第三方 库 Gdal 的node 版本 node-gdal。

需求

遥感数据一般比较大,几百MB 或者几个 GB,如何在前端浏览器可视化确实是个问题,数据量不仅数据加载缓慢、而且地图渲染性能有很高的要求。瓦片是解决数据量大最好的方案,那问题怎么把一个 GeoTiff切成瓦片,目前没有合适的开源工具。

切片流程

读取文件

Gdal node 版本

naturalatlas.github.io/node-gdal/c…

读取文件为GeoTiff 文件,这里使用 Gdal 进行读取。

// 引入node-gdal 库
var gdal = require("gdal-next")
var dataset = gdal.open("../landcover_JiangXi.tif");
复制代码

读取到文件 Gdal 提供了一些方法获取栅格影像的相关信息如坐标系、大小、分辨率,数据等等

重采样生成影像金字塔

为什么需要重采样,瓦片切片需要根据地图缩放层级、生成不同分辨率的影像也就生成瓦片金字塔模型。

不同分辨率

分辨率计算

地球周长除以地图像素大小既为分辨率,地球半径为固定值,地图像素大小跟地图缩放等级相关

  • 初始分辨率第 0 层级 一张 256 * 256 的瓦片即可包含整个世界。
var initialResolution =2  * Math.PI * 6378137 / 256
复制代码
  • 各层级分辨率计算
 var resolution = initialResolution / Math.pow(2, N)
 
复制代码

重采样大小

  • width、height 原栅格数据长、宽
  • xSize、ySize 原栅格数据分辨率
  • resolution 目标分辨率
 const x =  Math.round(width * xSize / resolution),
 const y =  Math.abs(Math.round(height * ySize / resolution))
复制代码

相关参数计算完成之后,我们就可以进行重采样了,这里可以使用 reprojectImage 方法。

   reprojectRaster(dataset, zoom, path) {
        const driver = dataset.driver;
        const tileHelp = this.tileHelper;
        const resolution = tileHelp.resolution(zoom);
        const rasterSize = dataset.rasterSize;
        const newSize = this.scaleZoomSize(rasterSize.x, rasterSize.y, dataset.geoTransform[1], dataset.geoTransform[5], resolution);
        const outDs = driver.create(path, newSize.x, newSize.y, 1, this.getDataType());
        const gcp = dataset.getGCPs();
        const gcpProject = dataset.srs.toWKT();
        const [x, xtr, xr, yx, yr, ytr] = dataset.geoTransform;
        outDs.geoTransform = [x, resolution, xr, yx, yr, -resolution];
        outDs.setGCPs(gcp, gcpProject);
        outDs.bands.get(1).colorInterpretation = dataset.bands.get(1).colorInterpretation;
        const option = {
            src: dataset,
            dst: outDs,
            s_srs: dataset.srs,
            t_srs: dataset.srs,
            resampling: gdal.GRA_NearestNeighbor,

        };
        gdal.reprojectImage(option)
        outDs.flush();
        outDs.close();

    }
复制代码

瓦片切片-平面分块

重采样之后,我们得到了不同分辨率的栅格影像,接下来就可以根据规则进行切片了。

栅格数据范围

栅格数据的经纬度范围直接 Gdal 可以读取,范围主要用于计算数据包含的瓦片范围进而进行切片。原始数据坐标系一般不是是经纬度坐标系,需要进行坐标转换。 原始坐标范围计算,ds.geoTransform 具体参数如下。

geoTransform坐标转换

 getRasterExtent() {
        const ds = this.dataset
        const size = ds.rasterSize
        const minCorner = { x: 0, y: size.y };
        const maxCorner = { x: size.x, y: 0 };
        const geotransform = ds.geoTransform;
        const wgs84 = gdal.SpatialReference.fromEPSG(4326);
        const coord_transform = new gdal.CoordinateTransformation(ds.srs, wgs84);
        const min = {
            x: geotransform[0] + minCorner.x * geotransform[1] + minCorner.y * geotransform[2],
            y: geotransform[3] + minCorner.x * geotransform[4] + minCorner.y * geotransform[5]
        }
        const max = {
            x: geotransform[0] + maxCorner.x * geotransform[1] + maxCorner.y * geotransform[2],
            y: geotransform[3] + maxCorner.x * geotransform[4] + maxCorner.y * geotransform[5]
        }
        const minWgs84 = coord_transform.transformPoint(min);
        const maxWgs84 = coord_transform.transformPoint(max);
        return [minWgs84.y, minWgs84.x, maxWgs84.y, maxWgs84.x]
    }
复制代码

瓦片范围计算

瓦片范围计算就需要了解瓦片技术规则了,经纬度坐标如何转瓦片坐标,如何计算参照上篇文章

 boundsToTileExtent(minLon, minLat, maxLon, maxLat, zoom) {
        const [minTx, minTy] = this.lonLatToTile(minLon, maxLat, zoom);
        const [maxTx, maxTy] = this.lonLatToTile(maxLon, minLat, zoom);
        return [[minTx, minTy], [maxTx, maxTy]]
    }
复制代码

读取数据

瓦片行列号范围计算好之后我们就可以根据行列号逐个读取瓦片的数据了。读取数据需要处理为边缘的瓦片,数据超出区域的情况。

    // 读取指定起点的瓦片数据 
    readRaster(origin) {
        const tileHelp = this.tileHelper;
        const band = this.dataset.bands.get(1);
        const rasterSize = this.dataset.rasterSize;
        const start = [Math.min(Math.max(0, origin[0]), rasterSize.x - 1), Math.min(Math.max(0, origin[1]), rasterSize.y - 1)];
        const end = [Math.min(origin[0] + 256, rasterSize.x), Math.min(origin[1] + 256, rasterSize.y)]
        const x = origin[0] < 0 ? -  origin[0] : 0;
        const y = origin[1] < 0 ? -  origin[1] : 0;
        const height= end[1] -start[1]
        const width= end[0] -start[0]
        const data = Array.from(band.pixels.read(start[0], start[1], width, height))
        return {
            data,
            x,
            y,
            height,
            width
        }
     
    }
复制代码

写入数据

写入数据也需要注意,在边缘的瓦片数据,数据并不是完整的,需要计算数据写入起始坐标。

 writeTiffTile(x, y, z, data) {
        const dataset = this.dataset;
        const driver = dataset.driver;
        const tileHelp = this.tileHelper;
        if (!fs.existsSync(`${this.outPath}/${z}`)) {
            fs.mkdirSync(`${this.outPath}/${z}`)
        }
        if (!fs.existsSync(`${this.outPath}/${z}/${x}`)) {
            fs.mkdirSync(`${this.outPath}/${z}/${x}`)
        }
        const outDs = driver.create(`${this.outPath}/${z}/${x}/${y}.tiff`, tileHelp.tileSize, tileHelp.tileSize, 1, this.getDataType());
        const gcp = dataset.getGCPs();
        const gcpProject = dataset.srs.toWKT();
        const resolution = tileHelp.resolution(z);
        const originMeters = tileHelp.pixelsToMeters(x * 256, y * 256, z)
        const geoTransform = [originMeters[0], resolution, 0, originMeters[1], 0, -resolution];
        outDs.geoTransform = geoTransform;

        outDs.setGCPs(gcp, gcpProject)
        const outBand = outDs.bands.get(1);
        outBand.colorInterpretation = dataset.bands.get(1).colorInterpretation;
        outBand.noDataValue = this.noDataValue;
        outDs.srs = dataset.srs;
        outBand.fill(this.noDataValue);
        // TODO 数据类型
        outBand.pixels.write(data.x, data.y, data.width, data.height, new DataTypeEnum[this.getDataType()](data.data))
        outBand.computeStatistics(true)
        outDs.flush()
        outDs.close();
    }
复制代码

数据可视化

完成上面的布局,我们就可以实现栅格数据切片了,不再需要担心数据量大,无法显示了。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表