C语言数字图像处理---1.1图像的像素格式与图像读写

前言

        本专栏面向所有希望或有兴趣从事数字图像处理工作、学习或研究的朋友,不懂MATLAB和OPENCV没关系,仅需要基础的C语言知识,便可以通过本专栏内容轻松入门数字图像处理方向。目前市面上的数字图像处理书籍种类繁多,往往令人眼花缭乱,不知从何而起,复杂的第三方库调用,也导致了大多数初学者苦不堪言,而本专栏内容将从繁就简,另辟蹊径,以简约明了的逻辑,无任何第三方库依赖的C语言代码,来帮助大家快速掌握,轻松入门, 这也是本专栏和作者的初衷。同时,本专栏内容的逻辑方法,并不依赖于C语言,大家也可以用同样的逻辑方法去学习其他语言的图像处理,这就是掌握学习方法的重要性。

图像像素格式

        对于初学者,往往搞不清楚,一个像素究竟是什么?针对数字图像中的位图而言,一张宽度W,高度H的图像是由W×H个像素点来表示的,每个像素都包含了各自的颜色信息,所以我们的感官才会感知到不同图像各自是什么颜色的。要有颜色的概念,我们就要先了解色彩的深度。

        色彩深度就是色彩的位数,代表了一个像素用多少个二进制位来表示颜色信息。常用的色彩深度有1位(也就是单色),2位(也就是4色CGA),4位(也就是16色VGA),8位(也就是256色),16位(增强色)以及24位和32位真彩色等。听起来对于初学者好像不容易理解,我们这里以黑白二值图、灰度图和24/32位彩色图四类来做说明。

        黑白二值单色图像:图像中每个像素点非黑即白,对于像素值非0即1,每一个像素用一个数值也就是1个二进制位即可表示(一个二进制位代表0或者1),因此,这种黑白二值图也可以叫作单色图,黑白二值图像举例如下图Fig.1所示。

                                                                                 Fig.1黑白二值图像示例

        在Fig.1中,对于任意像素P0,如果它是黑色像素,那么P0=0,反之,P0=1,这就是黑白二值图像中像素P0的数字表示。由于每个像素的数值都在0-255之间,因此,通常我们使用unsigned char类型的数组来存出每个像素的数值。对于Fig.1这张宽高为256×256大小的黑白二值图而言,我们可以用如下数组形式来存储数据:

unsigned char img[256*256]={1,1,1,....};

        8位灰度图像:8位灰度图像是指用8个bit位来表示颜色信息的图像,颜色信息范围位0-255,0是黑色,255是白色,对应的二进制位表示如下:

        0的二进制位表示:00000000

        255的二进制位表示:11111111

        8位灰度图像举例如图Fig.2所示,看起来是一张灰色的图像,但是人物细节等颜色信息明显要比单色二值图像要多很多,因为二值图像只有0和1两个颜色信息,而灰度图有0-255共256个颜色信息;

                                                                                      Fig.2 8位灰度图示例

        在Fig.2中,对于任意像素P0,如果它是黑色像素,那么P0=0,白色P0=255,其他颜色则P0在0到255之间。这就是8位灰度图像中像素P0的数字表示。由于每个像素的数值都在0-255之间,因此,通常我们依旧使用unsigned char类型的数组来存出每个像素的数值。对于Fig.2这张宽高为256×256大小的灰度图而言,我们可以用如下数组形式来存储数据:

unsigned char imggray[256*256]={255,255,255,....};

        24位彩色图像:为了表示更加丰富的彩色信息,我们基于三原色RGB,将每个像素分为了R、G和B三个颜色分量,即红色分量Red,绿色分量Green和蓝色分量Blue。同时,我们对于每个分量都使用8个二进制位也就是1个字节大小来表示它的颜色信息,对应数值范围为0-255。这样,一个像素占用3个字节,24个Bit位,也就是24位彩色图像。颜色信息则是RGB三个颜色分量的组合,由于每个分量可以表示0-255共256种颜色,因此,24位彩色图像像素共有256×256×256种颜色信息,我们也将RGB三个颜色分量叫作三个通道,举例如图Fig.3所示。 

                                                                                  Fig.3 24位彩色图像示例

        在Fig.3中,对于任意像素P0,如果它是黑色像素,那么P0=(R=0,G=0,B=0),白色P0=(R=255,G=255,B=255),通常我们用一个RGB坐标轴的三维坐标来表示,即黑色P0(0,0,0),白色P0(255,255,255)。这就是24位彩色图像中像素P0的数字表示。由于每个像素的RGB数值都在0-255之间,因此,通常我们依旧使用unsigned char类型的数组来存出每个像素的数值。对于Fig.3这张宽高为256×256大小的灰度图而言,由于每个像素有三个通道,我们可以用如下数组形式来存储数据:

unsigned char imgcolor24[256*256*3]={255,255,255,....};

        32位彩色图像:理解了24位彩色图像,那么,32位彩色图像就是在24位彩色图像的基础上添加了一个透明通道alpha位,我们经常看到一些有透明区域的图像,这些透明区域如何控制,就是依靠这个alpha通道来实现的。对于32位彩色图像的每个像素,我们使用RGBA四个颜色分量来表示,A就是透明度分量,同样占用1个字节8个bit,所以,一个像素共占用32个bit,4个字节。我们称32位彩色图像有4个通道,也就是RGBA四通道。对于黑色像素表示为(0,0,0,A),白色像素表示为(255,255,255,A),举例如图Fig.4所示。

                                                                                  Fig.4 32位彩色图像示例

        在Fig.4中,方格子区域就表示这些区域的像素透明通道是0(全透明),我们可以看到的人物区域像素的透明通道是255(不透明)。由于每个像素的RGBA数值都在0-255之间,因此,对于Fig.4这张宽高为256×256大小的灰度图而言,由于每个像素有四个通道,我们可以用如下数组形式来存储数据:

unsigned char imgcolor32[256*256*4]={255,255,255,....};

        对于上述几种格式,是我们比较常见的,而对于初学者,本文将以32位BGRA四通道位图格式为主,来教会大家如何入门数字图像处理。其他几种格式,大家可以简单理解为通道数的差别。

图像读写

        图像读写从专业角度又叫图像编解码,图像编解码是数字图像处理中的重要组成部分,甚至是一个可以单独出书的模块。由于图像格式多种多样,需要对每一种图像进行格式分析,然后单独编解码,同时还要考虑效率和质量问题,因此,也是一个难啃的骨头。对于初学者而言,想要自己实现常用图像的编解码算法,基本不太现实,常用的方法就是调用各种第三方库,比如libjpg/libpng等,或者直接使用opencv/matlab等数字图像处理库。而这些方法对于初学者而言,又是各种配置,各种依赖,苦不堪言。

        对于那些只想学下图像处理算法,并不像涉猎图像编解码,也不想花时间去使用和依赖第三方库的朋友们而言,有没有一种更好的方式,比如以简单的C语言调用来进行图像读写呢?答案是肯定的,这就是github上一份来自MIT的开源代码“stb”。

        stb的代码链接:STB图像编解码

        stb的代码中关于图像读写的部分只有两个头文件:stb_image.h和stb_image_write.h,可以实现常用图像格式如“BMP/JPG/PNG/TGA/HDR/PSD/GIF”等的编解码,而且支持从文件流和文件路径以及内存三个方式进行处理,算法进行了一定的汇编优化,最重要的是代码开源,速度快,效果好,逻辑简单!对于初学者,stb的出现真是一个不小的福音。

        为了更好的从初学者角度考虑,笔者对stb进行了二次封装,以32位bgra四通道格式基础,将stb的几种常用图像格式“BMP/JPG/PNG/TGA”编解码接口进行了合并融合,得到了如下简单的接口:

/***************************ImageFormat**************************/
enum IMAGE_FORMAT{BMP = 0, JPG, PNG, TGA};
/************************************************************
*Function:  Trent_ImgBase_ImageLoad
*Description: Image loading
*Params:    fileName-image file path,eg:"C:\\test.jpg".
*           width-image width.
*           height-image height.
*           component-the bits per pixel.
*                     1           grey
*                     2           grey, alpha
*                     3           red, green, blue
*                     4           red, green, blue, alpha
*Return:    image data.
************************************************************/
unsigned char* Trent_ImgBase_ImageLoad(char* fileName, int* width, int* height, int* component);
/************************************************************
*Function:  Trent_ImgBase_ImageSave
*Description: Image loading
*Params:    fileName-image file path,eg:"C:\\save.jpg".
*           width-image width.
*           height-image height.
*           data-the result image data to save, with format BGRA32.
*           format-image format,0-BMP,1-JPG,2-PNG,3-TGA
*Return:    0-OK.
************************************************************/
int Trent_ImgBase_ImageSave(char const *fileName, int width, int height, const void* data, int format);

        在上述封装代码中,我们可以看到,stb的多个接口被合并为了两个接口,Trent_ImgBase_ImageLoad图像加载和Trent_ImgBase_ImageSave图像保存接口,分别使用图像路径进行操作,简单明了,更加易用。由于stb源代码中本身对于bmp和jpg格式是返回24位三通道图像数据的,为了方便初学者学习,笔者统一将其扩充为了32位bgra格式,完整的封装代码如下:

#include"f_SF_ImgBase_RW.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#include<stdlib.h>
#include<string.h>
#include<math.h>

inline unsigned char* f_TImageLoad(char* fileName, int* width, int* height, int* component, int redcomp)
{
        unsigned char* tempData = stbi_load(fileName, width, height, component, redcomp);
        //printf("component:  %d", *component);
        //根据像素通道数component进行判断,分别将8/24/32位转换为32bgra格式数据
        if(*component == 4)
        {
             unsigned char* srcData = (unsigned char*)malloc(sizeof(unsigned char) * *width * *height * 4);
            unsigned char* pSrc = srcData;
            unsigned char* pTemp = tempData;
            for(int j = 0; j < *height; j++)
            {
                for(int i = 0; i < *width; i++)
                {
                     pSrc[0] = pTemp[2];
                     pSrc[1] = pTemp[1];
                     pSrc[2] = pTemp[0];
                     pSrc[3] = pTemp[3];
                     pSrc += 4;
                     pTemp += 4;
                }
            }
            free(tempData);
            return srcData;
        }
        else if(*component == 3)
        {
            unsigned char* srcData = (unsigned char*)malloc(sizeof(unsigned char) * *width * *height * 4);
            unsigned char* pSrc = srcData;
            unsigned char* pTemp = tempData;
            for(int j = 0; j < *height; j++)
            {
                for(int i = 0; i < *width; i++)
                {
                     pSrc[0] = pTemp[2];
                     pSrc[1] = pTemp[1];
                     pSrc[2] = pTemp[0];
                     pSrc[3] = 255;
                     pSrc += 4;
                     pTemp += 3;
                }
            }
            free(tempData);
            *component = 4;
            return srcData;
        } 
        else if(*component == 1)
        {
            unsigned char* srcData = (unsigned char*)malloc(sizeof(unsigned char) * *width * *height * 4);
            unsigned char* pSrc =  (unsigned char*)srcData;
            unsigned char* pTemp = tempData;
            for(int j = 0; j < *height; j++)
            {
                for(int i = 0; i < *width; i++)
                {
                     int gray = *pTemp++;
                     pSrc[0] = gray;
                     pSrc[1] = gray;
                     pSrc[2] = gray;
                     pSrc[3] = 255;
                     pSrc += 4;
                }
            }
            free(tempData);
            *component = 4;
            return srcData;
        }
        else
            return NULL;
};
inline int f_TImageSavePng(char const *fileName, int width, int height, int component, const void  *data, int stride_in_bytes)
{
        unsigned char* pSrc = (unsigned char*)data;
        for(int j = 0; j < height; j++)
        {
             for(int i = 0; i < width; i++)
             {
                 int temp = pSrc[0];
                 pSrc[0] = pSrc[2];
                 pSrc[2] = temp;
                 pSrc+=4;
             }
        }
	return stbi_write_png(fileName, width, height, component, data, stride_in_bytes);
};
inline int f_TImageSaveBmp(char const *fileName, int width, int height, int component, const void  *data)
{
        unsigned char* pSrc =  (unsigned char*)data;
        for(int j = 0; j < height; j++)
        {
             for(int i = 0; i < width; i++)
             {
                 int temp = pSrc[0];
                 pSrc[0] = pSrc[2];
                 pSrc[2] = temp;
                 pSrc+=4;
             }
        }
	return stbi_write_bmp(fileName, width, height, component, data);
};
inline int f_TImageSaveTga(char const *fileName, int width, int height, int component, const void  *data)
{
        unsigned char* pSrc =  (unsigned char*)data;
        for(int j = 0; j < height; j++)
        {
             for(int i = 0; i < width; i++)
             {
                 int temp = pSrc[0];
                 pSrc[0] = pSrc[2];
                 pSrc[2] = temp;
                 pSrc+=4;
             }
        }
	return stbi_write_tga(fileName, width, height, component, data);
};

