我的C/C++语言学习进阶之旅介绍一下NDK开发之C的简单易用图像库stb

Posted 字节卷动

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了我的C/C++语言学习进阶之旅介绍一下NDK开发之C的简单易用图像库stb相关的知识,希望对你有一定的参考价值。

一、stb_image 地址

stb 的Github地址为:https://github.com/nothings/stb


这个库已经有19K的人starred,有6.6k的人fork。非常受欢迎。

而且在LearnOpenGL网站的demo代码里面也是使用这个库来加载图片的。

二、 为什么叫stb库呢?

因为stb作者名字的首字母,作者名叫Sean T. Barrett。这不是出于自大的选择,而是作为对文件名和源函数名称进行命名空间的一种适度理智的方式。

三、stb库的几个库

我们着重关注下下面三个库即可。

四、怎么引入这些库?

单头文件库背后的想法是它们易于分发和部署,因为所有代码都包含在单个文件中。默认情况下,此处的 .h 文件充当它们自己的头文件,即它们声明文件中包含的函数,但实际上不会导致任何代码被编译。

因此,此外,您应该准确选择一个实际实例化代码的 C/C++ 源文件,最好是您不经常编辑的文件。这个文件应该定义一个特定的宏(每个库都有文档记录)来实际启用函数定义。

4.1 引入stb_image

例如,要使用 stb_image,您应该有将 stb_image.hC/C++ 文件添加到工程中,然后在C++文件添加下面的代码

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

通过定义STB_IMAGE_IMPLEMENTATION,预处理器会修改头文件,让其只包含相关的函数定义源码,等于是将这个头文件变为一个 .cpp 文件了。现在只需要在你的程序中包含stb_image.h并编译就可以了

4.2 引入stb_image_write.h

要使用 stb_image_write,您应该有将 stb_image_write.hC/C++ 文件添加到工程中,然后在C++文件添加下面的代码

#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"

4.3 引入stb_image_resize.h

要使用 stb_image_resize,您应该有将 stb_image_resize.hC/C++ 文件添加到工程中,然后在C++文件添加下面的代码

#define STB_IMAGE_RESIZE_IMPLEMENTATION
#include "stb_image_resize.h"

4.4 注意事项

上面的几个库的引用方式介绍了,但是千万要注意,别只引用了.h文件,而不引用配套的宏定义

比如使用stb_image库,应该添加下面两行代码

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

但是如果你只添加了.h头文件,而不添加宏定义的话,则会编译失败

#include "stb_image.h"

则会报错 error: undefined reference to 'stbi_load'error: undefined reference to 'stbi_image_free'之类的错误,如下所示:

上面的错误是我第一次添加stb_image.h库的时候犯的错误。直到我认真读完stbREADME文件才解决这个问题。

通过定义STB_IMAGE_IMPLEMENTATION,预处理器会修改头文件,让其只包含相关的函数定义源码,等于是将这个头文件变为一个.cpp文件了。现在只需要在你的程序中包含stb_image.h并编译就可以了。(工程中不要放stb_image.c文件,否则会报其他错误)

五、实践一下

5.1 准备工作

我们来测试一下上面三个库
我们准备读取下面这张yangchaoyue.png图片

这张yangchaoyue.png图片的信息如下:

文件名: yangchaoyue.png [1/1]
图片大小: 589.4KB
修改日期: 2022/06/01 15:59:41
图片信息: 470x834 (PNG,RGBA32)

5.2 测试思路

然后我们分别执行下面几个步骤:

  1. 读取图片信息
  2. 改变图片尺寸
  3. 重新写入到新的图片

5.3 测试代码

5.3.1 项目结构

如下图所示,我们分别将stb_image.h,stb_image_resize.h,stb_image_write.h三个头文件添加到项目中,然后写一个stb_test.cpp用来测试。

5.3.2 测试代码

stb_test.cpp代码如下所示:

#include <iostream>

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#define STB_IMAGE_RESIZE_IMPLEMENTATION
#include "stb_image_resize.h"
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <vector>

using namespace std;

