最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

使用Java拼接長圖/網(wǎng)格圖的避坑指南

 更新時間:2026年04月26日 14:17:32   作者:九轉(zhuǎn)成圣  
在日常開發(fā)中,我們經(jīng)常會遇到需要將多張圖片拼接成一張大圖的場景,今天就來盤點(diǎn)一下 Java 圖片拼接中最容易踩的 4 個大坑,以及完美的避坑指南,需要的朋友可以參考下

實(shí)戰(zhàn)記錄:用 Java 拼接長圖/網(wǎng)格圖,我踩了哪些坑?

在日常開發(fā)中,我們經(jīng)常會遇到需要將多張圖片拼接成一張大圖的場景,比如電商領(lǐng)域的商品詳情圖拼接、視頻抽幀后的雪碧圖生成等。

一開始,我覺得這事兒特別簡單:不就是創(chuàng)建一個大畫布(BufferedImage),然后用 Graphics2D 寫個 for 循環(huán)遍歷調(diào)用 drawImage 嗎?

結(jié)果真到了實(shí)戰(zhàn),尤其是接入手邊各種不規(guī)則的真實(shí)素材后,才發(fā)現(xiàn)里面暗礁險(xiǎn)灘到處都是。今天就來盤點(diǎn)一下 Java 圖片拼接中最容易踩的 4 個大坑,以及完美的避坑指南。

坑一:內(nèi)存 OOM (Out Of Memory) 爆炸

踩坑現(xiàn)象:
當(dāng)測試只用幾張小圖時,代碼跑得非常絲滑。但當(dāng)我把幾十張高清商品圖或者視頻抽幀序列塞進(jìn)去時,JVM 直接拋出 java.lang.OutOfMemoryError: Java heap space,程序當(dāng)場崩潰。

填坑指南:
Java 在將圖片讀取為 BufferedImage 時,是在內(nèi)存中將其解壓為無損的位圖(Bitmap)數(shù)據(jù)的。一張幾 MB 的 JPG,解壓到內(nèi)存里可能會占用幾十 MB 甚至上百 MB。
如果在 for 循環(huán)里不斷 ImageIO.read 而不釋放,內(nèi)存瞬間就會被吃光。

正解: 在每次繪制完畢后,立刻手動調(diào)用 flush() 方法清理底層緩存。

BufferedImage img = ImageIO.read(file);
g2d.drawImage(img, x, y, null);
// 關(guān)鍵:畫完立刻釋放資源,防止 OOM!
img.flush(); 

坑二:詭異的排序陷阱(1, 10, 2…)

踩坑現(xiàn)象:
明明文件夾里的圖片命名是 1.jpg, 2.jpg10.jpg,結(jié)果拼接出來的圖片順序完全亂了,第 10 張圖跑到了第 2 張圖前面!

填坑指南:
這是因?yàn)槲覀兂S玫?File.listFiles() 獲取到的文件列表是無序的,而使用默認(rèn)的 Comparator 排序時,采用的是字典序(String 比較)。在字典序中,字符 '1' 后面跟著 '0'10.jpg 會排在 '2' 開頭的 2.jpg 前面。

正解: 需要寫一個**“數(shù)字敏感”的自定義排序器(Natural Sort)**。嘗試提取文件名中的數(shù)字進(jìn)行比較:

// 使用自定義比較器,提取純數(shù)字進(jìn)行對比
.sorted((f1, f2) -> {
    String name1 = f1.getName().replaceAll("[^0-9]", "");
    String name2 = f2.getName().replaceAll("[^0-9]", "");
    try {
        if (!name1.isEmpty() && !name2.isEmpty()) {
            return Integer.compare(Integer.parseInt(name1), Integer.parseInt(name2));
        }
    } catch (NumberFormatException ignored) {}
    return f1.getName().compareTo(f2.getName()); // 提取失敗則退化為字典序
})

坑三(最致命):圖片尺寸不一導(dǎo)致網(wǎng)格崩壞

踩坑現(xiàn)象:
我們通常會把第一張圖片的大小作為“標(biāo)準(zhǔn)格子”的尺寸。如果所有的圖片都一樣大,那就天下太平。但現(xiàn)實(shí)是骨感的:素材庫里往往既有高挑的模特展示圖,又有寬扁的尺碼表,還有正方形的局部細(xì)節(jié)圖。
如果直接畫進(jìn)去,大圖片會直接溢出當(dāng)前的格子,覆蓋掉旁邊的圖片,最后拼出來的大圖簡直是個災(zāi)難(排版錯亂、畫面互相遮擋)。

