SoftGLRender源码:(工具类)ImageUtils

ImageUtils

文件:ImageUtils.hImageUtils.cpp

图像工具类ImageUtils,主要提供读取图像、保存图像的功能. 实际实现中,对图像的操作,依赖于stb_image库.

ImageUtils声明

class ImageUtils {
public:
	static std::shared_ptr<Buffer<RGBA>> readImageRGBA(const std::string& path);
	static void writeImage(char const* file, int w, int h, int comp, 
		const void* data, int strideInBytes, bool flipY);

	static void convertFloatImage(RGBA* dst, float* src, uint32_t width, uint32_t height);
};
函数 描述
readImageRGBA(path) 从图片文件读取数据,并转换为 RGBA 格式
writeImage(...) 将图像数据保存为 PNG 文件
convertFloatImage(...) 将 float 灰度图(如深度图)转换为 RGBA 彩色图像

readImageRGBA 读取图片文件

readImageRGBA 负责从指定路径读取图片文件,根据通道数,将每个像素转换RGBA格式,并存放到std::shared<Buffer<RGBA>>管理的一块内存中.

Buffer<RGBA>是SoftGLRender定义的中间结构,用于存放图像像素数据(RGBA);一个Buffer<RGBA>缓存,对应一幅图像.

std::shared_ptr<Buffer<RGBA>> ImageUtils::readImageRGBA(const std::string& path) {
	int iw = 0, ih = 0, n = 0;
    // stbi_load() 加载图片,得到宽、高、通道数 n
	unsigned char* data = stbi_load(path.c_str(), &iw, &ih, &n, STBI_default);
	if (data == nullptr) {
		LOGD("ImageUtils::readImageRGBA failed, path: %s", path.c_str());
		return nullptr;
	}

	auto buffer = Buffer<RGBA>::makeDefault(iw, ih);

    // traverse every pixel, then convert to rgba by n
	for (size_t y = 0; y < ih; y++) {
		for (size_t x = 0; x < iw; x++) {
			auto& to = *buffer->get(x, y);
			size_t idx = x + y * iw;

			switch (n) {
			case STBI_grey: { // 1通道
				to.r = data[idx];
				to.g = to.b = to.r;
				to.a = 255;
				break;
			}
			case STBI_grey_alpha: { // 2通道
				to.r = data[idx * 2 + 0];
				to.g = to.b = to.r;
				to.a = data[idx * 2 + 1];
				break;
			}
			case STBI_rgb: { // 3通道
				to.r = data[idx * 3 + 0];
				to.g = data[idx * 3 + 1];
				to.b = data[idx * 3 + 2];
				to.a = 255;
				break;
			}
			case STBI_rgb_alpha: { // 4通道
				to.r = data[idx * 4 + 0];
				to.g = data[idx * 4 + 1];
				to.b = data[idx * 4 + 2];
				to.a = data[idx * 4 + 3];
				break;
			}
			default:
				break;
			}
		}
	}

	stbi_image_free(data); // 释放stbi_load加载的资源
	return buffer;
}

tips: RGBA类型本质是glm::u8vec4,即包含4个u8类型的glm::vec. i.e. RGBA每个通道占1byte,4个占4byte.

// GLMINc.h

using RGBA = glm::u8vec4;

STBI库支持的通道数:

STBI_grey         // 1通道,灰度
STBI_grey_alpha   // 2通道,灰度+透明度
STBI_rgb          // 3通道,RGB
STBI_rgb_alpha    // 4通道,RGBA

writeImage 写图片文件

writeImage 将图像数据保存为 PNG 图片.

// 将像素数据保存为 PNG 图片文件,依赖于STB图像库(stb_image_write.h)
// @param filename 输出文件路径(如"output.png")
// @param w 图像宽度(单位像素)
// @param h 图像高度(单位像素)
// @param comp 图像的通道数(如 3=RGB, 4=RGBA)
// @param data 像素数据指针(需连续存储)
// @param strideInBytes 每行像素的字节跨度(用于对齐)
// @param flipY 是否垂直翻转图像
void ImageUtils::writeImage(char const* filename, int w, int h, int comp, 
	const void* data, int strideInBytes, bool flipY) {
	stbi_flip_vertically_on_write(flipY); // 垂直翻转
	stbi_write_png(filename, w, h, comp, data, strideInBytes);
}