int main() 
	std::cout << "Hello, STB_Image" << std::endl;

	string inputPath = "./yangchaoyue.png";
	int imageWidth, imageHeight, nrComponents;

	// 加载图片获取宽、高、颜色通道信息
	unsigned char *imagedata = stbi_load(inputPath.c_str(), &imageWidth, &imageHeight, &nrComponents, 0);
    std::cout << "测试 STB_Image 读取到的信息为:imageWidth = "<< imageWidth << 
             ",imageHeight = "<<imageHeight << ", nrComponents = " << nrComponents 
             << std::endl;  

    // 输出的宽和高为原来的一半
	int outputWidth = imageWidth / 2;
	int outputHeight = imageHeight / 2;
    // 申请内存
	auto *outputImageData = (unsigned char *)malloc(outputWidth * outputHeight * nrComponents);
    std::cout << "测试 STBIR_RESIZE, 改变后的图片尺寸为:outputWidth = "<< outputWidth<<", outputHeight = "<< outputHeight << std::endl;
	// 改变图片尺寸
	stbir_resize(imagedata, imageWidth, imageHeight, 0, outputImageData, outputWidth, outputHeight, 0, STBIR_TYPE_UINT8, nrComponents, STBIR_ALPHA_CHANNEL_NONE, 0,
		STBIR_EDGE_CLAMP, STBIR_EDGE_CLAMP,
		STBIR_FILTER_BOX, STBIR_FILTER_BOX,
		STBIR_COLORSPACE_SRGB, nullptr
		);

	string outputPath = "./yangchaoyue_output.png";
    std::cout << "测试 STB_WRITE 要写入的地址为: "<< outputPath << std::endl;
	// 写入图片
	stbi_write_png(outputPath.c_str(), outputWidth, outputHeight, nrComponents, outputImageData, 0);

	stbi_image_free(imagedata);
	stbi_image_free(outputImageData);
	return 0;


5.3 测试结果

上面代码运行如下所示:

Hello, STB_Image
测试 STB_Image 读取到的信息为:imageWidth = 470,imageHeight = 834, nrComponents = 4
测试 STBIR_RESIZE, 改变后的图片尺寸为:outputWidth = 235, outputHeight = 417
测试 STB_WRITE 要写入的地址为: ./yangchaoyue_output.png

可以看得出来,我们正常读取到了原图的信息,并将原图尺寸的宽和高改成原来的一半,并输出到新的图片yangchaoyue_output.png

yangchaoyue_output.png 打开如下所示:

文件名: yangchaoyue_output.png [2/2]
图片大小: 251.5KB
修改日期: 2022/06/01 16:03:47
图片信息: 235x417 (PNG,RGBA32)

六、API介绍

上面代码运行完毕之后,我们来介绍下用法。

6.1 stb_image

STBIDEF stbi_uc *stbi_load(char const *filename, int *x, int *y, int *comp, int req_comp)

   FILE *f = stbi__fopen(filename, "rb");
   unsigned char *result;
   if (!f) return stbi__errpuc("can't fopen", "Unable to open file");
   result = stbi_load_from_file(f,x,y,comp,req_comp);
   fclose(f);
   return result;


STBIDEF stbi_uc *stbi_load_from_file(FILE *f, int *x, int *y, int *comp, int req_comp)

   unsigned char *result;
   stbi__context s;
   stbi__start_file(&s,f);
   result = stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp);
   if (result) 
      // need to 'unget' all the characters in the IO buffer
      fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR);
   
   return result;


我们调用的代码

string inputPath = "./yangchaoyue.png";
int imageWidth, imageHeight, nrComponents;

// 加载图片获取宽、高、颜色通道信息
unsigned char *imagedata = stbi_load(inputPath.c_str(), &imageWidth, &imageHeight, &nrComponents, 0);
std::cout << "测试 STB_Image 读取到的信息为:imageWidth = "<< imageWidth << 
             ",imageHeight = "<<imageHeight << ", nrComponents = " << nrComponents 
             << std::endl;  

我们首先是调用 stbi_load 方法去加载图像数据,并获取相关信息。传入的参数除了图片文件地址,还有宽、高、颜色通道信息的引用。

变量 nrComponents 就代表图片的颜色通道值,通常有如下的情况:

  • 1 : 灰度图
  • 2 : 灰度图加透明度
  • 3 : 红绿蓝 RGB 三色图
  • 4 : 红绿蓝加透明度 RGBA 图

最后一个参数,我们传入了0,实际上就是STBI_default ,它还可以传入下面几个参数。

enum

   STBI_default = 0, // only used for desired_channels

   STBI_grey       = 1,
   STBI_grey_alpha = 2,
   STBI_rgb        = 3,
   STBI_rgb_alpha  = 4
;

返回的结果就是图片像素数据的指针了。

stbi_load 不仅仅支持 png 格式,把上面例子中的图片改成 jpg 格式后缀的依旧可行。

