png图片拼接和分割

本实验是基于 Linux 下的 qt 环境,采用 opencv 库函数实现。其中题目一和题目二、题目三和题目四、题目五和题目六都是互为关联性的题目。


题目一 图片等份切割

题目要求

将一张png大图片,切割为一个个指定像素的小图块(如 256*256)。

  • 输入:大图片的路径
  • 输出:文件夹路径,文件按照行号/列号命名,比如第一行第一列图块文件名为 image/1/1.png

算法思路

方法一:因为原图片不满足 256256 的倍数,所以需要进行边界填充。将大图转换成四通道图,用第四通道(透明度)来填充边缘无效区域,这样将大图填充成 256256 的倍数大小的图片,再去进行切割。

方法二:边界重叠(边界重合,不推荐)

算法实现

/*
 * @detail 方法一:边界填充
 * @param inputImagePath 输入大图的路径
 *        outputDir 输出分割图片路径
 *        tileSize 分割像素大小
*/
void splitImageIntoTiles(const QString& inputImagePath, const QString& outputDir, int tileSize);


/*
 * @detail 方法二:边界重叠(边界重合,不推荐)
 * @param inputImagePath 输入大图的路径
 *        outputDir 输出分割图片路径
 *        tileSize 分割像素大小
*/
void splitImageIntoTiles_2(const QString& inputImagePath, const QString& outputDir, int tileSize);

题目二 等份图片合并

将上述题目中切割的小像素图片,基于目录命名规则,即按照文件索引,合并成一张大图。

  • 输入:文件夹路径,文件按照行号/列号命名,比如第一行第一列图块文件名为 image/1/1.png
  • 输出:大图片的路径

算法思路

用题目一生成小图的文件索引,基于文件目录把小图合并成大图,合并的大图是我们边界填充过的大图,需要再根据第四通道做边界切割,根据边缘无效区域的第四通道,与有效部分进行区分,从而恢复成原图。

代码实现

/*
 * @detail 用于切割边界,边界条件为第四通道透明度为0
 * @param inputImage 输入待切割图片
 * @return 返回切割边界后的图片
*/
cv::Mat cropRedBorder(const cv::Mat& inputImage);

/*
 * @detail 用于拼接图块,将小图合并成大图
 * @param tilesDir 输入小图片路径
 *        outputDir 输出大图片路径
 *        tileSize 分割小图像素大小
*/
void mergeImageIntoFile(const string& tilesDir, const string& outputDir, int tileSize);

题目三 图片紧密拼凑(二维装箱问题)

题目要求

将一个目录下大小各不相同的小图png格式,合并成若干规定大小的大图(2048*2048)。

  • 输入:文件夹路径 - 包含小图列表
  • 输出:文件夹路径 - 大图列表以及位置索引列表,名字对应(比如:第一张大图包含两个文件1.png, 1.txt)
  • 指标:合成速度(越快越好),大图数量(越少越好)

算法思路

(1)加载与排序

  • 从指定文件夹加载所有小图(PNG 格式),并存储到内存中。
  • 对小图按照高度(或宽度)排序,从大到小排列。这样可以使较大的小图优先填充,有助于减少剩余的空隙,提高空间利用率。

(2)布局(装箱)算法

采用二维装箱算法(Bin Packing),将小图逐行放置到 2048x2048 大图中。

步骤:

  • 设置初始位置 (x=0, y=0) 以及 maxHeightInRow=0 记录当前行的最大高度。
  • 遍历所有小图,判断当前行是否有足够宽度容纳该小图。如果足够,则放置小图并更新 x 坐标。
  • 如果当前行的剩余宽度不足,则换行:x=0, y=y+maxHeightInRow,并重置 maxHeightInRow 为0。
  • 检查是否越过大图底部,如果超过,则保存当前大图并清空,创建一个新的大图进行填充。

换行逻辑:

  • 每当一行的宽度超过2048或已满,换行,将小图放入下一行。
  • 每张小图的高度超过 maxHeightInRow 时,更新 maxHeightInRow,确保每一行有足够的垂直空间。

(3)位置索引生成

  • 在每次成功放置一张小图时,记录该小图的文件名以及在大图中的位置(x, y, width, height)。
  • 每个大图创建一个独立的索引文件,存储该大图内所有小图的文件名和位置,用于后续检索。

