大图切片预览

文章目录

  • 前言
  • 处理流程
  • 完整代码
  • 前端预览

前言

最近有需求,前端要预览百兆以上的大图,这直接访问应该就不太行了,系统打开都在加载好一会儿,刚好从事的又是 gis 行业,于是打算用类似加载地图的方式来切片加载大图。这里最好是按标准的切片方式来,这样就可以用现成的地图引擎来预览了。这里就按 TMS 标准来切片。


引用一下 ChatGPT 的回答

“TMS” 代表的是 “Tile Map Service”,是一种用于在Web地图应用中加载和显示地图瓦片的标准协议。瓦片地图是将地图划分成小块瓦片,每个瓦片包含地图的一部分信息,通过加载这些瓦片可以实现整个地图的显示。
.
TMS 瓦片标准是一种用于组织和管理这些地图瓦片的约定。以下是 TMS 瓦片标准的一些关键概念:
瓦片坐标系统: TMS 使用一个瓦片坐标系统,其中地图被划分为网格状的瓦片,每个瓦片由一个唯一的坐标标识。通常,左上角的瓦片坐标是 (0,0),并且随着地图的缩放级别的增加,瓦片的坐标也相应地增加。
缩放级别: TMS 支持不同的缩放级别,每个级别对应于地图的不同分辨率。每个缩放级别的瓦片数目是前一个级别的两倍。缩放级别通过整数值表示,例如,缩放级别为 0 表示最低级别,而缩放级别为 1 表示比级别 0 更高的分辨率。
瓦片命名规则: TMS 使用一种规范的瓦片命名规则,其中瓦片的坐标和缩放级别被编码到URL中。例如,一个瓦片的URL可能类似于 http://example.com/{z}/{x}/{y}.png,其中 {z} 表示缩放级别, {x} 和 {y} 表示瓦片的坐标。
坐标原点: TMS 有两种坐标原点的定义方式,一种是以地图左上角为原点,另一种是以地图左下角为原点。这两种方式在不同的实现中有不同的选择,但都在相应的文档中明确定义。

总体而言,TMS 瓦片标准通过定义一种通用的方式来命名和组织地图瓦片,使得不同的地图服务和应用程序可以遵循相同的规范,从而实现更好的互操作性。这种标准化有助于开发者创建和集成地图服务,同时也简化了地图数据的发布和共享。


TMS 的切片可以采用金字塔切片方式,缩放级别为 0 时表示最低级别,只有一个瓦片,随着缩放级别的增加,地图被划分成更多的瓦片,每个瓦片下一级可以拆成四个,所以每一层级瓦片数就是上一层级数的四倍。
单个瓦片尺寸通常是 256x256像素

这种感觉:
https://zhuanlan.zhihu.com/p/64736752?utm_id=0
图像来源:GIS理论知识(四)之地图的图层(切片/瓦片)概念


我们项目设计是前端是固定的几个大图预览,所以直接开发个工具来切片使用就可以了。

这里决定就用 Java 来开发,也是为了后续可能做后台管理打铺垫, 但 Java 这块图像操作相关 API 真不熟,直接上 ChatGPT 问一下。

开始用 BufferedImage 来实现,但是效率不是太高,网上查了 OpenCV 效率貌似很高,直接让 ChatGPT用 OpenCV 再实现一遍,实践对比了下确实提升很大

分享一波 ChatGPT 问答
在这里插入图片描述

基于这代码改一点点,就可以完美实现了!!