tips: stbi_write_png决定了保存的图像文件格式为png,如果想指定其他格式,可调用stbi_write_bmp, stbi_write_tga, stbi_write_hdr, stbi_write_jpg etc.

为什么要垂直翻转?

答:因为图形学中y坐标,通常垂直向上;而屏幕等设备y坐标,通常垂直向下.

参数strideInBytes什么含义?为什么需要该参数?

答:strideInBytes表示每行的字节数(而不是像素数),为了对齐要求.

  • 如果没有对齐需求
strideInBytes = w * comp

每行包含 w 个像素,每个像素 comp 个字节.

对于RGB图像:comp=3 -> 每个像素3byte;
对于RGBA图像:comp=4 -> 每个像素4byte

∴对于宽度=100的RGBA图像,strideInBytes=100*4=400byte

  • 如果有对齐需求

原本strideInBytes = 100 * 4 = 400,但为了对齐,向上取整到512byte.

i.e. 每行像素只用到前400byte,后面112byte是padding(填充).

convertFloatImage 转换float图像

convertFloatImage 将float格式图像(如深度图、灰度图)转换成RGBA图像.

原理:

  1. 遍历整个float图像,找出最大、最小值,归一化处理[0,1];
  2. 每个像素*255,映射到8 bit灰度值;
  3. 用该灰度值填充R、G、B(灰度图像),A=255.
// convert float image[width x height] to RGBA image[width x height]
// float image
void ImageUtils::convertFloatImage(RGBA* dst, float* src, uint32_t width, uint32_t height) {
	float* srcPixel = src;

    // 遍历float图像,找出最大、最小值
	float depthMin = FLT_MAX;
	float depthMax = FLT_MIN;
	for (int i = 0; i < width * height; i++) {
		float depth = *srcPixel;
		depthMin    = std::min(depthMin, depth);
		depthMax    = std::max(depthMax, depth);
		srcPixel++;
	}

	srcPixel = src;
	RGBA* dstPixel = dst;
	for (int i = 0; i < width * height; i++) {
		float depth = *srcPixel;
        // 归一化到[0, 1]
		depth       = (depth - depthMin) / (depthMax - depthMin);
        // float转8bit
		dstPixel->r = glm::clamp((int)(depth * 255.f), 0, 255);
		dstPixel->g = dstPixel->r; // 灰度图(R=G=B)
		dstPixel->b = dstPixel->r;
		dstPixel->a = 255; // 不透明

		srcPixel++;
		dstPixel++;
	}
}

float图像是什么?

答:float 图像指使用float类型(通常32bit浮点数)来表示图像中每个像素的通道值. 与常见的uint8_t(8bit 无符号整数,值域 [0, 255])图像不同,float图像的每个像素通道值是浮点数,通常范围在:

  • [0.0, 1.0] —— 归一化颜色或灰度值(常见于HDR图像、OpenGL处理)

  • 任意范围 —— 例如深度图、法线图、科学数据可视化等

stb image 库

STB Image库(简称STBI)是STB系列开源库之一,用于图像加载和处理. 是一个轻量级C库,可加载多种常见图像格式.

stbi_load

stbi_load 用于从图像文件(如 PNG、JPEG、BMP 等)加载像素数据到内存中.

原型如下,

#include <stb/stb_image.h>

// 返回指向图像像素数据的指针
unsigned char *stbi_load(
    const char *filename,   // 图像文件路径
    int *width,            // 返回图像的宽度(输出参数)
    int *height,           // 返回图像的高度(输出参数)
    int *channels,         // 返回图像的通道数(如 3=RGB, 4=RGBA)
    int desired_channels   // 期望的通道数(0=保持原样,1=灰度,3=RGB,4=RGBA)
);

