HDR格式图像的保存与读取+实例

一:   High-Dynamic Range,简称HDR

高动态范围图像(High-Dynamic Range,简称HDR),相比普通的图像,可以提供更多的动态范围和图像细节,根据不同的曝光时间的LDR(Low-Dynamic Range)图像,利用每个曝光时间相对应最佳细节的LDR图像来合成最终HDR图像,能够更好的反映出真实环境中的视觉效果

 HDRI文件是一种文件,扩展名是hdrtif格式,有足够的能力保存光照信息,但不一定是全景图。Dynamic Range(动态范围)是指一个场景的最亮和最暗部分之间的相对比值。一张HDR图片,它记录了远远超出256个级别的实际场景的亮度值,超出的部分在屏幕上是显示不出来的。它比其它格式的图像有着更大亮度的数据贮存,而且它记录亮度的方式与传统的图片不同,不是用非线性的方式将亮度信息压缩到8bit16bit的颜色空间内,而是用直接对应的方式记录亮度信息,它可以说记录了图片环境中的照明信息,因此我们可以使用这种图象来照亮场景。

    图象数据采用浮点数据 。我们可以采用浮点方式来处理和存放亮度数据,抛弃不准确的整数数据;同时计算机在引入浮点数据来存储象素的各个参数并且在运算的全过程都使用浮点数据,这样就可以有效的提高据的精确度。


2HDRI文件格式介绍(OpenEXRRadianceRGBEFloatTIFF)

HDRIHigh-DynamicRange Image)就是记录采用了HDR技术的图象数据文件。常用的HDRI文件有OpenEXRRadianceRGBEFloatTIFF三种格式。

2.1 Radiance RGBE文件格式

RGBE文件的扩展名为.hdrRGBE正式名称为RadianceRGBE格式。这个本来是BRFR等作为radiance材质的一种格式,也叫做radiancemap,后来成为流行的一种HDR格式。所谓E,就是指数。RadianceRGBE文件每个通道为8bitBYTE数据类型,4个通道一共是32bitRGBE可以使用RLE压缩编码压缩,也可以不压缩。由文件头、RGBE数据组成。

文件头如下:

类型输出格式

char programtype[16]; //#?Radiance/n#Generated by still/n

float gamma; //1.0

float exposure; //1.0

字符串常量//FORMAT=32-bit_rle_rgbe/n/n

int nWidth, int nHeight //-Y nHeight +X nWidth/n

RGBE数据与HDRFP32(RGB)相互转换公式如下:

1rgbe->FP32(RGB)       (读操作)

如果e0R = G= B = 0.0,否则:

R = r * 2^(e 128 - 8);

G = g * 2^(e 128 - 8);

B = b * 2^(e 128 - 8);

 

2FP32(RGB)-> rgbe    (写操作)

v = max(R, G, B);

v用科学计算法表示成v = m * 2 ^ n ( 0 < m < 1)

r = R * m * 256.0/v;

g = G * m * 256.0/v;

b = B * m * 256.0/v;

e = n + 128;

需要注意的是:

我们一般说HDR采用FP32,指的是HDR图象运算时候的内存数据类型,而RadianceRGBE文件采用8bitBYTE类型存储HDR数据。也就是说打开RadianceRGBE文件,要使用上面的公式1RadianceRGBE文件的8bitBYTE文件数据转换为FP32HDR内存数据进行运算;保存为RadianceRGBE文件时,要使用上面的公式2HDRFP32内存数据转换为RadianceRGBE8bitBYTE文件数据进行保存。同理,OpenEXR文件的读写也存在将其FP16的文件数据到HDRFP32图象数据的转换;而下面将要讲的FloatTiff是不需要进行数据转换,直接将HDRFP 。

读写类链接地址:http://www.graphics.cornell.edu/~bjw/rgbe.html

读写库.h文件

#pragma once
#ifndef _H_RGBE
#define _H_RGBE
/* THIS CODE CARRIES NO GUARANTEE OF USABILITY OR FITNESS FOR ANY PURPOSE.
* WHILE THE AUTHORS HAVE TRIED TO ENSURE THE PROGRAM WORKS CORRECTLY,
* IT IS STRICTLY USE AT YOUR OWN RISK.  */

