ElmaCote07 发表于 2023-3-21 22:23

C++操作图像、图片

今天想说的是如何用C++语言操作图片(其实案例代码是用C++写的,如果想用别的语言操作图片,看完本片就会了)。更准确的说是如何从图片文件本身去操作,而不受限于用什么语言。可能这句话有很多人不是很理解,下面我将仔细阐述一下。
就拿我第一各带图形的程序来说吧。例如下面这个五子棋游戏,下面这张图就是这个项目的主界面。


当时,我是用的C语言图形库叫Easxy(感兴趣的可以了解一下)。里面有个loadimage函数,是专门读取jpg格式的图片文件。我当时费劲半天找到这个函数,以为自己会操作图片了。结果没过多久,我又用C++ Win32想写个带图形的程序,结果费牛劲找到了GDI、GDI+两个图像库,里面分别有LoadBitmap函数和Image对象是处理图形的,才解决了问题。后来也用其他编程语言都写过带图形的程序,反正都很费劲才解决我想要的问题。
对本人而言,更倾向于底层造轮子的。所以,后来专门对图片进行研究了一番,才知道我对图片理解错了。我们在编程时操作图片时,不要依赖于程序语言的提供的函数接口,而更应该是从图片文件本身来处理问题。哪些各种编程语言之所以提供这些接口,更多是提高开发效率,而且大多数程序员也不关注图片具体原理。目前我们熟悉的图片格式有BMP(又称位图,计算机原始图,未经压缩),JPG(JPEG编码),JPGE(JPEG),PNG(没有具体了解,感兴趣的可以了解一下)等。所以我们要想操作图片,我们必须了解这些格式的图片是怎么存储的(编码),这样我们才能对图片进行解析。
下面BMP格式的图片进行举例说明,分别对缩放,裁剪,灰度等(三个进行说明)。因为BMP是最容易处理的,知其一,后续任何格式都知道怎么操作了(就是麻烦点而已)。

一问:如何读取、存储BMP图片文件?
首先,我们得知道BMP的编码格式,不知道到去网上查。BMP是包含三个部分(文件头,图片信息头,调色板数据[可选],颜色数据)。调色板数据,只有位深(位深,简单来说一个像素点的大小,比如8位的图就是灰度图;24位的图片,一般是RGB彩色图;32位的图片就是RGBA带透明度的彩色图)为8位的图片才有。知道这些后,我设计出数据结构,然后按照二进制方式读取、写入文件即可。
//灰度类型
enum GrayType
    {
      GT_NIL,   //无类型
      GT_AVE,   //平均值灰度
      GT_STD,   //标准值灰度
      GT_BYTE8    //8字节灰度(24和32才有意义)
    };

//图片数据结构
class PictureData
    {
    public:
      PictureFormat m_enumType;       //类型
      unsigned int m_unHeight;      //高度
      unsigned int m_unWidth;         //宽度
      unsigned long m_ulPixelLen;   //像素长度
      unsigned char *m_ptuchPixel;    //像素数据
      unsigned short m_unPixelBit;    //像素位数
    public:
      bool m_bIsGray;               //是否灰度处理过
    public:
      //构造 赋值 拷贝 析构
      PictureData(void);
      PictureData& operator =(const PictureData& obj);
      PictureData(const PictureData& obj);
      virtual ~PictureData(void);
    };

