图像处理之(24位)BMP旋转以及镜像算法

  在图像处理,图形学、计算机视觉中,我们经常能够见到bmp这种格式的图片;那么对于我们来说想要处理这种图片,首先就应当了解这种图片,知己知彼方能百战不殆。

  那么首先我们来了解一下bmp格式的图片:


一、了解BMP

(一)概述

  BMP(全称Bitmap)是Windows操作系统中的标准图像文件格式,可以分成两类:设备相关位图(DDB)和设备无关位图(DIB),使用非常广。它采用位映射存储格式,除了图像深度可选以外,不采用其他任何压缩,因此,BMP文件所占用的空间很大。BMP文件的图像深度可选lbit、4bit、8bit及24bit。BMP文件存储数据时,图像的扫描方式是按从左到右、从下到上的顺序。由于BMP文件格式是Windows环境中交换与图有关的数据的一种标准,因此在Windows环境中运行的图形图像软件都支持BMP图像格式。


(二)格式特性

  了解完BMP的通用性之后,其实我们想要处理BMP格式图片,最关心的还是这个格式的组成是怎样的,有什么特点,怎样处理这个格式的图片。
  因此我们来把重点放在BMP的格式组成上:


  BMP其实是图片格式中最简单的一种,一般来说,典型的BMP图像数据文件由四部分组成:
   1. 位图文件头数据(BITMAPFILEHEADER):这个数据结构包含了BMP图像文件的类型、大小等信息;
typedef struct targetBITMAPFILEHEADER{
    WORD bfType; //文件类型,对于位图来说,这一部分为0x4d42
    DWORD bfSize; //文件大小(包含这14字节)
    WORD bfReserved1; //保留字,不考虑
    WORD bfReserved2; //保留字,同上
    DWORD bfOffBits; //实际位图数据的偏移字节数,即前三个部分长度之和 
}BITMAPFILEHEADER;


  2. 位图信息头数据(BITMAPINFOHEADER):这个数据结构则是包含了BMP图像数据的宽、高、压缩方法、定义颜色、占用空间等等信息;
typedef struct targetBITMAPINFOHEADER{
    DWORD   biSize;             //指定此结构体的长度,为40  
    LONG    biWidth;            //位图宽  
    LONG    biHeight;           //位图高  
    WORD    biPlanes;           //平面数,为1  
    WORD    biBitCount;         //采用颜色位数,可以是1,2,4,8,16,24,新的可以是32  
    DWORD   biCompression;      //压缩方式,可以是0,1,2,其中0表示不压缩  
    DWORD   biSizeImage;        //实际位图数据占用的字节数  
    LONG    biXPelsPerMeter;    //X方向分辨率  
    LONG    biYPelsPerMeter;    //Y方向分辨率  
    DWORD   biClrUsed;          //使用的颜色数,如果为0,则表示默认值(2^颜色位数)  
    DWORD   biClrImportant;     //重要颜色数,如果为0,则表示所有颜色都是重要的  
}BITMAPINFOHEADER;


  3.调色板(RGBQUAD):其中,这一部分的数据结构是可选择的,有些为徒需要调色板,有些位图则不需要(比如24位的真彩图就不需要);
typedef struct tagRGBQUAD{
    BYTE rgbBlue;
    BYTE rgbGreen;
    BYTE rgbRed;
    BYTE rgbReserved;
}RGBQUAD;


  4.位图数据:这部分的内容根据BMP位图使用的位数不同而不同,在24位真彩图中,直接使用RGB,而其他的小于24位的则使用调色板中颜色的索引值。
typedef struct tagIMAGEDATA  
{  
    BYTE blue;  
    BYTE green;  
    BYTE red;  
}IMAGEDATA;




二、旋转BMP的理论准备

  在进行编码之前,我们首先需要知道——旋转BMP需要什么样的准备工作、怎样去做,那么仔细考虑一下其实我们面临的是以下的几个问题:
  1. 首先我们在进行旋转的时候应当注意在旋转前与旋转后的坐标对应关系;
  2.当我们旋转之后,如果旋转之后的像素点并不是很如人意的落在像素点上,而是落在临近的四个像素点构成的正方形区域内(而且这种情况应该是很常见的一种),我们应当怎样确定旋转之后的像素的颜色值;
  3.怎样对图像进行旋转;
  4.旋转之后怎样将整个图片显示出来;




