SoftGLRender源码:(工具类)ImageUtils
ImageUtils
文件:ImageUtils.h
,ImageUtils.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=400
byte
- 如果有对齐需求
原本strideInBytes = 100 * 4 = 400
,但为了对齐,向上取整到512byte.
i.e. 每行像素只用到前400byte,后面112byte是padding(填充).
convertFloatImage 转换float图像
convertFloatImage
将float格式图像(如深度图、灰度图)转换成RGBA图像.
原理:
- 遍历整个float图像,找出最大、最小值,归一化处理[0,1];
- 每个像素*255,映射到8 bit灰度值;
- 用该灰度值填充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);