处理流程

  1. 判断图像分辨率是否是 256x256 的整数倍,如果不是则需要扩大补图。(如果不这样做切好的瓦片肯定会有分辨率小于256 x 256 的,部分地图引擎可能会直接拉伸尺寸导致变形)

    Mat inputImage  = Imgcodecs.imread("xxx.tif");
    // 标准切片是正方形,只需要判断宽高最大值是否是 256 的整数倍即可
     int max = Math.max(inputImage.cols(), inputImage.rows());
     if (max % tileSize != 0) {
         double ceil = Math.ceil(max / (double) tileSize);
         inputImage = mergeTile(inputImage, (int) ceil * tileSize);
     }
    
  2. 对处理好的图像开始切片。

     int useLevel = 当前层级;
     for (int y = 0; y < inputImage.rows(); y += tileSize) {
          for (int x = 0; x < inputImage.cols(); x += tileSize) {
              // 第三四参数直接 tileSize 也可以,开始这么写是因为没有对图像尺寸做补图处理,防止超出图像尺寸报错。
              Rect roi = new Rect(x, y, Math.min(tileSize, inputImage.cols() - x), Math.min(tileSize, inputImage.rows() - y));
              Mat tile = new Mat(inputImage, roi);
              // 输出文件,如果做网络服务的话做好索引存数据库我感觉更好。
              File outputTileFile = new File(outputPath,  useLevel + File.separator + x / tileSize + File.separator + y / tileSize + ".jpg");
              if (!outputTileFile.getParentFile().exists()) {
                  outputTileFile.getParentFile().mkdirs();
              }
              Imgcodecs.imwrite(outputTileFile.getAbsolutePath(), tile);
          }
      }
    
  3. 切完一级将图像尺寸缩放一半,如果缩放一半后尺寸仍 >= 256x256,就继续循环切片。反之就结束。

    do {
        // 切片
    	// ...
    	
        Imgproc.resize(inputImage, inputImage, new Size(inputImage.cols() / 2, inputImage.rows() / 2));
    } while ((inputImage.cols() >= tileSize && inputImage.rows() >= tileSize))
    

这里打好了一个 jar 包,欢迎大家使用体验! 下载地址

在这里插入图片描述

完整代码

package top.easydu.easytools.utils;

import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;

public class ImageUtils {

    static {
        // 加载动态库,这个就是加载的 resources 目录的dll
        LibUtil.loadResourcesLibrary("lib/opencv/x64/opencv_java455.dll");
    }

    private static final Logger log = LoggerFactory.getLogger(ImageUtils.class);

    public static class ImageSplitResult {

        /**
         * 瓦片数量
         */
        public int tileCount = 0;

        /**
         * 层级数
         */
        public int levels = 0;

        @Override
        public String toString() {
            return "ImageSplitResult{" +
                    "tileCount=" + tileCount +
                    ", levels=" + levels +
                    '}';
        }
    }


    /**
     * 默认瓦片大小
     */
    private static final int DEFAULT_TILE_SIZE = 256;

    /**
     * 计算有多少级
     * @param width
     * @param height
     * @param tileSize
     * @return
     */
    private static int computeLevel(int width, int height, int tileSize) {
        int level = 0;

        do {

            width = width /2;
            height = height /2;
            level++;
        } while (width >= tileSize && height >= tileSize);

        return level;
    }

    /**
     * 图片拆分
     * @param file 图像文件
     * @param outputPath 输出路径
     */
    public static ImageSplitResult splitImage(File file, String outputPath) throws IOException {

        if (!file.exists()) {
            throw new FileNotFoundException(file.getPath());
        }

        final int tileSize = DEFAULT_TILE_SIZE;

        ImageSplitResult result = new ImageSplitResult();

        Mat inputImage  = Imgcodecs.imread(file.getAbsolutePath());

        log.info(String.format("load image: %s x %s", inputImage.rows(), inputImage.cols()));


        // 分辨率补充
        int max = Math.max(inputImage.cols(), inputImage.rows());
        if (max % tileSize != 0) {
            double ceil = Math.ceil(max / (double) tileSize);
            inputImage = mergeTile(inputImage, (int) ceil * tileSize);
        }

        File outDir = new File(outputPath);
        if (!outDir.exists()) {
            outDir.mkdirs();
        }

        long startTime = System.currentTimeMillis();

        int totalLevel = computeLevel(inputImage.cols(), inputImage.width(), tileSize);

        result.levels = totalLevel;
        
        int count = 0; // 处理了几级

        do {

            long _start = System.currentTimeMillis();

            int useLevel = totalLevel - count - 1;
            // Break the image into small tiles
            for (int y = 0; y < inputImage.rows(); y += tileSize) {
                for (int x = 0; x < inputImage.cols(); x += tileSize) {
                    Rect roi = new Rect(x, y, Math.min(tileSize, inputImage.cols() - x), Math.min(tileSize, inputImage.rows() - y));
                    Mat tile = new Mat(inputImage, roi);
                    // Save the tile to the output folder
                    File outputTileFile = new File(outputPath,  useLevel + File.separator + x / tileSize + File.separator + y / tileSize + ".jpg");
                    if (!outputTileFile.getParentFile().exists()) {
                        outputTileFile.getParentFile().mkdirs();
                    }
                    Imgcodecs.imwrite(outputTileFile.getAbsolutePath(), tile);
                    result.tileCount++;
                }
            }
            log.info(String.format("level: %s time: %s ms", useLevel, System.currentTimeMillis() - _start));

            Imgproc.resize(inputImage, inputImage, new Size(inputImage.cols() / 2, inputImage.rows() / 2));

            count ++;
        } while ((inputImage.cols() >= tileSize && inputImage.rows() >= tileSize));

        log.info(String.format("切片完成, 耗时: %s MS", System.currentTimeMillis() - startTime));


        return result;
    }