(4)保存输出

  • 每个2048x2048的大图保存为一张PNG图片,文件名格式如 0.png、1.png。
  • 索引文件格式如 0.txt、1.txt,每行记录一个小图的 name, x, y, width, height 信息,便于后续根据索引快速找到小图在大图中的位置。

思路优化

上述装填过程中,在每张大图片的右边界、下边界、行之间会存在间隙,优化思路就是用小图片来填充这些间隙,记录这些空隙的空间坐标,然后用小图片来填充这些间隙。

代码实现

class MergeImage {
public:
    MergeImage() {}

    MergeImage(string inPath, string outPath, int width, int height);

    ~MergeImage() {}

    /*
     * @detail 拼凑成大图(排序后二次优化,填充空隙,包括右边界、下边界、行间隙)
    */
    void packImagesWithSort_fix();

    /*
     * @detail 拼凑成大图(排序优化后)
    */
    void packImagesWithSort();

    /*
     * @detail 拼凑成大图(正常顺序)
    */
    void packImagesWithoutSort();

    /*
     * @detail 获取内存占用率
    */
    inline double getImageUsage() {
        if(m_totalSize != 0) {
            return (double)m_used/m_totalSize;
        }
        return 0;
    }

    /*
     * @detail 获取已用内存
    */
    inline int getUsed() { return m_used; }

    /*
     * @detail 获取总内存
    */
    inline int getTotalSize() { return m_totalSize; }

    inline void setOutputPath(string path) { m_outputPath = path; }

private:
    /*
     * @detail 拿出文件目录的图片进行自定义排序
     * @return 返回排序后的图片集
    */
    std::vector<ImageInfo> sortImages();

    /*
     * @detail 用于填充空隙
    */
    void fillGaps(cv::Mat &bigImage, std::vector<ImageInfo>& currentImageInfoList, std::vector<ImageInfo> &smallImages, std::vector<GapInfo> &gaps);

private:
    std::string m_inputPath;        // 输入小图片路径
    std::string m_outputPath;       // 输出路径
    long long m_used;               // 已用内存
    long long m_totalSize;          // 总内存
    int bigWidth;                   // 大照片宽度
    int bigHeight;                  // 大照片高度
};

题目四 图片分割

题目要求

基于png大图和位置索引文件,快速定位并提取小图,将其还原为原始文件夹中的结构,即在上述题目中生成的大图中找到所有的小图。

  • 输入:文件夹路径 - 大图列表以及位置索引列表,名字对应(比如:第一张大图包含两个文件1.png, 1.txt)
  • 输出:文件夹路径 - 包含小图列表
  • 指标:加载速度(越快越好),图片显示速度(越快越好)

算法思路

(1)读取索引文件

  • 遍历所有大图的索引文件,读取其中每张小图的位置信息。
  • 索引文件的格式已按每张大图分开,每行记录一个小图的文件名及其在大图中的位置坐标 (x, y, width, height)。

(2)定位和提取小图

对于每张索引文件中记录的小图信息:

  • 根据索引坐标,从大图中提取对应区域的矩形(即小图所在位置)。
  • 使用 OpenCV 的矩形裁剪功能 cv::Rect(x, y, width, height) 定位小图位置。
  • 裁剪后的区域使用 clone() 方法复制为独立图像,并保存。

(3)保存提取的图像

  • 将提取出的每个小图按原始文件名命名,保存到指定文件夹,实现原图还原。
  • 由于文件名与索引文件中的名称保持一致,因此无需进一步匹配和查找。

代码实现

/*
 * @detail 根据大图和索引信息,拆分出小图
 * @param inputFolder 输入路径
 *        outputFolder 输出路径
*/
void extractImages(const std::string &inputFolder, const std::string &outputFolder);

题目五 构建金字塔图层

题目要求

基于一张png大图,构建为256*256像素整数倍数的金字塔。

  • 输入:large_image.png
  • 输出:tiles文件夹,文件按照行号/列号命名,比如第一级第一行第一列图块文件名为tiles/1/1/1.png
        第1级:1张256*256的小图,即将大图,压缩到这个尺寸
        第2级:4张256*256的小图,组合尺寸512*512
        第3级:16张256*256的小图,组合尺寸1024*1024
        第4级:64张256*256的小图,组合尺寸2048*2048
        第5级:256张256*256的小图,组合尺寸4096*4096
        第6级:1024张256*256的小图,组合尺寸8192*8192
  • 注意:大图需要填充透明区域,到256*2的整数次幂尺寸。