/* utility for reading and writing Ward's rgbe image format.
See rgbe.txt file for more details.
*/

#include <stdio.h>

typedef struct 
{
	int valid;            /* indicate which fields are valid */
	char programtype[16]; /* listed at beginning of file to identify it
						  * after "#?".  defaults to "RGBE" */
	float gamma;          /* image has already been gamma corrected with
						  * given gamma.  defaults to 1.0 (no correction) */
	float exposure;       /* a value of 1.0 in an image corresponds to
						  * <exposure> watts/steradian/m^2.
						  * defaults to 1.0 */
} rgbe_header_info;

/* flags indicating which fields in an rgbe_header_info are valid */
#define RGBE_VALID_PROGRAMTYPE 0x01
#define RGBE_VALID_GAMMA       0x02
#define RGBE_VALID_EXPOSURE    0x04

/* return codes for rgbe routines */
#define RGBE_RETURN_SUCCESS 0
#define RGBE_RETURN_FAILURE -1

/* read or write headers */
/* you may set rgbe_header_info to null if you want to */
int RGBE_WriteHeader(FILE *fp, int width, int height, rgbe_header_info *info);
int RGBE_ReadHeader(FILE *fp, int *width, int *height, rgbe_header_info *info);

/* read or write pixels */
/* can read or write pixels in chunks of any size including single pixels*/
int RGBE_WritePixels(FILE *fp, float *data, int numpixels);
int RGBE_ReadPixels(FILE *fp, float *data, int numpixels);

/* read or write run length encoded files */
/* must be called to read or write whole scanlines */
int RGBE_WritePixels_RLE(FILE *fp, float *data, int scanline_width,
	int num_scanlines);
int RGBE_ReadPixels_RLE(FILE *fp, float *data, int scanline_width,
	int num_scanlines);

#endif /* _H_RGBE */

读写库.cpp文件

/* THIS CODE CARRIES NO GUARANTEE OF USABILITY OR FITNESS FOR ANY PURPOSE.
* WHILE THE AUTHORS HAVE TRIED TO ENSURE THE PROGRAM WORKS CORRECTLY,
* IT IS STRICTLY USE AT YOUR OWN RISK.  */

#include "rgbe.h"
#include <math.h>
#include <malloc.h>
#include <string.h>
#include <ctype.h>

/* This file contains code to read and write four byte rgbe file format
developed by Greg Ward.  It handles the conversions between rgbe and
pixels consisting of floats.  The data is assumed to be an array of floats.
By default there are three floats per pixel in the order red, green, blue.
(RGBE_DATA_??? values control this.)  Only the mimimal header reading and
writing is implemented.  Each routine does error checking and will return
a status value as defined below.  This code is intended as a skeleton so
feel free to modify it to suit your needs.

(Place notice here if you modified the code.)
posted to http://www.graphics.cornell.edu/~bjw/
written by Bruce Walter  (bjw@graphics.cornell.edu)  5/26/95
based on code written by Greg Ward
*/

#ifdef _CPLUSPLUS
/* define if your compiler understands inline commands */
#define INLINE inline
#else
#define INLINE
#endif

/* offsets to red, green, and blue components in a data (float) pixel */
#define RGBE_DATA_RED    0
#define RGBE_DATA_GREEN  1
#define RGBE_DATA_BLUE   2
/* number of floats per pixel */
#define RGBE_DATA_SIZE   3

enum rgbe_error_codes {
	rgbe_read_error,
	rgbe_write_error,
	rgbe_format_error,
	rgbe_memory_error,
};

/* default error routine.  change this to change error handling */
static int rgbe_error(int rgbe_error_code, char *msg)
{
	switch (rgbe_error_code) {
	case rgbe_read_error:
		perror("RGBE read error");
		break;
	case rgbe_write_error:
		perror("RGBE write error");
		break;
	case rgbe_format_error:
		fprintf(stderr, "RGBE bad file format: %s\n", msg);
		break;
	default:
	case rgbe_memory_error:
		fprintf(stderr, "RGBE error: %s\n", msg);
	}
	return RGBE_RETURN_FAILURE;
}