    private static Mat mergeTile(Mat tile, int size) {

        if (tile.rows() == size && tile.cols() == size) {
            return tile;
        }
        Mat baseTile = new Mat(size, size, CvType.CV_8UC3, Scalar.all(255));
        Rect newRoi = new Rect(0, 0, tile.cols(), tile.rows());
        Mat roiMat = new Mat(baseTile, newRoi);
        tile.copyTo(roiMat);

        return baseTile;
    }

    public static ImageSplitResult splitImage(String filePath, String outputPath) throws IOException {

        return splitImage(new File(filePath), outputPath);

    }
}

前端预览

直接使用 leatlet 来加载切好的瓦片,效果还是很不错的 !!! 理论上支持 TMS 瓦片标准的地图引擎都可以直接使用的!
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


有问题或优化建议欢迎指导 ~~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/301810.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

视频做成二维码查看?多格式视频二维码生成器的使用方法

现在音视频是工作和生活中经常需要使用的一种内容表现形式&#xff0c;很多人都通过这种方式来查看视频内容&#xff0c;比如产品介绍、使用说明、安装教程等。通过一个二维码就可以来承载视频内容&#xff0c;与传统的方式相比拥有更快的内容传播速度&#xff0c;简化用户获取…

04.SpringCloud网关-gateway

1.Gateway服务网关 Spring Cloud Gateway 是 Spring Cloud 的一个全新项目&#xff0c;该项目是基于 Spring 5.0&#xff0c;Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关&#xff0c;它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式…

Stm32cube keil5配置串口printf 蓝牙打印不出来

1.检查cube里面波特率是否与AT蓝牙设置一致 2.keil里面设置是否打开Use MicroLIB 3、stm32cube是否开启串口中断 4.检测线路是否接触不良&#xff0c;读写线插反等。

这一次技术学习分享,超过苦读30本书

同学们&#xff0c;做个问卷调查&#xff0c;你参加了这次由腾讯云主办的第四期“云梯计划”了不&#xff1f; “云梯计划”已连续举办三年&#xff0c;免费为超过1万名大学生提供了腾讯云认证培训和考试名额&#xff0c;帮助其提升就业竞争力。 想要得到免费的系统性、实战性…

SpringBoot集成 Websocket 实现服务与客户端进行消息发送和接收

介绍 WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。 效果 客户端效果 服务端日志 pom依赖 <!-- websocket --> <dependency><groupId>org.springfram…

APM32F035有感矢量控制方案

一、先来几句废话 首先这两年公司越来越多的开始使用国产的MCU&#xff0c;用过GD32、AT32、APM32等等&#xff0c;目前稳定使用的是APM32,包括身边朋友工作室&#xff0c;也开始从TI、STM、NXP换成APM32。上个月有幸拿到APM32F035电路控制板&#xff0c;非常感谢面包板社区提供…

Pytest自动化测试框架

1、pytest简介 pytest是Python的一种单元测试框架&#xff0c;与python自带的unittest测试框架类似&#xff0c;但是比unittest框架使用起来更简洁&#xff0c;效率更高。 执行测试过程中可以将某些测试跳过&#xff0c;或者对某些预期失败的case标记成失败能够支持简单的单元…

服务器网卡介绍

本篇文章对服务器网卡进行基础介绍&#xff0c;包括基本概念、网卡厂商及网卡绑定策略等。 1、基本概念 服务器网卡&#xff08;Network Interface Card&#xff09;是一种用于连接服务器与网络之间的硬件设备。它允许服务器通过网络与其他设备进行通信&#xff0c;包括传输数…

九州金榜孩子厌学原因及解决方法