下面是读取代码:
//byteData 图片文件的二进制数据
//byteLen 字节长度
//PictureData 保存图片信息的数据结构
//erroInfo 错误信息
//这里之所以这样传参,是因为本人进行封装了的,拆分一下应该不难,如果觉得有困难,那说明你还得继续学习啊
bool Picture::LoadBMP(const unsigned char *byteData, unsigned long byteLen, PictureData &pictureData, string* errorInfo)
    {
      //偏移量
      unsigned long offset = 0;
      char szLog = { 0 };

      //格式
      pictureData.m_enumType = PictureFormat::PF_BMP;

      //位图头部结构(2字节对齐)
#pragma pack(push, 2)
      struct S_BMP_FILE_HEADER {
            unsigned short bfType;
            unsigned long bfSize;
            unsigned short bfReserved1;
            unsigned short bfReserved2;
            unsigned long bfOffBits;
      };
#pragma pack(pop)

      //位图信息结构
      struct S_BMP_INFO_HEADER {
            unsigned long biSize;
            long biWidth;
            long biHeight;
            unsigned short biPlanes;
            unsigned short biBitCount;
            unsigned long biCompression;
            unsigned long biSizeImage;
            long biXPelsPerMeter;
            long biYPelsPerMeter;
            unsigned long biClrUsed;
            unsigned long biClrImportant;
      };

      //读取文件头
      S_BMP_FILE_HEADER fileHeader;
      ::memset(&fileHeader, 0, sizeof(S_BMP_FILE_HEADER));
      ::memcpy(&fileHeader, byteData + offset, sizeof(S_BMP_FILE_HEADER));
      offset += sizeof(S_BMP_FILE_HEADER);

      //读取信息头
      S_BMP_INFO_HEADER infoHeader;
      ::memset(&infoHeader, 0, sizeof(S_BMP_INFO_HEADER));
      ::memcpy(&infoHeader, byteData + offset, sizeof(S_BMP_INFO_HEADER));
      offset += sizeof(S_BMP_INFO_HEADER);
      pictureData.m_unWidth = infoHeader.biWidth;
      pictureData.m_unHeight = infoHeader.biHeight;
      pictureData.m_unPixelBit = infoHeader.biBitCount;

      //判断位深
      if (infoHeader.biBitCount != 8 && infoHeader.biBitCount != 24 && infoHeader.biBitCount != 32) {
            sprintf(szLog, "Picture - LoadBMP - Format Error %d Bit! Only Supports 8-Bit, 24-Bit and 32-Bit!", pictureData.m_unPixelBit);
            *errorInfo = szLog;
            return false;
      }

      //判断尺寸大小
      if (infoHeader.biWidth == 0 || infoHeader.biHeight == 0) {
            sprintf(szLog, "Picture - LoadBMP - Size Error! Width Is Zero Or Height Is Zero!");
            *errorInfo = szLog;
            return false;
      }

      //判断是否是灰度图(灰度需要读取颜色表4 * 256)
      unsigned char* ptColorTable = new unsigned char;
      if (8 == infoHeader.biBitCount) {
            ::memcpy(ptColorTable, byteData + offset, 4 * 256);
            offset += (4 * 256);
      }
      delete[]ptColorTable;
      ptColorTable = nullptr;

      //读取颜色数据
      unsigned int lineBytes = ((infoHeader.biWidth * (infoHeader.biBitCount / 8) + 3) >> 2) << 2;
      pictureData.m_ulPixelLen = lineBytes * infoHeader.biHeight;
      pictureData.m_ptuchPixel = new unsigned char;
      ::memcpy(pictureData.m_ptuchPixel, byteData + offset, pictureData.m_ulPixelLen);

      return true;
    }
下面是存储代码:
//byteData 图片文件的二进制数据
//byteLen 字节长度
//PictureData 保存图片信息的数据结构
//erroInfo 错误信息
//这里之所以这样传参,是因为本人进行封装了的,拆分一下应该不难,如果觉得有困难,那说明你还得继续学习啊
bool Picture::SaveBMP(unsigned char* &byteData, unsigned long &byteLen, const PictureData &pictureData, string* errorInfo)
    {
      //偏移量
      unsigned long offset = 0;
      char szLog = { 0 };

      //位图头部结构(2字节对齐)
#pragma pack(push, 2)
      struct S_BMP_FILE_HEADER {
            unsigned short bfType;
            unsigned long bfSize;
            unsigned short bfReserved1;
            unsigned short bfReserved2;
            unsigned long bfOffBits;
      };
#pragma pack(pop)

      //位图信息结构
      struct S_BMP_INFO_HEADER {
            unsigned long biSize;
            long biWidth;
            long biHeight;
            unsigned short biPlanes;
            unsigned short biBitCount;
            unsigned long biCompression;
            unsigned long biSizeImage;
            long biXPelsPerMeter;
            long biYPelsPerMeter;
            unsigned long biClrUsed;
            unsigned long biClrImportant;
      };

      byteLen = sizeof(S_BMP_FILE_HEADER) + sizeof(S_BMP_INFO_HEADER) + (8 == pictureData.m_unPixelBit ? (4 * 256) : 0) + pictureData.m_ulPixelLen;
      byteData = new unsigned char;

      unsigned short depthBytes = pictureData.m_unPixelBit / 8;
      unsigned int lineBytes = ((pictureData.m_unWidth * depthBytes + 3) >> 2) << 2;
      unsigned int colorBytes = lineBytes * pictureData.m_unHeight;
      unsigned long headerSize = sizeof(S_BMP_FILE_HEADER) + sizeof(S_BMP_INFO_HEADER);
      unsigned long fileSize = headerSize + colorBytes;

      //写入文件头
      S_BMP_FILE_HEADER fileHeader;
      ::memset(&fileHeader, 0, sizeof(S_BMP_FILE_HEADER));
      fileHeader.bfType = 0x4D42;
      fileHeader.bfSize = fileSize;
      fileHeader.bfOffBits = headerSize;
      ::memcpy(byteData + offset, &fileHeader, sizeof(S_BMP_FILE_HEADER));
      offset += sizeof(S_BMP_FILE_HEADER);

      //写入信息头
      S_BMP_INFO_HEADER infoHeader;
      ::memset(&infoHeader, 0, sizeof(S_BMP_INFO_HEADER));
      infoHeader.biSize = 40;
      infoHeader.biWidth = pictureData.m_unWidth;
      infoHeader.biHeight = pictureData.m_unHeight;
      infoHeader.biPlanes = 1;
      infoHeader.biBitCount = pictureData.m_unPixelBit;
      infoHeader.biSizeImage = colorBytes;
      ::memcpy(byteData + offset, &infoHeader, sizeof(S_BMP_INFO_HEADER));
      offset += sizeof(S_BMP_INFO_HEADER);

      //判断是否为灰度图(灰度图需要写入颜色表4 * 256)
      if (8 == pictureData.m_unPixelBit) {
            int count = 4 * 256;
            unsigned char *ptColorTable = new unsigned char;
            unsigned char j = 0;
            for (int i = 0; i < count; i += 4, j++) {
                ptColorTable = j;
                ptColorTable = j;
                ptColorTable = j;
                ptColorTable = 0;
            }
            ::memcpy(byteData + offset, ptColorTable, count);
            offset += count;

            delete[]ptColorTable;
            ptColorTable = nullptr;
      }

      //写入颜色数据
      ::memcpy(byteData + offset, pictureData.m_ptuchPixel, pictureData.m_ulPixelLen);

      return true;
    }