/* standard conversion from float pixels to rgbe pixels */
/* note: you can remove the "inline"s if your compiler complains about it */
static INLINE void float2rgbe(unsigned char rgbe[4], float red, float green, float blue)
{
	float v;
	int e;

	v = red;
	if (green > v) 
		v = green;
	if (blue > v) 
		v = blue;
	if (v < 1e-32) 
	{
		rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0;
	}
	else 
	{
		v = frexp(v, &e) * 256.0 / v;
		rgbe[0] = (unsigned char)(red * v);
		rgbe[1] = (unsigned char)(green * v);
		rgbe[2] = (unsigned char)(blue * v);
		rgbe[3] = (unsigned char)(e + 128);
	}
}

/* standard conversion from rgbe to float pixels */
/* note: Ward uses ldexp(col+0.5,exp-(128+8)).  However we wanted pixels */
/*       in the range [0,1] to map back into the range [0,1].            */
static INLINE void rgbe2float(float *red, float *green, float *blue, unsigned char rgbe[4])
{
	float f;

	if (rgbe[3]) 
	{   /*nonzero pixel*/
		f = ldexp(1.0, rgbe[3] - (int)(128 + 8));
		*red = rgbe[0] * f;
		*green = rgbe[1] * f;
		*blue = rgbe[2] * f;
	}
	else
		*red = *green = *blue = 0.0;
}

/* default minimal header. modify if you want more information in header */
int RGBE_WriteHeader(FILE *fp, int width, int height, rgbe_header_info *info)
{
	char *programtype = "RGBE";

	if (info && (info->valid & RGBE_VALID_PROGRAMTYPE))
		programtype = info->programtype;
	if (fprintf(fp, "#?%s\n", programtype) < 0)
		return rgbe_error(rgbe_write_error, NULL);
	/* The #? is to identify file type, the programtype is optional. */
	if (info && (info->valid & RGBE_VALID_GAMMA)) 
	{
		if (fprintf(fp, "GAMMA=%g\n", info->gamma) < 0)
			return rgbe_error(rgbe_write_error, NULL);
	}
	if (info && (info->valid & RGBE_VALID_EXPOSURE))
	{
		if (fprintf(fp, "EXPOSURE=%g\n", info->exposure) < 0)
			return rgbe_error(rgbe_write_error, NULL);
	}

	if (fprintf(fp, "FORMAT=32-bit_rle_rgbe\n\n") < 0)
		return rgbe_error(rgbe_write_error, NULL);
	if (fprintf(fp, "-Y %d +X %d\n", height, width) < 0)
		return rgbe_error(rgbe_write_error, NULL);
	return RGBE_RETURN_SUCCESS;
}

/* minimal header reading.  modify if you want to parse more information */
int RGBE_ReadHeader(FILE *fp, int *width, int *height, rgbe_header_info *info)
{
	char buf[128];
	int found_format;
	float tempf;
	int i;

	found_format = 0;
	if (info) 
	{
		info->valid = 0;
		info->programtype[0] = 0;
		info->gamma = info->exposure = 1.0;
	}
	if (fgets(buf, sizeof(buf) / sizeof(buf[0]), fp) == NULL)
		return rgbe_error(rgbe_read_error, NULL);
	if ((buf[0] != '#') || (buf[1] != '?'))
	{
		/* if you want to require the magic token then uncomment the next line */
		/*return rgbe_error(rgbe_format_error,"bad initial token"); */
	}
	else if (info) 
	{
		info->valid |= RGBE_VALID_PROGRAMTYPE;
		for (i = 0; i<sizeof(info->programtype) - 1; i++) 
		{
			if ((buf[i + 2] == 0) || isspace(buf[i + 2]))
				break;
			info->programtype[i] = buf[i + 2];
		}
		info->programtype[i] = 0;
		if (fgets(buf, sizeof(buf) / sizeof(buf[0]), fp) == 0)
			return rgbe_error(rgbe_read_error, NULL);
	}
	for (;;) 
	{
		if ((buf[0] == 0) || (buf[0] == '\n'))
			return rgbe_error(rgbe_format_error, "no FORMAT specifier found");
		else if (strcmp(buf, "FORMAT=32-bit_rle_rgbe\n") == 0)
			break;       /* format found so break out of loop */
		else if (info && (sscanf(buf, "GAMMA=%g", &tempf) == 1))
		{
			info->gamma = tempf;
			info->valid |= RGBE_VALID_GAMMA;
		}
		else if (info && (sscanf(buf, "EXPOSURE=%g", &tempf) == 1))
		{
			info->exposure = tempf;
			info->valid |= RGBE_VALID_EXPOSURE;
		}
		if (fgets(buf, sizeof(buf) / sizeof(buf[0]), fp) == 0)
			return rgbe_error(rgbe_read_error, NULL);
	}
	if (fgets(buf, sizeof(buf) / sizeof(buf[0]), fp) == 0)
		return rgbe_error(rgbe_read_error, NULL);
	if (strcmp(buf, "\n") != 0)
		return rgbe_error(rgbe_format_error,
			"missing blank line after FORMAT specifier");
	if (fgets(buf, sizeof(buf) / sizeof(buf[0]), fp) == 0)
		return rgbe_error(rgbe_read_error, NULL);
	if (sscanf(buf, "-Y %d +X %d", height, width) < 2)
		return rgbe_error(rgbe_format_error, "missing image size specifier");
	return RGBE_RETURN_SUCCESS;
}