(一)问题一的求解

  那么针对于第一个问题,我们要做的就是进行坐标变换:如以下的图片:

num1

  这样我们就完成了第一个问题的求解,这样我们就得到了变换前后的坐标的相互关系。


(二)问题二的求解

  对于问题二的求解,我们采取的方式是,双线性插值的方法来完成计算该点的像素值。

num2

(三)问题三的求解

   我们现在已经完成了这些,我们就可以开始进行一些理论上的第三个问题——即怎样进行旋转,我们应当采用怎样的方式来进行图像的旋转——
  我们的第一个想法可能就是,我们通过原来的像素点,然后通过问题一种确定的坐标变换公式,然后向新建的空白图层中填充由坐标变换得到的数据。但是这样的方式可能存在一些问题:
  其一:在计算过程之中,可能得到的数据并不落在像素点上;
  其二:有些数据可能转出了现有的空间。(但是这个问题比较好处理,主要是前面的一个问题)


  鉴于以上的考虑,我们采用的是先创建新的空白图层,然后根据新图层上的像素点然后计算出对应的原始图像中的像素点,这时候虽然不一定落在像素点上,但是我们可以通过问题二的求解方法来完成点P颜色值的计算。


(四)问题四的求解

num3


三、具体代码

  首先,我们先创建一个bmp的头文件:
/**
* @author Mica_Dai
* @date 2017.10.2
* */
typedef unsigned char  BYTE;  
typedef unsigned short WORD;  
typedef unsigned long  DWORD;  
typedef long LONG; 

typedef struct targetBITMAPFILEHEADER{
    DWORD bfSize; //文件大小(包含这14字节)
    WORD bfReserved1; //保留字,不考虑
    WORD bfReserved2; //保留字,同上
    DWORD bfOffBits; //实际位图数据的偏移字节数,即前三个部分长度之和 
}BITMAPFILEHEADER;

typedef struct targetBITMAPINFOHEADER{
    DWORD   biSize;             //指定此结构体的长度,为40  
    LONG    biWidth;            //位图宽  
    LONG    biHeight;           //位图高  
    WORD    biPlanes;           //平面数,为1  
    WORD    biBitCount;         //采用颜色位数,可以是1,2,4,8,16,24,新的可以是32  
    DWORD   biCompression;      //压缩方式,可以是0,1,2,其中0表示不压缩  
    DWORD   biSizeImage;        //实际位图数据占用的字节数  
    LONG    biXPelsPerMeter;    //X方向分辨率  
    LONG    biYPelsPerMeter;    //Y方向分辨率  
    DWORD   biClrUsed;          //使用的颜色数,如果为0,则表示默认值(2^颜色位数)  
    DWORD   biClrImportant;     //重要颜色数,如果为0,则表示所有颜色都是重要的  
}BITMAPINFOHEADER;

typedef struct tagRGBQUAD{
    BYTE rgbBlue;
    BYTE rgbGreen;
    BYTE rgbRed;
    BYTE rgbReserved;
}RGBQUAD;


  然后的工作就主要放在文件读取上,我们就不断的从原始的文件中读取数据,然后将处理之后的数据写入新的文件当中:这样来说我们现在需要的做的就是
  1. 创建一个新文件,根据图像的配置等完成信息头等数据的写入,这些基本信息配置完成之后,我们就可以直接读取位图信息了;
  2. 申请空间存放新旧图像信息的数据,然后将图像信息存放在旧的数组内;
  3. 然后从头扫描到位,并对每一个扫描的像素点进行坐标变换,然后检测该变换之后的像素点是否在原始图像范围内
    a. 如果不在则将新像素点置为0;
    b. 在范围内,则通过双线性插值完成颜色值的配置
  5. 将变换之后的数据写入新数组,然后在写入文件内
  6. 关闭文件;




  接下来就是我们的第一部的相关代码:
   注意:在读取文件的时候一定要注意,我们读取一定要按照顺序来读,即一定要对齐,否则可能会数据错位而导致一系列的bug,最后导致结果图片无法显示,数据错误。我曾经重复读取了bftype,最后的结果就是一个400多K的图片欸处理成为了1.5G的“东西”,同时数据也损坏了。