算法思路

resize 缩放到指定层大小后,再按照文件目录索引进行切割和保存。

  • 优化一:采用多线程,每一个线程负责一层的图片处理,即纵向处理。但是这样的缺点是,总耗时≈最后一层处理时间,因为层数越高越耗时,最后一层的线程一定是最晚执行完的,其它层线程早执行完了就等最后一层的线程了,任务分配不充分。

  • 优化二:采用线程池,提前创建线程池,每个线程按每一层的图片块来进行处理,也就是把当前层划分成等量的任务喂给线程池,即横向处理。这样就可以避免任务分配不充分的问题,我这里采用的做法是,把当前图按行分割成不同的任务,喂给线程池。

  • 优化三:从大照片开始分割,尽可能复用上一次计算的结果。

代码实现

/*
 * @detail 方法一:直接生成金字塔图层
 * @param inputFolder 输入大图路径
 *        outputFolder 输出路径
 *        maxLevel 输出层级
*/
void createPyramid(const std::string& inputFolder, const std::string& outputFolder, int maxLevel = 6);


/*
 * @detail 方法二:多线程生成各层金字塔图层
 * @param inputFolder 输入大图路径
 *        outputFolder 输出路径
 *        maxLevel 输出层级
*/
void createPyramid_multiThread(const std::string& inputFolder, const std::string& outputFolder, int maxLevel = 6);

/*
 * @detail 方法三:线程池生成各层金字塔图层
 * @param inputFolder 输入大图路径
 *        outputFolder 输出路径
 *        maxLevel 输出层级
*/
void saveTiles_threadPool(const cv::Mat& resizedImage, int level, const std::string& outputFolder, int startRow, int endRow)

/*
 * @detail 方法四:复用上一层结果,生成金字塔图层(像素会失真)
 * @param inputFolder 输入大图路径
 *        outputFolder 输出路径
 *        maxLevel 输出层级
*/
void createPyramid_reuse(const std::string& inputFolder, const std::string& outputFolder, int maxLevel = 6);

题目六 局部性查看图片

题目要求

基于题目五生成的png图片金字塔,进行分级显示。

  • 输入:tiles文件夹,文件按照行号/列号命名,比如第一级第一行第一列图块文件名为tiles/1/1/1.png
  • 输出:显示分级列表,切换对应级别,在窗口中显示对应层级的合成大图
  • 注意:大图需要填充透明区域,到256*2的整数次幂尺寸。

算法思路

我们要查看一张很大的图时,屏幕分辨率甚至远远不够图片大小,那么我们要查看这张大图的某一部分时,如果采用先全部加载出来图片,再进行局部性查看,那么就会很耗时。所以我们需要利用局部性原理,通过使用题目五中生成的金字塔图层,利用文件索引,每次查看只需要加载所查看坐标周围附近的点,进行局部性展示,这样就会加速图片的加载速度。

代码实现

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(int x, int y, int width, int height, QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_pushButton_clicked();

    void on_pushButton_2_clicked();

    void on_pushButton_3_clicked();

    void on_pushButton_4_clicked();

    void on_pushButton_5_clicked();

    void on_pushButton_6_clicked();

private:
    /*
     * @detail 方法一:先合并成完整大图,再做偏移,时间损耗大(不推荐)
     * @param inputFolder 输入路径
     *        level 展示层级
    */
    void plotOffsetImage(const std::string& inputFolder, int level);

    /*
     * @detail 方法二:利用局部性原理,找屏幕尺寸大小对应的拼接子图(推荐)
     * @param inputFolder 输入路径
     * @return 返回局部性合成图片
    */
    cv::Mat loadLevelImages(const std::string& inputFolder);

    /*
     * @detail 方法二得到图片后进行显示
     * @param midImage 输入局部性合成图片
    */
    void plotLevelImage(cv::Mat& midImage);

private:
    Ui::MainWindow *ui;
    int pos_x;          // 目标中心点x坐标
    int pos_y;          // 目标中心点y坐标
    int windowWidth;    // 显示屏幕尺寸
    int windowHeight;
    int baseSize;       // 小图分辨率256*256
    int m_level;        // 层级
    std::string m_path; // 小图路径
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值