它支持的所有格式如下:

  • png
  • jpg
  • tga
  • bmp
  • psd
  • gif
  • hdr
  • pic

格式虽多,不过一般用到 png 和 jpg 就好了。

6.2 sbt_image_resize

加载完图片像素数据之后,就可以通过 stbir_resize 方法改变图片的尺寸。

STBIRDEF int stbir_resize(         const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes,
                                         void *output_pixels, int output_w, int output_h, int output_stride_in_bytes,
                                   stbir_datatype datatype,
                                   int num_channels, int alpha_channel, int flags,
                                   stbir_edge edge_mode_horizontal, stbir_edge edge_mode_vertical,
                                   stbir_filter filter_horizontal,  stbir_filter filter_vertical,
                                   stbir_colorspace space, void *alloc_context)

    return stbir__resize_arbitrary(alloc_context, input_pixels, input_w, input_h, input_stride_in_bytes,
        output_pixels, output_w, output_h, output_stride_in_bytes,
        0,0,1,1,NULL,num_channels,alpha_channel,flags, datatype, filter_horizontal, filter_vertical,
        edge_mode_horizontal, edge_mode_vertical, space);


stbir_edgestbir_filter 类型的参数,stb_image_resize 提供了多种类型:

typedef enum

    STBIR_EDGE_CLAMP   = 1,
    STBIR_EDGE_REFLECT = 2,
    STBIR_EDGE_WRAP    = 3,
    STBIR_EDGE_ZERO    = 4,
 stbir_edge;

typedef enum

    STBIR_FILTER_DEFAULT      = 0,  // use same filter type that easy-to-use API chooses
    STBIR_FILTER_BOX          = 1,  // A trapezoid w/1-pixel wide ramps, same result as box for integer scale ratios
    STBIR_FILTER_TRIANGLE     = 2,  // On upsampling, produces same results as bilinear texture filtering
    STBIR_FILTER_CUBICBSPLINE = 3,  // The cubic b-spline (aka Mitchell-Netrevalli with B=1,C=0), gaussian-esque
    STBIR_FILTER_CATMULLROM   = 4,  // An interpolating cubic spline
    STBIR_FILTER_MITCHELL     = 5,  // Mitchell-Netrevalli filter with B=1/3, C=1/3
 stbir_filter;

6.3 stb_image_write

最后就是调用 stbi_write_png 方法将像素数据写入文件中

#ifndef STBI_WRITE_NO_STDIO
STBIWDEF int stbi_write_png(char const *filename, int x, int y, int comp, const void *data, int stride_bytes)

   FILE *f;
   int len;
   unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len);
   if (png == NULL) return 0;

   f = stbiw__fopen(filename, "wb");
   if (!f)  STBIW_FREE(png); return 0; 
   fwrite(png, 1, len, f);
   fclose(f);
   STBIW_FREE(png);
   return 1;

#endif

除此之外,stb_image_write 还提供了

  • stbi_write_jpg 方法来保存 jpg 格式图片。
  • stbi_write_bmp 方法来保存 bmp 格式图片。
  • stbi_write_tga 方法来保存 tga 格式图片。
  • stbi_write_hdr 方法来保存 hdr 格式图片。

根据格式的不同,方法调用的参数也是不一样的。

#ifndef STBI_WRITE_NO_STDIO
STBIWDEF int stbi_write_png(char const *filename, int w, int h, int comp, const void  *data, int stride_in_bytes);
STBIWDEF int stbi_write_bmp(char const *filename, int w, int h, int comp, const void  *data);
STBIWDEF int stbi_write_tga(char const *filename, int w, int h, int comp, const void  *data);
STBIWDEF int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data);
STBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void  *data, int quality);

七、总结

stb库还是很简单的就可以使用了。

读者可以通过本项目源代码 https://github.com/ouyangpeng/stb_test 来测试学习。

八、结合OpenGL

8.1 LearnOpenGL官网的例子

可以参考下面的链接的内容

使用纹理之前要做的第一件事是把它们加载到我们的应用中。纹理图像可能被储存为各种各样的格式,每种都有自己的数据结构和排列,所以我们如何才能把这些图像加载到应用中呢?一个解决方案是选一个需要的文件格式,比如.PNG,然后自己写一个图像加载器,把图像转化为字节序列。写自己的图像加载器虽然不难,但仍然挺麻烦的,而且如果要支持更多文件格式呢?你就不得不为每种你希望支持的格式写加载器了。