/* simple write routine that does not use run length encoding */
/* These routines can be made faster by allocating a larger buffer and
fread-ing and fwrite-ing the data in larger chunks */
int RGBE_WritePixels(FILE *fp, float *data, int numpixels)
{
	unsigned char rgbe[4];

	while (numpixels-- > 0) 
	{
		float2rgbe(rgbe, data[RGBE_DATA_RED],
			data[RGBE_DATA_GREEN], data[RGBE_DATA_BLUE]);
		data += RGBE_DATA_SIZE;
		if (fwrite(rgbe, sizeof(rgbe), 1, fp) < 1)
			return rgbe_error(rgbe_write_error, NULL);
	}
	return RGBE_RETURN_SUCCESS;
}

/* simple read routine.  will not correctly handle run length encoding */
int RGBE_ReadPixels(FILE *fp, float *data, int numpixels)
{
	unsigned char rgbe[4];

	while (numpixels-- > 0) 
	{
		if (fread(rgbe, sizeof(rgbe), 1, fp) < 1)
			return rgbe_error(rgbe_read_error, NULL);
		rgbe2float(&data[RGBE_DATA_RED], &data[RGBE_DATA_GREEN],
			&data[RGBE_DATA_BLUE], rgbe);
		data += RGBE_DATA_SIZE;
	}
	return RGBE_RETURN_SUCCESS;
}

/* The code below is only needed for the run-length encoded files. */
/* Run length encoding adds considerable complexity but does */
/* save some space.  For each scanline, each channel (r,g,b,e) is */
/* encoded separately for better compression. */

static int RGBE_WriteBytes_RLE(FILE *fp, unsigned char *data, int numbytes)
{
#define MINRUNLENGTH 4
	int cur, beg_run, run_count, old_run_count, nonrun_count;
	unsigned char buf[2];

	cur = 0;
	while (cur < numbytes) {
		beg_run = cur;
		/* find next run of length at least 4 if one exists */
		run_count = old_run_count = 0;
		while ((run_count < MINRUNLENGTH) && (beg_run < numbytes)) {
			beg_run += run_count;
			old_run_count = run_count;
			run_count = 1;
			while ((beg_run + run_count < numbytes) && (run_count < 127)
				&& (data[beg_run] == data[beg_run + run_count]))
				run_count++;
		}
		/* if data before next big run is a short run then write it as such */
		if ((old_run_count > 1) && (old_run_count == beg_run - cur)) {
			buf[0] = 128 + old_run_count;   /*write short run*/
			buf[1] = data[cur];
			if (fwrite(buf, sizeof(buf[0]) * 2, 1, fp) < 1)
				return rgbe_error(rgbe_write_error, NULL);
			cur = beg_run;
		}
		/* write out bytes until we reach the start of the next run */
		while (cur < beg_run) {
			nonrun_count = beg_run - cur;
			if (nonrun_count > 128)
				nonrun_count = 128;
			buf[0] = nonrun_count;
			if (fwrite(buf, sizeof(buf[0]), 1, fp) < 1)
				return rgbe_error(rgbe_write_error, NULL);
			if (fwrite(&data[cur], sizeof(data[0])*nonrun_count, 1, fp) < 1)
				return rgbe_error(rgbe_write_error, NULL);
			cur += nonrun_count;
		}
		/* write out next run if one was found */
		if (run_count >= MINRUNLENGTH) {
			buf[0] = 128 + run_count;
			buf[1] = data[beg_run];
			if (fwrite(buf, sizeof(buf[0]) * 2, 1, fp) < 1)
				return rgbe_error(rgbe_write_error, NULL);
			cur += run_count;
		}
	}
	return RGBE_RETURN_SUCCESS;
#undef MINRUNLENGTH
}