二问:如何操作BMP图片文件?
我们读取BMP文件之后就已经拿到这个图片基本信息了,比如长度、宽度、深度、颜色数据等。接下来操作无非是对这些数据进行操作而已。
裁剪操作:(具体怎么做不是很难,可以自己对着代码看,文字不好描述)
//x 横向偏移坐标(就是从哪里开始裁剪)
//y 纵向偏移坐标(就是从哪里开始裁剪)
//w 裁剪宽度
//h 裁剪高度
//PictureData 保存图片信息的数据结构
//erroInfo 错误信息
//这里之所以这样传参,是因为本人进行封装了的,拆分一下应该不难,如果觉得有困难,那说明你还得继续学习啊
bool PictureOperate::Crop(unsigned int x, unsigned int y, unsigned int w, unsigned int h, PictureData &pictureData, string* errorInfo)
    {
      char szLog = { 0 };
      if (x + w > pictureData.m_unWidth || y + h > pictureData.m_unHeight || x < 0 || y < 0 || w <= 0 || h <= 0) {
            sprintf(szLog, "PictureOperate - Crop - Crop Size Error!");
            *errorInfo = szLog;
            return false;
      }

      unsigned int depthBytes = pictureData.m_unPixelBit / 8;
      unsigned int lineBytes = ((pictureData.m_unWidth * depthBytes + 3) >> 2) << 2;

      unsigned int dstLineBytes = ((w * depthBytes + 3) >> 2) << 2;
      unsigned long dstPixelLen = dstLineBytes * h;
      unsigned char *dstColorData = new unsigned char;

      for (unsigned int i = 0; i < h; i++) {
            memcpy(dstColorData + i * dstLineBytes, pictureData.m_ptuchPixel + (i + pictureData.m_unHeight - h - y) * lineBytes + x * depthBytes, dstLineBytes);
      }
      delete[] pictureData.m_ptuchPixel;
      pictureData.m_ptuchPixel = dstColorData;
      pictureData.m_ulPixelLen = dstPixelLen;
      pictureData.m_unHeight = h;
      pictureData.m_unWidth = w;

      return true;
    }