函数 返回向图像像素数据的指针:

  • 每个像素包含 desired_channels 个字节(比如 3=RGB、4=RGBA)
  • 图像数据按行优先顺序排列
  • 成功时返回非空指针,失败返回 nullptr

注意:

1)加载后的数据需要手动释放,调用:

stbi_image_free(data);

2)stbi_load默认将图像原样读取(左上角是图像开头),如需翻转,可调用

stbi_set_flip_vertically_on_load(true);

支持格式

stbi_load 支持多种常见格式:

  • JPEG(基线 & 渐进式)
  • PNG(8/16 位)
  • BMP
  • GIF
  • PSD
  • TGA
  • HDR(高动态范围)
  • PIC
  • PNM(PPM/PGM/PBM)

错误处理

如果加载失败,stbi_load返回NULL,可用stbi_failure_reason()获取错误信息.

if (!data) {
    std::cerr << "Failed to load image: " << stbi_failure_reason() << std::endl;
}

读取图像文件示例

stbi_load 读取图像文件典型流程:

#define STB_IMAGE_IMPLEMENTATION // 使用stbi_load, 必须定义该宏
#include <stb/stb_image.h>

int width, height, channels;
unsigned char* data = stbi_load("image.png", &width, &height, &channels, STBI_rgb_alpha); // 强制输出RGBA

if (data) {
    // 使用 data,例如渲染到纹理中或保存为其他格式
    // 每像素 4 字节,data 是连续存储的 width * height * 4 字节数据

    stbi_image_free(data); // 记得释放
} else {
    std::cerr << "Failed to load image: " << stbi_failure_reason() << std::endl;
}

stbi_write_png

stbi_write_png用于将内存中的图像数据保存为 PNG 文件.

原型:

// @param filename PNG 输出文件名(如 "output.png")
// @param w 图像宽度
// @param h 图像高度
// @param comp 每像素的通道数,必须是 1=灰度、2=灰度+alpha、3=RGB、4=RGBA
// @param data 像素数据指针(连续内存),从左到右,从上到下
// @param stride_in_bytes 每行像素占据的字节数,通常是 w * comp。但如果你希望每行有 padding,可以加上
int stbi_write_png(
    char const* filename,  // 输出文件名
    int w,                 // 图像宽度(像素)
    int h,                 // 图像高度(像素)
    int comp,              // 每个像素的通道数(如 3=RGB,4=RGBA)
    const void* data,      // 图像数据指针
    int stride_in_bytes    // 每行字节数(行跨度),通常为 w * comp
);

成功返回 1;失败返回 0(文件无法写入或内存不足)

tips: 如果需要垂直翻转,同样可调用stbi_flip_vertically_on_write(true).

其他格式

stb_image_write.h 还支持其他格式:

函数 格式 示例
stbi_write_bmp BMP stbi_write_bmp("out.bmp", w, h, c, data)
stbi_write_jpg JPEG stbi_write_jpg("out.jpg", w, h, c, data, 90) (图像压缩质量=90)
stbi_write_tga TGA stbi_write_tga("out.tga", w, h, c, data)
stbi_write_hdr HDR(浮点) stbi_write_hdr("out.hdr", w, h, c, float_data)

写图像文件示例

#define STB_IMAGE_WRITE_IMPLEMENTATION // 使用stbi_write_png必须定义该宏
#include <stb/stb_image_write.h>

int width  = 256;
int height = 256;
int comp   = 3; // RGB
std::vector<unsigned char> image(width * height * comp);

// 填充一些颜色数据(简单的红色渐变)
for (int y = 0; y < height; ++y) {
    for (int x = 0; x < width; ++x) {
        int index = (y * width + x) * comp;
        image[index + 0] = x;   // R
        image[index + 1] = y;   // G
        image[index + 2] = 128; // B
    }
}

// 保存为 PNG
stbi_write_png("output.png", width, height, comp, image.data(), width * comp);
posted @ 2025-05-18 16:54  明明1109  阅读(83)  评论(0)    收藏  举报