/**
* @author Mica_Dai
* @Date 2017.10.2
* */
    FILE *file, *targetFile;
    int rotateX, rotateY;
    int write_rotateX, write_rotateY;
    BITMAPFILEHEADER bmpFile, writeBmpFile;
    BITMAPINFOHEADER bmpInfo, writeBmpInfo;

    int angle;
    double thelta;
    char fileName[20];
    WORD bfType;
    cout << "please input the bmp file's name : ";
    cin >> fileName;
    file = fopen(fileName, "rb");
    targetFile = fopen("16.bmp", "wb");

    /**
     * step 1 : 图片处理第一步,首先是完成将文件头,信息头等的数据迁移
    */
    fread(&bfType, 1, sizeof(WORD), file);  
    fwrite(&bfType, 1, sizeof(WORD), targetFile);
    if (0x4d42 != bfType){
        cout << "wrong format!" << endl;
        return -1;
    }

    cout << "please input the rotate angle : ";
    cin >> angle;
    if (angle % 360 == 270){
        angle ++;
    }
    thelta = (double)(angle * PI / 180);
    // 整个变化中bmp的文件头是相似的,只有bfsie变化了
    fread(&bmpFile, 1, sizeof(BITMAPFILEHEADER), file);
    writeBmpFile = bmpFile;
    // 整个变换过程之中bmp的信息头也是相似的,这个则是biwidth、biheight,以及bisizeimage发生变化
    fread(&bmpInfo, 1, sizeof(BITMAPINFOHEADER), file);
    writeBmpInfo = bmpInfo;

    int width = bmpInfo.biWidth;
    int height = bmpInfo.biHeight;

    int newWidth = abs(width * cos(thelta) + height * sin(thelta));
    // int newHeight = 1054;
    int newHeight = abs(width * sin(thelta) + height * cos(thelta));
    // int newWidth = 1500;

    writeBmpInfo.biWidth = newWidth;
    writeBmpInfo.biHeight = newHeight;

    // 在计算实际占用的空间的时候我们需要将宽度为4byte的倍数
    int writewidth = WIDTHBYTES(newWidth * writeBmpInfo.biBitCount); 
    writeBmpInfo.biSizeImage = writewidth * writeBmpInfo.biHeight;
    writeBmpFile.bfSize = 54 + writeBmpInfo.biSizeImage;
    fwrite(&writeBmpFile, 1, sizeof(BITMAPFILEHEADER), targetFile);
    fwrite(&writeBmpInfo, 1, sizeof(BITMAPINFOHEADER), targetFile);


   第二步:进行空间的申请以及数据的填充:
/**
     * step 2 : 完成空间的申请与分配的准备工作
    */
    // 在这里因为待处理的图片宽度应当是4byte的倍数,因此我们首先要完成对宽度进行完成4byte的倍数化
    int l_width = WIDTHBYTES(width * bmpInfo.biBitCount);
    int write_width = WIDTHBYTES(writeBmpInfo.biWidth * writeBmpInfo.biBitCount);
    rotateX = width / 2;
    rotateY = height / 2;
    write_rotateX = newWidth / 2;
    write_rotateY = newHeight / 2;

    // cout << "writeBmpInfo.biWidth : " << writeBmpInfo.biWidth << endl;    
    // cout << "writeBmpInfo.biBitCount : " << writeBmpInfo.biBitCount << endl;
    // cout << "write_width" << write_width << endl;
    // cout << "writewidth" << writewidth << endl;


    // 准备工作完成之后,我们现在就要将bmp文件中的数据文件存放在一个数组中,因此我们需要申请空间创建数组来完成数据的存放
    BYTE *preData = (BYTE *)malloc(height * l_width);
    memset(preData, 0, height * l_width);

    BYTE *aftData = (BYTE *)malloc(newHeight * writewidth);
    memset(aftData, 0, newHeight * writewidth);


    // cout << "i'm here!" << endl;
    int OriginalImg = l_width * height;
    int LaterImg = writewidth * newHeight;

    fread(preData, 1, OriginalImg, file);


  第三步:就是整个处理的重头戏——对于整个图像的旋转处理:
