今天想说的是如何用C++语言操作图片(其实案例代码是用C++写的,如果想用别的语言操作图片,看完本片就会了)。更准确的说是如何从图片文件本身去操作,而不受限于用什么语言。可能这句话有很多人不是很理解,下面我将仔细阐述一下。
就拿我第一各带图形的程序来说吧。例如下面这个五子棋游戏,下面这张图就是这个项目的主界面。
当时,我是用的C语言图形库叫Easxy(感兴趣的可以了解一下)。里面有个loadimage函数,是专门读取jpg格式的图片文件。我当时费劲半天找到这个函数,以为自己会操作图片了。结果没过多久,我又用C++ Win32想写个带图形的程序,结果费牛劲找到了GDI、GDI+两个图像库,里面分别有LoadBitmap函数和Image对象是处理图形的,才解决了问题。后来也用其他编程语言都写过带图形的程序,反正都很费劲才解决我想要的问题。
对本人而言,更倾向于底层造轮子的。所以,后来专门对图片进行研究了一番,才知道我对图片理解错了。我们在编程时操作图片时,不要依赖于程序语言的提供的函数接口,而更应该是从图片文件本身来处理问题。哪些各种编程语言之所以提供这些接口,更多是提高开发效率,而且大多数程序员也不关注图片具体原理。目前我们熟悉的图片格式有BMP(又称位图,计算机原始图,未经压缩),JPG(JPEG编码),JPGE(JPEG),PNG(没有具体了解,感兴趣的可以了解一下)等。所以我们要想操作图片,我们必须了解这些格式的图片是怎么存储的(编码),这样我们才能对图片进行解析。
下面BMP格式的图片进行举例说明,分别对缩放,裁剪,灰度等(三个进行说明)。因为BMP是最容易处理的,知其一,后续任何格式都知道怎么操作了(就是麻烦点而已)。
一问:如何读取、存储BMP图片文件?
首先,我们得知道BMP的编码格式,不知道到去网上查。BMP是包含三个部分(文件头[14字节],图片信息头[40字节],调色板数据[1024字节][可选],颜色数据)。调色板数据,只有位深(位深,简单来说一个像素点的大小,比如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[1024] = { 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[4 * 256];
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[pictureData.m_ulPixelLen];
::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[1024] = { 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[byteLen];
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[count];
unsigned char j = 0;
for (int i = 0; i < count; i += 4, j++) {
ptColorTable[i] = j;
ptColorTable[i + 1] = j;
ptColorTable[i + 2] = j;
ptColorTable[i + 3] = 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[1024] = { 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[dstPixelLen];
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[1024] = { 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[dstLineBytes * dstHeight];
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[leftUp + i] * (1 - offsetX) * (1 - offsetY);
double part2 = pictureData.m_ptuchPixel[rightUp + i] * offsetX * (1 - offsetY);
double part3 = pictureData.m_ptuchPixel[leftDown + i] * offsetY * (1 - offsetX);
double part4 = pictureData.m_ptuchPixel[rightDown + i] * offsetY * offsetX;
dstBytes[index + i] = 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[1024] = { 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[newLineBytes * pictureData.m_unHeight];
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[index + i]);
}
value /= 3;
int newIndex = h * pictureData.m_unWidth + w;
colorData[newIndex] = 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[index + i]);
}
value /= 3;
}
else if (GT_STD == gt) {
unsigned int r = unsigned int(pictureData.m_ptuchPixel[index]);
unsigned int g = unsigned int(pictureData.m_ptuchPixel[index + 1]);
unsigned int b = unsigned int(pictureData.m_ptuchPixel[index + 2]);
value = unsigned int(0.30 * r + 0.59 * g + 0.11 * b);
}
//不更改a值
for (int i = 0; i < 3; i++) {
pictureData.m_ptuchPixel[index + i] = value;
}
}
}
}
pictureData.m_bIsGray = true;
return true;
}
最后,需要说明的是,在操作BMP图片时一定要注意8位图得特殊处理,因位这种图片多1024字节的调色板数据。还有读取、写入文件时,图片每一行的字节数都必须是4的整数倍,这个一定得注意,不然颜色数据就偏移了。而且本人也只给了三种简单方式操作图片,有兴趣可以挑战PS、以及各种美图软件的对图片的操作,看看能不能实现。也算是对自己的一种锻炼。所以,了解图片本身之后,其他编程语言一样的也就很容易实现了。