缩放操作:(这个计算稍微有点麻烦,处理不好,容易溢出。但是也别害怕,方法总比困难多。这里用到了个双线插值法(这里不做解释,不知道的去百度))
//factorX 横向缩放(0-1)
//factorY 纵向缩放(0-1)
//PictureData 保存图片信息的数据结构
//erroInfo 错误信息
//这里之所以这样传参,是因为本人进行封装了的,拆分一下应该不难,如果觉得有困难,那说明你还得继续学习啊
bool PictureOperate::Zoom(double factorX, double factorY, PictureData &pictureData, string* errorInfo)
    {
      char szLog = { 0 };
      //判断位深
      if (pictureData.m_unPixelBit != 8 && pictureData.m_unPixelBit != 24 && pictureData.m_unPixelBit != 32) {
            sprintf(szLog, "PictureOperate - Zoom - Format Error %d Bit! Only Supports 8-Bit, 24-Bit and 32-Bit!", pictureData.m_unPixelBit);
            *errorInfo = szLog;
            return false;
      }

      //判断缩放因子
      if (factorX <= 0 || factorY <= 0) {
            sprintf(szLog, "PictureOperate - Zoom - Factor Error! Cannot Be Negative!");
            *errorInfo = szLog;
            return false;
      }

      double factorReciprocalX = 1 / factorX;
      double factorReciprocalY = 1 / factorY;
      unsigned int dstWidth = unsigned int(pictureData.m_unWidth * factorX);
      unsigned int dstHeight = unsigned int(pictureData.m_unHeight * factorY);

      unsigned int depthBytes = pictureData.m_unPixelBit / 8;
      unsigned int dstLineBytes = ((dstWidth * depthBytes + 3) >> 2) << 2;
      unsigned char *dstBytes = new unsigned char;
      unsigned int lineBytes = ((pictureData.m_unWidth * depthBytes + 3) >> 2) << 2;

      for (unsigned int h = 0; h < dstHeight; h++) {
            for (unsigned int w = 0; w < dstWidth; w++) {
                //原图像的真实位置
                double srcRealX = (w + 0.5) * factorReciprocalX - 0.5;
                double srcRealY = (h + 0.5) * factorReciprocalY - 0.5;

                //原图像对应的像素点位置
                int srcX = (int)srcRealX;
                int srcY = (int)srcRealY;

                //原图像位置偏移量
                double offsetX = srcRealX - srcX;
                double offsetY = srcRealY - srcY;

                //原图像位置的临近4个点
                int leftUp = srcY * lineBytes + srcX * depthBytes;
                int rightUp = srcY * lineBytes + (srcX + 1) * depthBytes;
                int leftDown = (srcY + 1) * lineBytes + srcX * depthBytes;
                int rightDown = (srcY + 1) * lineBytes + (srcX + 1) * depthBytes;

                if (srcY + 1 == dstHeight - 1) {
                  leftDown = leftUp;
                  rightDown = rightUp;
                }
                if (srcX + 1 == dstWidth - 1) {
                  rightUp = leftUp;
                  rightDown = leftDown;
                }

                //目的图像像素位置索引
                int index = h * dstLineBytes + w * depthBytes;
                for (unsigned int i = 0; i < depthBytes; i++) {
                  double part1 = pictureData.m_ptuchPixel * (1 - offsetX) * (1 - offsetY);
                  double part2 = pictureData.m_ptuchPixel * offsetX * (1 - offsetY);
                  double part3 = pictureData.m_ptuchPixel * offsetY * (1 - offsetX);
                  double part4 = pictureData.m_ptuchPixel * offsetY * offsetX;

                  dstBytes = unsigned char(part1 + part2 + part3 + part4);
                }
            }
      }

      delete[]pictureData.m_ptuchPixel;
      pictureData.m_ptuchPixel = dstBytes;
      pictureData.m_ulPixelLen = dstLineBytes * dstHeight;
      pictureData.m_unWidth = dstWidth;
      pictureData.m_unHeight = dstHeight;

      return true;
    }