inline int f_TImageSaveJpg(char const *fileName, int width, int height, int component, const void  *data, int quality)
{
        unsigned char* pSrc = (unsigned char*)data;
        for(int j = 0; j < height; j++)
        {
             for(int i = 0; i < width; i++)
             {
                 int temp = pSrc[0];
                 pSrc[0] = pSrc[2];
                 pSrc[2] = temp;
                 pSrc+=4;
             }
        }
	return stbi_write_jpg(fileName, width, height, component, data, quality);
};
/************************************************************
*Function:  Trent_ImgBase_ImageLoad
*Description: Image loading
*Params:    fileName-image file path,eg:"C:\\test.jpg".
*           width-image width.
*           height-image height.
*           component-the bits per pixel.
*                     1           grey
*                     2           grey, alpha
*                     3           red, green, blue
*                     4           red, green, blue, alpha
*Return:    image data.
************************************************************/
unsigned char* Trent_ImgBase_ImageLoad(char* fileName, int* width, int* height, int* component)
{
	int redcomp = 0;
	return f_TImageLoad(fileName, width, height, component, redcomp);
};
/************************************************************
*Function:  Trent_ImgBase_ImageSave
*Description: Image loading
*Params:    fileName-image file path,eg:"C:\\save.jpg".
*           width-image width.
*           height-image height.
*           data-the result image data to save, with format BGRA32.
*           format-image format,0-BMP,1-JPG,2-PNG,3-TGA
*Return:    0-OK.
************************************************************/
int Trent_ImgBase_ImageSave(char const *fileName, int width, int height, const void* data, int format)
{
	int component = 4;
	int ret = 0;
    //判断图像格式,根据格式进行图像保存
	switch(format)
	{
	case 0://bmp
		ret = f_TImageSaveBmp(fileName, width, height, component, data);
		break;
	case 1://jpg
		ret = f_TImageSaveJpg(fileName, width, height, component, data, 100);
		break;
	case 2://png
		ret = f_TImageSavePng(fileName, width, height, component, data, width * 4);
		break;
	case 3://tga
		ret = f_TImageSaveTga(fileName, width, height, component, data);
		break;
	default:
		printf("Trent_SF_ImgBase_ImageSave ERROR!");
		break;
	}
	return 0;
};

这两个接口的调用代码如下所示:

#include "stdafx.h"
#include"imgRW\f_SF_ImgBase_RW.h"