厌学是根据不同类型孩子&#xff0c;表现也有差异&#xff0c;但是者都会对成绩产生很大的影响。那么作为家长&#xff0c;我们应该怎么面对不同类型孩子的厌学&#xff0c;又该怎么样去解决呢&#xff0c;我们要如何让孩子重拾学习的信心呢&#xff1f;下面&#xff0c;我们从…

如何查看崩溃日志

​ 目录 描述 思路 查看ipa包崩溃日志 简单查看手机崩溃信息几种方式 方式1:手机设置查看崩溃日志 方式2: Xocde工具 方式3: 第三方软件克魔助手 环境配置 实时日志 奔溃日志分析 方式四&#xff1a;控制台资源库 线上崩溃日志 线上监听crash的几种方式 方式1: 三…

SQLServer设置端口,并设置SQLServer和SQLServer Browser服务

SQLServer默认使用动态端口&#xff0c;即每次启动sqlserver.exe时&#xff0c;端口port都会动态变化。若要使用静态端口&#xff0c;比如port1433&#xff0c;则需要在SQL Server Configuration Manager(简称SSMS&#xff09;里配置。这里以SQL Server 2005 Configuration Man…

计算机基础面试题 |16.精选计算机基础面试题

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

2024年【安全员-A证】及安全员-A证证考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 安全员-A证考前必练&#xff01;安全生产模拟考试一点通每个月更新安全员-A证证考试题目及答案&#xff01;多做几遍&#xff0c;其实通过安全员-A证复审模拟考试很简单。 1、【多选题】《建设工程安全生产管理条例》…

JavaScript版数据结构与算法(一)栈、队列、链表、集合、树

一、前言 为什么要学习数据结构与算法&#xff1f;最重要的就是面试要考算法&#xff0c;另外就是如果在实际工作当中&#xff0c;能够使用算法优化代码&#xff0c;会提升代码质量和运行效率&#xff0c;作为一名前端人员可能在实际中用的并不是特别多。数据结构与算法是分不…

在win10和Linux上配置SSH 无密码登录

文章目录 一、用途二、在本地机器上使用ssh-keygen产生公钥私钥对1&#xff09;在Linux (或macOS) 上产生SSH公私钥的方法2&#xff09;在win10上产生SSH公私钥的方法a&#xff09;检查windows 本地是否安装有sshb&#xff09;在本地生成SSH密钥对&#xff08;公钥和私钥&#…

Java TBA访问NetSuite Restlet时的403错误

本周有同学问为啥Java访问NetSuite Restlet时&#xff0c;按照知识会之前的文章分享&#xff0c;会一直报403 INVALID_LOGIN_ATTEMPT错误。 https://nk-community.blog.csdn.net/article/details/131399801https://nk-community.blog.csdn.net/article/details/131399801原因是…

jenkins 参数化构建过程,jenkins下拉框选择要部署项目,jenkins部署java微服务项目,jenkins部署微服务

1. jenkins部署demo 优化点&#xff1a; 选择丢弃旧的构建&#xff0c;最大个数可以选择3或者5个&#xff0c;如果微服务&#xff0c;十个jar,占用很多空间&#xff0c;多来几次部署&#xff0c;硬盘满了 2. 选择参数构建&#xff0c;需要部署哪个项目 名称可以设置为你的项目…

CANFDLog-OTL 的UDS诊断和刷写功能应用简介

随着汽车电控系统比重增加和车辆下线自动化程度的提高&#xff0c;企业生产节拍逐渐加快&#xff0c;整车下线的稳定可靠越来越依靠下线诊断系统。 下线诊断流程如下: 1.程序刷写 对控制器进行程序刷写&#xff0c;支持S19/HEX/BIN等格式 2.防盗匹配 钥匙防盗匹配功能和智能钥匙…

vue-springboot基于java的实验室安全考试系统

本系统为用户而设计制作实验室安全考试系统&#xff0c;旨在实现实验室安全考试智能化、现代化管理。本实验室安全考试管理自动化系统的开发和研制的最终目的是将实验室安全考试的运作模式从手工记录数据转变为网络信息查询管理&#xff0c;从而为现代管理人员的使用提供更多的…

list-watch和节点亲和性和node亲和性

k8s的集群调度 scheduler:负责调度资源&#xff0c;把pod调度到node节点 预算策略 优先策略 1、list-watch k8s集群当中&#xff0c;通过list-watch的机制进行每个组件的协作&#xff0c;保持数据同步&#xff0c;每个组件之间解耦 kubectl配置文件&#xff0c;向APIserv…