灰度操作:(这个有多种方式,比如单通道法(就是只有保留一个通道的颜色),平均值法(所有颜色通道值都一样)。本人给的样例是平均值法)
//gt 灰度类型
//PictureData 保存图片信息的数据结构
//erroInfo 错误信息
//这里之所以这样传参,是因为本人进行封装了的,拆分一下应该不难,如果觉得有困难,那说明你还得继续学习啊
bool PictureOperate::Gray(GrayType gt, PictureData &pictureData, string* errorInfo)
    {
      char szLog = { 0 };
      if (gt != GT_AVE && gt != GT_STD && gt != GT_BYTE8) {
            sprintf(szLog, "PictureOperate - Gray - Type Error! Value: %d", gt);
            *errorInfo = szLog;
            return false;
      }

      if (GT_BYTE8 == gt) {
            unsigned int depthBytes = pictureData.m_unPixelBit / 8;
            unsigned int lineBytes = ((pictureData.m_unWidth * depthBytes + 3) >> 2) << 2;

            unsigned int newLineBytes = ((pictureData.m_unWidth + 3) >> 2) << 2;
            unsigned char* colorData = new unsigned char;
            for (unsigned int h = 0; h < pictureData.m_unHeight; h++) {
                for (unsigned int w = 0; w < pictureData.m_unWidth; w++) {
                  int index = h * lineBytes + w * depthBytes;
                  unsigned int value = 0;
                  for (int i = 0; i < 3; i++) {
                        value += unsigned int(pictureData.m_ptuchPixel);
                  }
                  value /= 3;

                  int newIndex = h * pictureData.m_unWidth + w;
                  colorData = value;
                }
            }

            delete[] pictureData.m_ptuchPixel;
            pictureData.m_ptuchPixel = colorData;
            pictureData.m_ulPixelLen = newLineBytes * pictureData.m_unHeight;
            pictureData.m_unPixelBit = 8;
      }
      else {
            unsigned int depthBytes = pictureData.m_unPixelBit / 8;
            unsigned int lineBytes = ((pictureData.m_unWidth * depthBytes + 3) >> 2) << 2;

            for (unsigned int h = 0; h < pictureData.m_unHeight; h++) {
                for (unsigned int w = 0; w < pictureData.m_unWidth; w++) {

                  int index = h * lineBytes + w * depthBytes;
                  unsigned int value = 0;
                  //a值不参与计算
                  if (GT_AVE == gt) {
                        for (int i = 0; i < 3; i++) {
                            value += unsigned int(pictureData.m_ptuchPixel);
                        }
                        value /= 3;
                  }
                  else if (GT_STD == gt) {
                        unsigned int r = unsigned int(pictureData.m_ptuchPixel);
                        unsigned int g = unsigned int(pictureData.m_ptuchPixel);
                        unsigned int b = unsigned int(pictureData.m_ptuchPixel);

                        value = unsigned int(0.30 * r + 0.59 * g + 0.11 * b);
                  }

                  //不更改a值
                  for (int i = 0; i < 3; i++) {
                        pictureData.m_ptuchPixel = value;
                  }
                }
            }
      }
      pictureData.m_bIsGray = true;

      return true;
    }
最后,需要说明的是,在操作BMP图片时一定要注意8位图得特殊处理,因位这种图片多1024字节的调色板数据。还有读取、写入文件时,图片每一行的字节数都必须是4的整数倍,这个一定得注意,不然颜色数据就偏移了。而且本人也只给了三种简单方式操作图片,有兴趣可以挑战PS、以及各种美图软件的对图片的操作,看看能不能实现。也算是对自己的一种锻炼。所以,了解图片本身之后,其他编程语言一样的也就很容易实现了。

Baste 发表于 2023-3-21 22:31

这些苦力活前人都干过了,图形图像显示有Qt库,里面有QImage类可以显示各种格式的图像,还有对图像像素的简单操作。复杂的话,比如去噪模糊之类可以用opencv库。当然,因为这些图像的处理都是卷积的运算,更现代的方式是用gpu去做这些,常用的库有cuda

franciscochonge 发表于 2023-3-21 22:40

是有现成的框架,但是不懂原理,个人不是很喜欢。个人更倾向于造轮子,正所谓,道不同,不相为谋。

mypro334 发表于 2023-3-21 22:48

%%%

Ylisar 发表于 2023-3-21 22:54

我也做过读取bmp图片(24位的),是为了做最低有效位隐写,当时我的想法和你差不多,能自己写底层很爽,但现在我读图都是用opencv,去实现这些功能太麻烦了,对技术提高也没有帮助

DungDaj 发表于 2023-3-21 22:59

我觉得还是要看你工作性质,假如说这个事情就是个心细活但是不困难而且对工作而言提升不大就没必要.比如我是学计算机的但是我不打算从事图形学相关的工作我就不那么需要会这么多细节知识,如果是从事图形学工作而且这一次造轮子之后以后还可以使用那么我觉得又很有必要了.

HuldaGnodim 发表于 2023-3-21 23:00

请问如何读取或者处理图片的色域呢?

acecase 发表于 2023-3-21 23:05

算法移植很有必要,当然做框架类的,也是必须要了解的基础。

JoshWindsor 发表于 2023-3-21 23:12

你这代码,都没有class picture.就定义一大堆 picture::////。真是无语
页: [1]
查看完整版本: C++操作图像、图片