/**
     * step 3 : 完成将图像信息的迁移
*/
for (int hnum = 0; hnum < newHeight; ++ hnum){    
        // cout << "hh " << hnum << endl;
        for (int wnum = 0; wnum < newWidth; ++ wnum){
            // 新数据的下标为index
            int index = hnum * writewidth + wnum * 3;
            // cout << "index " << index << endl;
            // 利用公式计算这个原来的点的地方
            double d_original_img_hnum = (wnum - write_rotateX) * sin(thelta) + (hnum - write_rotateY) * cos(thelta) + rotateY;
            double d_original_img_wnum = (wnum - write_rotateX) * cos(thelta) - (hnum - write_rotateY) * sin(thelta) + rotateX;    

            if (d_original_img_hnum < 0 || d_original_img_hnum > height || d_original_img_wnum < 0 || d_original_img_wnum > width){
                aftData[index] = 0; // 这个相当于是R
                aftData[index + 1] = 0; // 这个相当于是G
                aftData[index + 2] = 0;  // 这个相当于是B                  
                continue;
            }else{
                /**
                 * 我们在这里使用双线性插值法来完成对应
                */
                int i_original_img_hnum = d_original_img_hnum;
                int i_original_img_wnum = d_original_img_wnum;
                double distance_to_a_X = d_original_img_wnum - i_original_img_wnum;
                double distance_to_a_Y = d_original_img_hnum - i_original_img_hnum;

                int original_point_A = i_original_img_hnum * l_width + i_original_img_wnum * 3;
                int original_point_B = i_original_img_hnum * l_width + (i_original_img_wnum + 1) * 3;
                int original_point_C = (i_original_img_hnum + 1) * l_width + i_original_img_wnum * 3;
                int original_point_D = (i_original_img_hnum + 1) * l_width + (i_original_img_wnum + 1) * 3;

                if (i_original_img_wnum == width - 1){
                    // cout << "hhhhh" << endl;
                    original_point_A = original_point_B;
                    original_point_C = original_point_D;
                }
                if (i_original_img_hnum == height - 1){
                    original_point_C = original_point_A;
                    original_point_D = original_point_B;
                }

                aftData[index] = (1 - distance_to_a_X) * (1 - distance_to_a_Y) * preData[original_point_A]
                                    + (1 - distance_to_a_X) * distance_to_a_Y * preData[original_point_B]
                                    + distance_to_a_X * (1 - distance_to_a_Y) * preData[original_point_C]
                                    + distance_to_a_X * distance_to_a_Y * preData[original_point_D]; 

                aftData[index + 1] = (1 - distance_to_a_X) * (1 - distance_to_a_Y) * preData[original_point_A + 1]
                                    + (1 - distance_to_a_X) * distance_to_a_Y * preData[original_point_B + 1]
                                    + distance_to_a_X * (1 - distance_to_a_Y) * preData[original_point_C + 1]
                                    + distance_to_a_X * distance_to_a_Y * preData[original_point_D + 1];

                aftData[index + 2] = (1 - distance_to_a_X) * (1 - distance_to_a_Y) * preData[original_point_A + 2]
                                    + (1 - distance_to_a_X) * distance_to_a_Y * preData[original_point_B + 2]
                                    + distance_to_a_X * (1 - distance_to_a_Y) * preData[original_point_C + 2]
                                    + distance_to_a_X * distance_to_a_Y * preData[original_point_D + 2];
            }    
        }
    }


   第四步:写入数据以及写入文件
/**
* step 4:写入数据
*/
    fwrite(aftData, 1, LaterImg, targetFile);
    fclose(file);
    fclose(targetFile);


四、旋转效果:

  最后的效果图如下图所示:
  旋转前:

num4

  旋转之后:(45°,60°,90°,180°)

num5
num6
num7
num8



五、图片镜像理论准备

  对于图片镜像,我们需要坐的理论掌握只有一个就是关于对称轴怎样变换:

num9



六、镜像编码

  前几步读取数据都是一样的,因此我们,只有在处理图片这一步不同:于是有如下代码:
/**
* @author Mica_Dai
* @Date 2017.10.2
* */

for (int hnum = 0; hnum < height; ++ hnum){
        for (int wnum = 0; wnum < width; ++ wnum){
            int index = hnum * actual_width + wnum * bCount;

            int original_X = width - wnum;
            int original_Y = hnum;

            for (int i = 0; i < bCount; ++ i){
                aftData[index + i] = preData[original_Y * actual_width + original_X * bCount + i];                
            }
        }
    }


七、镜像效果

num10

  以上。
发布了24 篇原创文章 · 获赞 32 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_36752072/article/details/78151770