另一个解决方案也许是一种更好的选择,使用一个支持多种流行格式的图像加载库来为我们解决这个问题。比如说我们要用的stb_image.h库。

stb_image.h

stb_image.hSean Barrett的一个非常流行的单头文件图像加载库,它能够加载大部分流行的文件格式,并且能够很简单得整合到你的工程之中。stb_image.h可以在这里下载。下载这一个头文件,将它以stb_image.h的名字加入你的工程,并另创建一个新的C++文件,输入以下代码:

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

通过定义STB_IMAGE_IMPLEMENTATION,预处理器会修改头文件,让其只包含相关的函数定义源码,等于是将这个头文件变为一个 .cpp 文件了。现在只需要在你的程序中包含stb_image.h并编译就可以了。

下面的教程中,我们会使用一张木箱的图片。要使用stb_image.h加载图片,我们需要使用它的stbi_load函数:

int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);

这个函数首先接受一个图像文件的位置作为输入。接下来它需要三个int作为它的第二、第三和第四个参数,stb_image.h将会用图像的宽度高度颜色通道的个数填充这三个变量。我们之后生成纹理的时候会用到的图像的宽度和高度的。

生成纹理

和之前生成的OpenGL对象一样,纹理也是使用ID引用的。让我们来创建一个:

unsigned int texture;
glGenTextures(1, &texture);

glGenTextures函数首先需要输入生成纹理的数量,然后把它们储存在第二个参数的unsigned int数组中(我们的例子中只是单独的一个unsigned int),就像其他对象一样,我们需要绑定它,让之后任何的纹理指令都可以配置当前绑定的纹理:

glBindTexture(GL_TEXTURE_2D, texture);

现在纹理已经绑定了,我们可以使用前面载入的图片数据生成一个纹理了。纹理可以通过glTexImage2D来生成:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);

函数很长,参数也不少,所以我们一个一个地讲解:

  • 第一个参数指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到GL_TEXTURE_1DGL_TEXTURE_3D的纹理不会受到影响)。
  • 第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别。
  • 第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有RGB值,因此我们也把纹理储存为RGB值。
  • 第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
  • 下个参数应该总是被设为0(历史遗留的问题)。
  • 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为char(byte)数组,我们将会传入对应值。
  • 最后一个参数是真正的图像数据。

当调用glTexImage2D时,当前绑定的纹理对象就会被附加上纹理图像。然而,目前只有基本级别(Base-level)的纹理图像被加载了,如果要使用多级渐远纹理,我们必须手动设置所有不同的图像(不断递增第二个参数)。或者,直接在生成纹理之后调用glGenerateMipmap。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。

生成了纹理和相应的多级渐远纹理后,释放图像的内存是一个很好的习惯。

stbi_image_free(data);

生成一个纹理的过程应该看起来像这样:

unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 为当前绑定的纹理对象设置环绕、过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);   
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载并生成纹理
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data)

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
    glGenerateMipmap(GL_TEXTURE_2D);

else

    std::cout << "Failed to load texture" << std::endl;

stbi_image_free(data);

应用纹理

后面的这部分我们会使用glDrawElements绘制[「你好,三角形」](…/04 Hello Triangle/)教程最后一部分的矩形。我们需要告知OpenGL如何采样纹理,所以我们必须使用纹理坐标更新顶点数据:

float vertices[] = 
//     ---- 位置 ----       ---- 颜色 ----     - 纹理坐标 -
     0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f,   // 右上
     0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 0.0f,   // 右下
    -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f,   // 左下
    -0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 1.0f    // 左上
;

以上是关于我的C/C++语言学习进阶之旅介绍一下NDK开发之C的简单易用图像库stb的主要内容,如果未能解决你的问题,请参考以下文章

我的C/C++语言学习进阶之旅NDK开发之Native层使用fopen打开Android设备上的文件

我的C/C++语言学习进阶之旅NDK开发之Native层使用fopen打开Android设备上的文件

我的C/C++语言学习进阶之旅NDK开发之解决错误:signal 5 (SIGTRAP), code 1 (TRAP_BRKPT), fault addr 0xXXX

我的C/C++语言学习进阶之旅NDK开发之解决错误:signal 5 (SIGTRAP), code 1 (TRAP_BRKPT), fault addr 0xXXX

我的C/C++语言学习进阶之旅JNI开发之转换C层返回的结构体为Java实体Bean

我的C/C++语言学习进阶之旅JNI开发之转换C层返回的结构体为Java实体Bean