C++ OpenCV生成九宫格图像

学更好的别人,

做更好的自己。

——《微卡智享》

f22be165a7eefcd37690bf2815326e52.png

本文长度为1959,预计阅读5分钟

前言

这几个月一直在做Android的东西,OpenCV的Demo基本没做,正好前两天也刚下载了VS2022,正好借助新的VS2022做个简单的OpenCV图像切割成九宫格的Demo。

71831b15494950e622dacb871777888b.png

实现效果

25945edd35612f17982eea009808687a.png

看上图的最右边的,就是切分成9个图的效果,看过我的《趣玩算法--OpenCV华容道AI自动解题》老朋友应该都知道我要干什么了。没错,做这个嘛是为了再做一个拼图的小游戏,右边每个图像上都用PutText打印出了图像对应的区域数字,现在是为了标识作用的。

实现思路

#思路
1加载图像后用Resize将图像缩放成正方形大小
2按图像起始位置开始,计算每个截取区域的图像大小
3将截取的区域存入到Vector的容器中,存放的过程中随机排序
4生成一个新的画布,遍历容器将每个图像显示出来

核心代码讲解

5855e4b6d2b77a0135d87da37c5fd807.png

微卡智享

01

关于分割的图像容器

最开始想使用map的方式,后来觉得不太好,就创建了一个结构,就是分割后的图像原来的序号位置,图像Mat,还有一个是现在的位置三个属性。

e84f094e5e1dfbc3f9515fa2cde5d42b.png

而生成分割后的图像容器用了一个SplitMats的函数来实现。

fc6bbece99a096777ad0470120bfc7e3.png

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来实现设置图像相同大小。

73ab4758566edffe034062cb9d9dfbdd.png

02

关于图像打乱顺序的解决

前面定义的结构里面,通过生成随机位置赋值给了curPosition属性,考虑到显示出来要按照curPosition属性顺序显示,传统的方法就是两个思路:

  1. 使用Map存储,Key为curPosition,遍历时查找Key找到对应的Map,时间复杂度为O1,用Map空间换时间。

  2. 返回的容器,重新按照curPosition排序,时间复杂度On。

因为我们容器只有9个,所以用上面两个基本的速度也可以忽略,不过即然在生成的过程中已经赋值随机数了,所以当时也直接指定存放位置也可以,完全不需要用上面两种方案。

32a4e6cd51f904c096f869a32f20c9c8.png

通过传入的行和列数字,直接设置容器的个数。

f6f74be1f120336d373806884d4a5b44.png

根据生成的指定位置,直接修改容器的下标值。

cd7ecd8293b668223006e0bb4c9f9277.png

ced31ecdc60371114d0f0746a3051879.png

整个项目中新建了一个MatSet的类,绘制和生成图像都在这里实现的,main.cpp就是加载图像和外部调用。

完整代码

MatSet.h

#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

#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

#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;
  }
}

e463aaf39df49516ffdf9a360309df0e.png

整个Demo用的VS2022和OpenCV4.5.4做的,用VS2022的C++里,智能提示感觉和VS2019差不多,并不像我上篇说的和C#中一样强大。另一个问题就是用了OpenCV4.5.4后,运行过程中控制台多了一些加载错误的输出,虽然并不影响运行,不过看着不舒服。图如下:

4bb38e7e6c4c97c11f1431a486e8cc1b.png

如果有知道怎么解决的小伙伴麻烦留言告之一下,万分谢谢。

d3f3f44fe08cfd5ea8c83f83970cab37.png

扫描二维码

获取更多精彩

微卡智享

aac1a48b9264e1a4dce17f7d63fe083d.png

「 往期文章 」

制作一个Android Sqlite远程运维小工具

VS2022 MAUI Hello World——Windows平台及Android平台效果

Android BaseQuickAdapter3.0.4版本二级列表的使用及遇到的问题

 

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vaccae

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值