填坑指南:
絕對不能盲目繪制!我們需要引入**“標(biāo)準(zhǔn)單元格 (Cell)”** 和 “等比例縮放居中 (Scale to Fit & Center)” 的概念。

  1. 以第一張圖確立標(biāo)準(zhǔn)格子的 cellWidthcellHeight。
  2. 對于后續(xù)每一張圖,計(jì)算它與標(biāo)準(zhǔn)格子的寬高比,得出縮放比例 scale(取寬高比例中較小的值,以確保整張圖都能塞進(jìn)格子里)。
  3. 計(jì)算居中繪制的偏移量 drawXdrawY??瞻撞糠肿匀粫冻龅咨ㄈ绨咨?/li>

正解核心邏輯:

int imgW = img.getWidth();
int imgH = img.getHeight();

// 1. 計(jì)算縮放比,取極小值確保不越界
double scale = Math.min((double) cellWidth / imgW, (double) cellHeight / imgH);

// 2. 計(jì)算實(shí)際繪制尺寸
int drawW = (int) (imgW * scale);
int drawH = (int) (imgH * scale);

// 3. 計(jì)算居中坐標(biāo) (cellStartX/Y 是當(dāng)前格子的左上角起點(diǎn))
int drawX = cellStartX + (cellWidth - drawW) / 2;
int drawY = cellStartY + (cellHeight - drawH) / 2;

// 4. 指定寬高進(jìn)行繪制
g2d.drawImage(img, drawX, drawY, drawW, drawH, null);

坑四:縮放導(dǎo)致尺碼表文字模糊

踩坑現(xiàn)象:
解決了坑三之后,發(fā)現(xiàn)排版雖然整齊了,但是像“尺碼表”、“詳情說明”這種含有大量文字的圖片,在經(jīng)過 Java 的縮放后,變得非常模糊,且邊緣有嚴(yán)重的鋸齒,根本看不清字。

填坑指南:
Java Graphics2D 默認(rèn)的渲染策略追求速度而不是質(zhì)量。在涉及到縮放(Scale)操作時,必須手動開啟高質(zhì)量的插值算法和抗鋸齒功能。

正解: 在創(chuàng)建畫布后,立馬設(shè)置 RenderingHints

Graphics2D g2d = finalImg.createGraphics();
// 開啟雙線性插值,保證縮放后的圖像清晰度
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
// 開啟抗鋸齒,使文字和圖形邊緣更平滑
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

總結(jié)與終極版源碼

做圖像處理,永遠(yuǎn)不要假設(shè)輸入的數(shù)據(jù)是“理想”的。防 OOM、防亂序、防尺寸不一、保證畫質(zhì),是 Java 圖片合成必須要做的四道防線。