int RGBE_WritePixels_RLE(FILE *fp, float *data, int scanline_width,
	int num_scanlines)
{
	unsigned char rgbe[4];
	unsigned char *buffer;
	int i, err;

	if ((scanline_width < 8) || (scanline_width > 0x7fff))
		/* run length encoding is not allowed so write flat*/
		return RGBE_WritePixels(fp, data, scanline_width*num_scanlines);
	buffer = (unsigned char *)malloc(sizeof(unsigned char) * 4 * scanline_width);
	if (buffer == NULL)
		/* no buffer space so write flat */
		return RGBE_WritePixels(fp, data, scanline_width*num_scanlines);
	while (num_scanlines-- > 0) {
		rgbe[0] = 2;
		rgbe[1] = 2;
		rgbe[2] = scanline_width >> 8;
		rgbe[3] = scanline_width & 0xFF;
		if (fwrite(rgbe, sizeof(rgbe), 1, fp) < 1) {
			free(buffer);
			return rgbe_error(rgbe_write_error, NULL);
		}
		for (i = 0; i<scanline_width; i++) {
			float2rgbe(rgbe, data[RGBE_DATA_RED],
				data[RGBE_DATA_GREEN], data[RGBE_DATA_BLUE]);
			buffer[i] = rgbe[0];
			buffer[i + scanline_width] = rgbe[1];
			buffer[i + 2 * scanline_width] = rgbe[2];
			buffer[i + 3 * scanline_width] = rgbe[3];
			data += RGBE_DATA_SIZE;
		}
		/* write out each of the four channels separately run length encoded */
		/* first red, then green, then blue, then exponent */
		for (i = 0; i<4; i++) {
			if ((err = RGBE_WriteBytes_RLE(fp, &buffer[i*scanline_width],
				scanline_width)) != RGBE_RETURN_SUCCESS) {
				free(buffer);
				return err;
			}
		}
	}
	free(buffer);
	return RGBE_RETURN_SUCCESS;
}

int RGBE_ReadPixels_RLE(FILE *fp, float *data, int scanline_width,
	int num_scanlines)
{
	unsigned char rgbe[4], *scanline_buffer, *ptr, *ptr_end;
	int i, count;
	unsigned char buf[2];

	if ((scanline_width < 8) || (scanline_width > 0x7fff))
		/* run length encoding is not allowed so read flat*/
		return RGBE_ReadPixels(fp, data, scanline_width*num_scanlines);
	scanline_buffer = NULL;
	/* read in each successive scanline */
	while (num_scanlines > 0) {
		if (fread(rgbe, sizeof(rgbe), 1, fp) < 1) {
			free(scanline_buffer);
			return rgbe_error(rgbe_read_error, NULL);
		}
		if ((rgbe[0] != 2) || (rgbe[1] != 2) || (rgbe[2] & 0x80)) {
			/* this file is not run length encoded */
			rgbe2float(&data[0], &data[1], &data[2], rgbe);
			data += RGBE_DATA_SIZE;
			free(scanline_buffer);
			return RGBE_ReadPixels(fp, data, scanline_width*num_scanlines - 1);
		}
		if ((((int)rgbe[2]) << 8 | rgbe[3]) != scanline_width) {
			free(scanline_buffer);
			return rgbe_error(rgbe_format_error, "wrong scanline width");
		}
		if (scanline_buffer == NULL)
			scanline_buffer = (unsigned char *)
			malloc(sizeof(unsigned char) * 4 * scanline_width);
		if (scanline_buffer == NULL)
			return rgbe_error(rgbe_memory_error, "unable to allocate buffer space");

		ptr = &scanline_buffer[0];
		/* read each of the four channels for the scanline into the buffer */
		for (i = 0; i<4; i++) {
			ptr_end = &scanline_buffer[(i + 1)*scanline_width];
			while (ptr < ptr_end) {
				if (fread(buf, sizeof(buf[0]) * 2, 1, fp) < 1) {
					free(scanline_buffer);
					return rgbe_error(rgbe_read_error, NULL);
				}
				if (buf[0] > 128) {
					/* a run of the same value */
					count = buf[0] - 128;
					if ((count == 0) || (count > ptr_end - ptr)) {
						free(scanline_buffer);
						return rgbe_error(rgbe_format_error, "bad scanline data");
					}
					while (count-- > 0)
						*ptr++ = buf[1];
				}
				else {
					/* a non-run */
					count = buf[0];
					if ((count == 0) || (count > ptr_end - ptr)) {
						free(scanline_buffer);
						return rgbe_error(rgbe_format_error, "bad scanline data");
					}
					*ptr++ = buf[1];
					if (--count > 0) {
						if (fread(ptr, sizeof(*ptr)*count, 1, fp) < 1) {
							free(scanline_buffer);
							return rgbe_error(rgbe_read_error, NULL);
						}
						ptr += count;
					}
				}
			}
		}
		/* now convert data from buffer into floats */
		for (i = 0; i<scanline_width; i++) {
			rgbe[0] = scanline_buffer[i];
			rgbe[1] = scanline_buffer[i + scanline_width];
			rgbe[2] = scanline_buffer[i + 2 * scanline_width];
			rgbe[3] = scanline_buffer[i + 3 * scanline_width];
			rgbe2float(&data[RGBE_DATA_RED], &data[RGBE_DATA_GREEN],
				&data[RGBE_DATA_BLUE], rgbe);
			data += RGBE_DATA_SIZE;
		}
		num_scanlines--;
	}
	free(scanline_buffer);
	return RGBE_RETURN_SUCCESS;
}