int _tmain(int argc, _TCHAR* argv[])
{
	//定义输入图像路径
	char* inputImgPath = "C://Test.jpg";
	//定义输出图像路径
	char* outputImgPath = "D://Test_Res.jpg";
	//定义图像宽高信息
	int width = 0, height = 0, component = 0, stride = 0;
	//图像读取(得到32位bgra格式图像数据)
	unsigned char* bgraData = Trent_ImgBase_ImageLoad(inputImgPath, &width, &height, &component);
	stride = width * 4;
	//其他图像处理操作(这里以32位彩色图像灰度化为例)
	//IMAGE PROCESS/
	unsigned char* pSrc = bgraData;
	for(int j = 0; j < height; j++)
	{
		for(int i = 0; i < width; i++)
		{
			int gray = (pSrc[0] + pSrc[1] + pSrc[2]) / 3;
			pSrc[0] = pSrc[1] = pSrc[2] = gray;
			pSrc += 4;
		}
	}
	
	//图像保存
	int ret = Trent_ImgBase_ImageSave(outputImgPath, width, height, bgraData, JPG);
	free(bgraData);
	return 0;
}

        这段测试代码中,我们使用简单的32位彩色图像灰度化效果来进行说明,对应给出测试效果图如下图5所示,简单的几行代码,快速实现了图像读写和32位彩色图像灰度化处理。

                                                                                      Fig.5 图像读写测试

        对于测试代码中,我们使用到了stride,这个概念很多初学者会产生疑惑,不知道是什么,这里给大家简单讲解一下。Stride表示图像数据在内存中的行跨度。这个行跨度并不一定是图像每一行数据的真实宽度。通常在内存中,图像的行数据是以4字节对齐的,也就是行跨度的值是4的倍数。对于32位bgra格式的图像,他的行跨度Stride=width*4,本身就是4的倍数,因此Stride与真实数据的宽度一致,不用考虑对齐问题。而对于24位rgb或bgr格式,它的每一行真实的图像数据是width*3,而这个数字并不一定是4的倍数,比如:

        一行有 11 个像素(Width = 11), 对一个 24 位(每个像素 3 字节)的图像, Stride = 11 * 3 + 3 = 36,而真实的行数据位11*3=33,这是就出现了偏差,而这个偏差值3就是扩展出来用于4字节对齐的部分。

        本文中考虑的是32位图像,大家可以忽略stride,但是,对于其他格式图像,这里我们给出一个Stride的计算公式:

        ①Stride = 每像素占用的字节数(也就是像素位数/8) * Width;

     ②如果 Stride 不是 4 的倍数, 那么 Stride = Stride + (4 - Stride mod 4);

         这里,我们给出整个工程的代码:C语言图像读写代码

        上面内容作为本专栏的第一个章节,我们用较为简单和通俗易懂的方式,来讲解了图像像素和图像读写,可能没有专业书籍那么专业,但是,笔者的宗旨是让每一个初学者能够轻松入门!

        最后,谈一下对于初学者的一些建议:对于学习图像算法,个人觉得,还是不要使用opencv和matlab的好,为什么?无论是opencv还是matlab或者其他类似的库,都只是一种图像处理工具,他们功能强大,封装了各种图像算法,但是,你在使用它的时候,往往是简单的调用它所提供的接口,而不是去了解它的具体算法,长此以往,不利于图像算法的学习。实践出真知,这才是学好算法的王道!

        本人QQ1358009172,有什么疑问欢迎相互讨论!

 

 

Trent1985 CSDN认证博客专家 深度学习 人像美颜美妆算法 图像特效
本人多年来专注图像特效、人像美颜美妆算法研究、AI美颜美妆探索,著有《图像视频滤镜与人像美颜美妆算法详解》一书,欢迎 志同道合的朋友们一起学习交流!
个人座右铭:谨言慎行,三思而后行!
©️2020 CSDN 皮肤主题: 成长之路 设计师:Amelia_0503 返回首页
实付 19.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值