package utils;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class ImageStitcherUtil {

    public static void main(String[] args) {
        String inputDir = "C:\\Users\\lixiewen\\Desktop\\666";
        String outputPath = "C:\\Users\\lixiewen\\Desktop\\666\\666.jpg";

        // 調(diào)用重構(gòu)后的方法:設(shè)置 15 像素縫隙,純白背景
        stitchImages(inputDir, outputPath, 15, Color.WHITE);
    }

    /**
     * 默認(rèn)無縫隙拼接 (兼容老代碼調(diào)用)
     */
    public static void stitchImages(String inputDir, String outputPath) {
        stitchImages(inputDir, outputPath, 0, Color.WHITE);
    }

    /**
     * 將目錄下的圖片序列拼接成一張網(wǎng)格大圖,支持自適應(yīng)不同尺寸的圖片(等比例縮放+居中)
     *
     * @param inputDir     包含圖片序列的目錄路徑
     * @param outputPath   輸出合成大圖的文件路徑
     * @param padding      圖片/格子之間的縫隙大小(像素)
     * @param paddingColor 縫隙及背景的顏色
     */
    public static void stitchImages(String inputDir, String outputPath, int padding, Color paddingColor) {
        File dir = new File(inputDir);
        if (!dir.exists() || !dir.isDirectory()) {
            System.err.println("? 輸入目錄不存在或不是一個目錄: " + inputDir);
            return;
        }

        File outputFile = new File(outputPath);

        // 1. 獲取圖片文件并過濾
        File[] rawFiles = dir.listFiles((d, name) -> {
            String lowerName = name.toLowerCase();
            return (lowerName.endsWith(".jpg") || lowerName.endsWith(".png") || lowerName.endsWith(".jpeg"));
        });

        if (rawFiles == null || rawFiles.length == 0) {
            System.err.println("? 目錄中沒有找到圖片文件");
            return;
        }

        // 2. 過濾并使用【自然數(shù)字排序】 (解決 1.jpg, 10.jpg, 2.jpg 排序錯亂問題)
        List<File> imageFiles = Arrays.stream(rawFiles)
                .filter(file -> !file.getAbsolutePath().equalsIgnoreCase(outputFile.getAbsolutePath()))
                .sorted((f1, f2) -> {
                    String name1 = f1.getName().replaceAll("[^0-9]", "");
                    String name2 = f2.getName().replaceAll("[^0-9]", "");
                    try {
                        if (!name1.isEmpty() && !name2.isEmpty()) {
                            return Integer.compare(Integer.parseInt(name1), Integer.parseInt(name2));
                        }
                    } catch (NumberFormatException ignored) {}
                    return f1.getName().compareTo(f2.getName());
                })
                .collect(Collectors.toList());

        int imageCount = imageFiles.size();
        System.out.println("?? 找到 " + imageCount + " 張有效圖片,準(zhǔn)備拼接...");
        if (imageCount == 0) return;

        try {
            // 3. 讀取第一張圖作為【標(biāo)準(zhǔn)單元格(Cell)】的基準(zhǔn)寬高
            BufferedImage firstImage = ImageIO.read(imageFiles.get(0));
            if (firstImage == null) {
                System.err.println("? 第一張圖片讀取失敗,請檢查文件是否損壞");
                return;
            }
            int cellWidth = firstImage.getWidth();
            int cellHeight = firstImage.getHeight();
            firstImage.flush();

            // 4. 計(jì)算網(wǎng)格排布 (默認(rèn)盡量正方形)
            int cols = (int) Math.ceil(Math.sqrt(imageCount));
            int rows = (int) Math.ceil((double) imageCount / cols);

            // 5. 計(jì)算帶縫隙的總畫布尺寸
            int finalWidth = cols * cellWidth + (cols + 1) * padding;
            int finalHeight = rows * cellHeight + (rows + 1) * padding;

            // 6. 初始化大畫布
            BufferedImage finalImg = new BufferedImage(finalWidth, finalHeight, BufferedImage.TYPE_INT_RGB);
            Graphics2D g2d = finalImg.createGraphics();

            // 開啟抗鋸齒和高質(zhì)量插值渲染(對縮放非常重要,保證縮放后的尺碼表文字依然清晰)
            g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

            // 填充背景底色
            g2d.setColor(paddingColor);
            g2d.fillRect(0, 0, finalWidth, finalHeight);

            // 7. 循環(huán)繪制每一張圖片
            int index = 0;
            for (int row = 0; row < rows; row++) {
                for (int col = 0; col < cols; col++) {
                    if (index >= imageCount) break;

                    BufferedImage img = ImageIO.read(imageFiles.get(index));
                    if (img != null) {
                        // 【核心邏輯】:計(jì)算等比例縮放與居中坐標(biāo)
                        int imgW = img.getWidth();
                        int imgH = img.getHeight();

                        // 計(jì)算縮放比例,取寬高縮放比中較小的一個,確保圖片能完整放入格子內(nèi)
                        double scale = Math.min((double) cellWidth / imgW, (double) cellHeight / imgH);

                        // 計(jì)算實(shí)際繪制的寬高
                        int drawW = (int) (imgW * scale);
                        int drawH = (int) (imgH * scale);

                        // 計(jì)算居中繪制的起始 X 和 Y 坐標(biāo)
                        int cellStartX = padding + col * (cellWidth + padding);
                        int cellStartY = padding + row * (cellHeight + padding);
                        int drawX = cellStartX + (cellWidth - drawW) / 2;
                        int drawY = cellStartY + (cellHeight - drawH) / 2;

                        // 繪制縮放后的圖片
                        g2d.drawImage(img, drawX, drawY, drawW, drawH, null);
                        img.flush();
                    }
                    index++;
                }
            }
            g2d.dispose();

            // 8. 確保持有輸出文件的目錄存在
            if (!outputFile.getParentFile().exists()) {
                outputFile.getParentFile().mkdirs();
            }

            // 9. 動態(tài)獲取輸出格式后綴(避免寫死 jpg)
            String format = "jpg";
            int dotIndex = outputPath.lastIndexOf('.');
            if (dotIndex > 0) {
                format = outputPath.substring(dotIndex + 1);
            }

            // 10. 寫入文件
            ImageIO.write(finalImg, format, outputFile);
            finalImg.flush();

            System.out.println("? 圖片序列拼接完成,輸出至: " + outputPath);

        } catch (IOException e) {
            System.err.println("? 圖片拼接過程中發(fā)生異常: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

希望這篇避坑指南能幫你少掉幾根頭發(fā)。

以上就是使用Java拼接長圖/網(wǎng)格圖的避坑指南的詳細(xì)內(nèi)容,更多關(guān)于Java拼接長圖/網(wǎng)格圖踩坑的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Java實(shí)現(xiàn)輕松提取word和pdf文檔內(nèi)容

    Java實(shí)現(xiàn)輕松提取word和pdf文檔內(nèi)容

    這篇文章主要為大家詳細(xì)介紹了如何使用Java實(shí)現(xiàn)輕松提取word和pdf文檔內(nèi)容,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2025-10-10
  • IDEA?中使用?Hudi的示例代碼

    IDEA?中使用?Hudi的示例代碼

    這篇文章主要介紹了IDEA?中使用?Hudi的相關(guān)知識,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-06-06
  • 詳解Spring整合Quartz實(shí)現(xiàn)動態(tài)定時任務(wù)

    詳解Spring整合Quartz實(shí)現(xiàn)動態(tài)定時任務(wù)

    本篇文章主要介紹了詳解Spring整合Quartz實(shí)現(xiàn)動態(tài)定時任務(wù),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。
    2017-03-03
  • Spring Boot 集成 Kafkad的實(shí)現(xiàn)示例

    Spring Boot 集成 Kafkad的實(shí)現(xiàn)示例

    這篇文章主要介紹了Spring Boot 集成 Kafkad的示例,幫助大家更好的理解和學(xué)習(xí)使用Spring Boot框架,感興趣的朋友可以了解下
    2021-04-04
  • TKmybatis的框架介紹和原理解析

    TKmybatis的框架介紹和原理解析

    tkmybatis是在mybatis框架的基礎(chǔ)上提供了很多工具,讓開發(fā)更加高效,下面來看看這個框架的基本使用,后面會對相關(guān)源碼進(jìn)行分析,感興趣的同學(xué)可以看一下,挺不錯的一個工具
    2020-12-12
  • Java遍歷json字符串取值的實(shí)例

    Java遍歷json字符串取值的實(shí)例

    下面小編就為大家分享一篇Java遍歷json字符串取值的實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-03-03
  • Spring @Order注解使用詳解

    Spring @Order注解使用詳解

    注解@Order或者接口Ordered的作用是定義Spring IOC容器中Bean的執(zhí)行順序的優(yōu)先級,而不是定義Bean的加載順序,Bean的加載順序不受@Order或Ordered接口的影響
    2022-08-08
  • mybatis對象List<String> List<Integer>屬性映射方式

    mybatis對象List<String> List<Integer>屬性映射方式

    這篇文章主要介紹了mybatis對象List<String> List<Integer>屬性映射方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-12-12
  • springboot配置kafka批量消費(fèi),并發(fā)消費(fèi)方式

    springboot配置kafka批量消費(fèi),并發(fā)消費(fèi)方式

    文章介紹了如何在Spring Boot中配置Kafka進(jìn)行批量消費(fèi),并發(fā)消費(fèi),需要注意的是,并發(fā)量必須小于等于分區(qū)數(shù),否則會導(dǎo)致線程空閑,文章還總結(jié)了創(chuàng)建Kafka分區(qū)的命令,并鼓勵讀者分享經(jīng)驗(yàn)
    2024-12-12
  • 關(guān)于Java項(xiàng)目讀取resources資源文件路徑的那點(diǎn)事

    關(guān)于Java項(xiàng)目讀取resources資源文件路徑的那點(diǎn)事

    這篇文章主要介紹了關(guān)于Java項(xiàng)目讀取resources資源文件路徑的那點(diǎn)事,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-07-07

最新評論

石狮市| 阿克| 策勒县| 陆良县| 东乌珠穆沁旗| 阳谷县| 长寿区| 衡阳市| 台南市| 莱州市| 东宁县| 思南县| 东丽区| 阿合奇县| 宜黄县| 怀来县| 阿尔山市| 张家界市| 堆龙德庆县| 卓资县| 上饶县| 鄢陵县| 连州市| 三河市| 孝昌县| 汝州市| 鹤壁市| 浠水县| 兴隆县| 斗六市| 个旧市| 兴宁市| 广水市| 昆明市| 河北省| 巩留县| 东明县| 汶上县| 扎鲁特旗| 银川市| 阳朔县|