本文长度为1959字,预计阅读5分钟
前言
这几个月一直在做Android的东西,OpenCV的Demo基本没做,正好前两天也刚下载了VS2022,正好借助新的VS2022做个简单的OpenCV图像切割成九宫格的Demo。
实现效果
看上图的最右边的,就是切分成9个图的效果,看过我的《趣玩算法--OpenCV华容道AI自动解题》老朋友应该都知道我要干什么了。没错,做这个嘛是为了再做一个拼图的小游戏,右边每个图像上都用PutText打印出了图像对应的区域数字,现在是为了标识作用的。
实现思路
# | 思路 |
---|---|
1 | 加载图像后用Resize将图像缩放成正方形大小 |
2 | 按图像起始位置开始,计算每个截取区域的图像大小 |
3 | 将截取的区域存入到Vector的容器中,存放的过程中随机排序 |
4 | 生成一个新的画布,遍历容器将每个图像显示出来 |
核心代码讲解
微卡智享
01
关于分割的图像容器
最开始想使用map的方式,后来觉得不太好,就创建了一个结构,就是分割后的图像原来的序号位置,图像Mat,还有一个是现在的位置三个属性。
而生成分割后的图像容器用了一个SplitMats的函数来实现。
代码语言:javascript复制std::vector<CutMat*> MatSet::SplitMats(cv::Mat& img, int cols, int rows)
{
std::vector<CutMat*> matvts;
if (cols == 0 || rows == 0)
{
std::cout << "行数和列数不能为0" << std::endl;
return matvts;
}
matvts.resize(cols * rows);
//计算平均分的格数的width和height
int width = img.cols / cols;
int height = img.rows / rows;
//生成序号列表
std::vector<int> nums = GetVtsPos(cols, rows);
//根据输入的行和列划分开矩形
for (int row = 0; row < rows; row ) {
for (int col = 0; col < cols; col ) {
//计算当前矩形的起始X和Y坐标
int x = col * width;
if (x > 0) x ;
int y = row * height;
if (y > 0) y ;
//计算截取矩形的宽和高,加入控制不能超过源图像的边界
int rwidth = width;
if (x rwidth > img.cols) rwidth = img.cols - x;
int rheight = height;
if (y rheight > img.rows) rheight = img.rows - y;
//生成截取的矩形并截取图像存放到map中
cv::Rect rect = cv::Rect(x, y, rwidth, rheight);
cv::Mat matrect = img(rect);
//截取后的图像需要判断是否宽高一致,不一致时缩放为一样大,用于在一张图像显示
if (rwidth != width || rheight != height) {
cv::resize(matrect, matrect, cv::Size(width, height));
}
//当前Mat的序号
int pos = row * rows col 1;
CutMat* tmpcurmat = new CutMat(pos, matrect);
//随机指定的新位置
tmpcurmat->curPosition = GetRandNum(nums);
//根据随机排序后的位置插入到容器中
matvts[tmpcurmat->curPosition - 1] = tmpcurmat;
}
}
return matvts;
}
载取的过程中要注意的是,根据图像大小平均分割后,最后的矩形长度要判断是否超出图像边缘了,如果超出后,需要长度设为到图像边缘的长度,然后再通过Resize来实现设置图像相同大小。
02
关于图像打乱顺序的解决
前面定义的结构里面,通过生成随机位置赋值给了curPosition属性,考虑到显示出来要按照curPosition属性顺序显示,传统的方法就是两个思路:
- 使用Map存储,Key为curPosition,遍历时查找Key找到对应的Map,时间复杂度为O1,用Map空间换时间。
- 返回的容器,重新按照curPosition排序,时间复杂度On。
因为我们容器只有9个,所以用上面两个基本的速度也可以忽略,不过即然在生成的过程中已经赋值随机数了,所以当时也直接指定存放位置也可以,完全不需要用上面两种方案。
通过传入的行和列数字,直接设置容器的个数。
根据生成的指定位置,直接修改容器的下标值。
整个项目中新建了一个MatSet的类,绘制和生成图像都在这里实现的,main.cpp就是加载图像和外部调用。
完整代码
MatSet.h
代码语言:javascript复制#pragma once
#include <opencv2/opencv.hpp>
#include <iostream>
struct CutMat {
public:
//图像应存放位置
int Position;
//图像当前所在位置
int curPosition;
//图像数据
cv::Mat mat;
CutMat(int _pos, cv::Mat _mat) : Position(_pos), curPosition(_pos), mat(_mat)
{
}
};
class MatSet
{
public:
//生成分割后的图像容器
static std::vector<CutMat*> SplitMats(cv::Mat& img, int cols = 3, int rows = 3);
//显示拼图游戏图像
static void DrawPuzzleMat(std::vector<CutMat*>& cutmats, std::vector<std::vector<cv::Point>>& contours, bool iscontours = false, int cols = 3);
private:
//根据行和列生成对应的位置序号容器
static std::vector<int> GetVtsPos(int cols = 3, int rows = 3);
//获取当前随机序号
static int GetRandNum(std::vector<int>& nums);
//插入轮廓
static void InsertContours(std::vector<std::vector<cv::Point>>& contours, cv::Rect rect);
};
MatSet.cpp
代码语言:javascript复制#include "MatSet.h"
std::vector<CutMat*> MatSet::SplitMats(cv::Mat& img, int cols, int rows)
{
std::vector<CutMat*> matvts;
if (cols == 0 || rows == 0)
{
std::cout << "行数和列数不能为0" << std::endl;
return matvts;
}
//根据行和列直接设置容器的个数
matvts.resize(cols * rows);
//计算平均分的格数的width和height
int width = img.cols / cols;
int height = img.rows / rows;
//生成序号列表
std::vector<int> nums = GetVtsPos(cols, rows);
//根据输入的行和列划分开矩形
for (int row = 0; row < rows; row ) {
for (int col = 0; col < cols; col ) {
//计算当前矩形的起始X和Y坐标
int x = col * width;
if (x > 0) x ;
int y = row * height;
if (y > 0) y ;
//计算截取矩形的宽和高,加入控制不能超过源图像的边界
int rwidth = width;
if (x rwidth > img.cols) rwidth = img.cols - x;
int rheight = height;
if (y rheight > img.rows) rheight = img.rows - y;
//生成截取的矩形并截取图像存放到map中
cv::Rect rect = cv::Rect(x, y, rwidth, rheight);
cv::Mat matrect = img(rect);
//截取后的图像需要判断是否宽高一致,不一致时缩放为一样大,用于在一张图像显示
if (rwidth != width || rheight != height) {
cv::resize(matrect, matrect, cv::Size(width, height));
}
//当前Mat的序号
int pos = row * rows col 1;
CutMat* tmpcurmat = new CutMat(pos, matrect);
//随机指定的新位置
tmpcurmat->curPosition = GetRandNum(nums);
//根据随机排序后的位置插入到容器中
matvts[tmpcurmat->curPosition - 1] = tmpcurmat;
}
}
return matvts;
}
void MatSet::DrawPuzzleMat(std::vector<CutMat*>& cutmats, std::vector<std::vector<cv::Point>>& contours,
bool iscontours, int cols)
{
if (cutmats.empty()) return;
int starttop = 20;
int startleft = 20;
int rectwidth = cutmats[0]->mat.cols;
int rectheight = cutmats[0]->mat.rows;
int nums = cutmats.size();
//创建图像
cv::Mat src = cv::Mat(cv::Size(600, 700), CV_8UC3, cv::Scalar(240, 240, 240));
for (int i = 0; i < nums; i) {
//计算当前顺序为二维数组的几行几列
int row = i / cols;
int col = i % cols;
//生成对应的矩形框
cv::Rect rect = cv::Rect(startleft col * rectwidth, starttop row * rectheight, rectwidth, rectheight);
//替换对应位置的图像
cutmats[i]->mat.copyTo(src(rect));
//轮廓插入
if (iscontours) InsertContours(contours, rect);
}
imshow("puzzle", src);
}
//生成序号容器
std::vector<int> MatSet::GetVtsPos(int cols, int rows)
{
std::vector<int> nums;
int total = cols * rows;
if (total > 0) {
for (int i = 1; i <= total; i) {
nums.push_back(i);
}
}
return nums;
}
int MatSet::GetRandNum(std::vector<int>& nums)
{
//初始化随机数种子
srand((int)time(0));
//右下角的最后一个赋值不对,所以取余数时不计算在内
int index = nums.size() == 1 ? 0 : rand() % (nums.size() - 1);
//获取到返回值
int resint = nums[index];
//容器中删除已经赋值的数字
nums.erase(nums.begin() index);
return resint;
}
void MatSet::InsertContours(std::vector<std::vector<cv::Point>>& contours, cv::Rect rect)
{
std::vector<cv::Point> vetpt;
cv::Point pt1 = cv::Point(rect.x, rect.y);
vetpt.push_back(pt1);
cv::Point pt2 = cv::Point(rect.x rect.width, rect.y);
vetpt.push_back(pt2);
cv::Point pt3 = cv::Point(rect.x rect.width, rect.y rect.height);
vetpt.push_back(pt3);
cv::Point pt4 = cv::Point(rect.x, rect.y rect.height);
vetpt.push_back(pt4);
contours.push_back(vetpt);
}
main.cpp
代码语言:javascript复制#pragma once
#include <opencv2/opencv.hpp>
#include <iostream>
#include "MatSet.h"
using namespace std;
using namespace cv;
//定义轮廓区域
vector<vector<Point>> contours;
int main(int argc, char** argv) {
try
{
Mat src = imread("E:/DCIM/test8.jpg");
if (src.empty()) {
cout << "图像加载失败。。。。" << endl;
waitKey(0);
return -1;
}
//设置图像缩放到500*500
Mat tmpsrc;
resize(src, tmpsrc, Size(500, 500));
imshow("src", src);
imshow("tmpsrc", tmpsrc);
//获取图像分割后的集合
vector<CutMat*> vtsmat = MatSet::SplitMats(tmpsrc);
//将图像分割后的集合对应的序号列出来
for (int i = 0; i < vtsmat.size(); i) {
Mat tmpmat = vtsmat[i]->mat;
string title = to_string(vtsmat[i]->Position);
putText(tmpmat, title, Point(tmpmat.cols/2, tmpmat.rows/2), 2, 2, Scalar(0, 0, 255));
}
//绘制图像
MatSet::DrawPuzzleMat(vtsmat, contours, true);
cv::waitKey(0);
return 0;
}
catch (const std::exception& ex)
{
cout << ex.what() << endl;
cv::waitKey(0);
return -1;
}
}
整个Demo用的VS2022和OpenCV4.5.4做的,用VS2022的C 里,智能提示感觉和VS2019差不多,并不像我上篇说的和C#中一样强大。另一个问题就是用了OpenCV4.5.4后,运行过程中控制台多了一些加载错误的输出,虽然并不影响运行,不过看着不舒服。图如下:
如果有知道怎么解决的小伙伴麻烦留言告之一下,万分谢谢。
完