本实验是基于 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; // 小图路径
};