把raw数据保存为hdr实例:

//raw 数据保存为 RGBE
void Raw2RGBE(const char filename[], int width = 1024, int height = 1024)
{
	size_t size = width;
	size *= height;
	unsigned short *data = new unsigned short[size];
	if (data == NULL)
	{
		std::cout << "you are wrong in malloc space" << std::endl;
		return;
	}
	memset(data, 0, size*sizeof(unsigned short));

	FILE* file;
	if (fopen_s(&file, filename, "rb+"))
	{
		std::cout << "you are wrong in file open" << std::endl;
		delete[] data;
		data = NULL;
		return;
	}
	fread(data, sizeof(unsigned short), size, file);
	fclose(file);

	cv::Mat channel0(height, width, CV_16UC1, data);
	cv::Mat channel[3];
	channel0.copyTo(channel[0]);
	channel0.copyTo(channel[1]);
	channel0.copyTo(channel[2]);
	cv::Mat temp(height, width, CV_16UC3);
	cv::merge(channel, 3, temp);
	cv::Mat tranfer(height, width, CV_32FC3);
	temp.convertTo(tranfer, CV_32FC3);

	//write data
	char filePath[]{ "D:\\testraw.hdr" };
	FILE* file1;
	if (fopen_s(&file1, filePath, "wb+"))
	{
		std::cout << "you are wrong in file open" << std::endl;
		delete[] data;
		data = NULL;
		return;
	}
	rgbe_header_info info;
	memset(&info, 0, sizeof(rgbe_header_info));
	info.valid = 1;
	strcpy_s(info.programtype, "RADIANCE");

	RGBE_WriteHeader(file1, width, height, &info);
	float* wdata = new float[size * 3];
	if (wdata==NULL)
	{
		std::cout << "you are wrong in malloc space for write" << std::endl;
		delete[] data;
		data = NULL;
		return;
	}
	memset(wdata, 0, size * 3 * sizeof(float));

	size_t position = 0;
	int nc = width * tranfer.channels();
	for (int i = 0; i < height; i++)
	{
		float* data = tranfer.ptr<float>(i);
		for (int j = 0; j < nc; j++)
		{
			wdata[position + j] = data[j];
		}
		position += nc;
	}
	if (RGBE_WritePixels(file1, wdata, size))
	{
	std::cout << "write wrong" << std::endl;
	}
	fclose(file1);
	delete[] data;
	data = NULL;
	delete[] wdata;
	wdata = NULL;
	return;
}


  • 2
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值