华为CANN训练营笔记——应用开发全流程 [5](with 代码版)

10 篇文章 5 订阅

2. 数据预处理

第二部分——数据预处理

2.1 数据预处理概述

数据预处理提供两个接口AIPP和DVPP。AIPP前文已经介绍过,这部分主要介绍DVPP(数字视觉与处理)

下文**“数字视觉与处理”**专指DVPP。

DVPP提供了五个功能模块,视频编码(VENC)、视频解码(VDEC)、JPEG图像编码(JPEGE)、JPEG图像解码(JPEGD)、视觉与处理模块(VPC)。VPC模块还提供了如格式转换,图像缩放,裁剪等其他功能

  • VPC:抠图、缩放、叠加、拼接、格式转换
  • JPEGD:解码为YUV格式
  • JPEGE:将YUV编码为.jpg
  • VDEC:视频解码
  • VENC:视频编码

与数据预处理相关的内存必须通过acldvppMalloc接口申请,通过acldvppFree接口释放。除了acldvppMalloc和acldvppFree,数字视觉预处理(DVPP)其他接口只能再HOST上调用,不能在Device上调用。

2.2 数字视觉预处理共用接口

  • 整体调用流程:创建通道描述——创建通道——创建图片描述——执行操作——销毁图片描述——创建图片描述——执行操作——销毁图片描述——销毁通道——销毁通道描述

1. dvpp内存的申请与释放

acldvppMalloc接口负责分配内存给Device侧媒体数据预处理时使用,申请后必须用acldvppFree释放(不能delete),内存地址32对齐。频繁调用acldvppMalloc与acldvppFree对,对造成性能损耗。

aclError acldvppMalloc(void **devPtr, size_t_size)
// devPtr, 输出:Device上已分配内存的指针的指针
// size, 输入: 申请内存的大小

aclError acldvppFree(void *devPtr)
// devPtr, 输入:待释放内存的指针

若用户使用acldvppMalloc接口申请大块内存并自行划分、管理内存时,用户在管理内存时,需按每张图片的实际数据大小向上对齐成32整数倍+32字节(ALIGN_UP[len]+32字节)来管理内存。如管理n张图片内存,每张大小为X个字节,则实际应按 n × ( ALIGN_UP(X) + 32字节)大小管理内存。每张图片地址按 ALIGN_UP(X) + 32字节偏移。

  • ALIGN_UP = ((len - 1)/32 + 1) × 32

2. 通道&通道描述创建与销毁

创建通道描述 -> 创建通道 -> 销毁通道 -> 销毁通道描述信息

// 通道描述创建与销毁
acldvppChannelDesc *acldvppCreateChannelDesc()
// 函数功能:创建acldvppChannelDesc类型数据,表示创建图片数据处理通道时的通道描述信息,为同步接口
// 返回acldvppChannelDesc类型表示成功;失败返回 null

aclError acldvppDestroyChannelDesc(acldvppChannelDesc *channelDesc)
// 必须调用acldvppDestroyChannel(销毁通道)后,再调用acldvppDestroyChannelDesc(销毁通道信息),否则报错
// channelDesc输入

// ”通道“创建与销毁
aclError acldvppCreateChannel(acldvppChannelDesc* channelDesc)
// 函数功能:创建通道,同一通道可以反复使用,销毁后不可再使用
//  约束:通道为非线程安全,即不同线程要求创建不同通道
// channelDesc,输入与输出:指定通道描述信息

aclError acldvppDestroyChannel(acldvppChannel *channel)

3. 创建与销毁描述图片描述信息

acldvppPicDesc* acldvppCreatePicDesc()
// 创建图片描述信息,同步接口
// 返回acldvppPicDesc类型数据表示成功,返回null则失败

aclError acldvppDestroyPicDesc(acldvppPicDesc* picDesc)
// 销毁由acldvppCreatePicDesc创建的图片描述信息 

4. acldvppSetPicDesc系列接口——设置图片描述

在这里插入图片描述
在这里插入图片描述

2.3 JPEG解码

  • 像素对齐
    针对不同的编码格式,解码后输出不同格式的图片,如YUV444SP,YUV422SP,YUV420SP。解码对输出图片有对齐要求:宽128,高16.即,假如原始输出宽高为(129,17)应对齐为(256,32)

2.3.1 解码约束

  • 输入约束:
    • 最大分辨率:8192×8192, 最小分辨率:32×32
    • 只支持哈夫曼编码,码流色域为YUV,下采样码流为444,422,420,400
    • 不支持渐进JPEG
    • 不支持JPEG2000
  • 输出要求:
    • 宽对齐128,高对齐16(像素对齐)
  • 内存大小:内存大小与输出图片数据格式相关
    • YUV420SP: widthStride × heightStride × 3 / 2
    • YUV422SP: widthStride × heightStride × 2
    • YUV444SP: widthStride × heightStride × 3

DVPP对输出的图片宽高有对齐要求:宽128,高16(指像素对齐而不是内存对齐)

2.3.2 解码流程图

在这里插入图片描述

2.3.3 图片解码接口

1. 解码接口
aclError acldvppJpegDecodeAsync(
	acldvppChannelDesc *channelDesc, // 输入、通道描述,与调用acldvppCreateChannel时传入的channelDesc一致
	const void *data,  // 输入图片的内存地址
	uint32_t size, // 输入图片的实际数据大小,单位Byte
	acldvppPicDesc* outputDesc, // 输出图片信息。
	aclrtStream stream // 指定此接口绑定的stream
)

// 关于outputDesc
// 1. 调用acldvppCreatePicDesc接口,创建图片描述信息
// 2. JPEG原图的高宽,可以通过acldvppJpegGetImageInfo获取
// 3. 输出图片的内存大小可提前调用acldvppJpegPredictDecSize接口获取
// 4. 输出图片格式支持设置成jpeg原图编码格式或NV12或NV21格式
2. 读取图像宽高及通道数
aclError acldvppJpegGetImageInfo(
	const void* data, // 图像内存地址
	uint32_t size,  // 实际数据大小,单位Byte
	uint32_t *width, 
	uint32_t *height,
	int32_t *components // 通道个数
)
3. 预测输出内存

自动计算输出占用内存

aclError acldvppJpegPredictDecSize(
	const void* data, // 图像内存地址
	uint32_t size,  // 实际数据大小,单位Byte
	acldvppPixelFormat outputPixelFormat,  // 解码后输出图片的格式
	uint32_t *decsize // 返回JPEG图片解码后所需输出内存的大小
)

2.3.4 图像解码Demo

头文件 #include "acl/ops/acl_dvpp.h"

1. jpegd.h
# pragma once
# include "acl/acl.h"
# include <iostream>
# include <cstdint>
// DVPP头文件
# include "acl/ops/acl_dvpp.h"

# define INFO_LOG(fmt, args...) fprintf(stdout, "[INFO]" fmt "\n", ##args)
# define WARN_LOG(fmt, args...) fprintf(stdout, "[WARN] " fmt "\n", ##args)
# define ERROR_LOG(fmt, args...) fprintf(stdout, "[ERROR] " fmt "\n", ##args)

typedef enum Result {
	SUCCESS = 0 ,
	FAILED = 1
}Result;

typedef struct PicDesc {
	std::string picName;
	int width;
	int height;
}PicDesc;

// 根据图片描述信息获取Device内存
static void* GetDeviceBufferOfPicture(const PicDesc &picDesc, uint32_t &devPicBufferSize);
// 从硬盘取文件
static char* ReadBinFile(std:: string fileName, uint32_t &fileSize);
// 保存dvpp输出结果至文件
static Result SaveDvppOutputData(const char *fileName,const void *devPtr, uint32_t datasize);
// 设置输入
void SetInput(void *inDevBuffer, int inDevBufferSize, int inputWidth, int inputHeight);
// 获取输出
void GetOutput(void **outputBuffer, int &outputSize);

void DestroyResource();
void DestroyDecodeResource();

int32_t deviceId_ = 0;
aclrtContext context_= nullptr;
aclrtStream stream_=nullptr;
acldvppChannelDesc *dvppChannelDesc_; // 频道描述信息

void* decodeOutDevBuffer_; // 解码输出内存地址
acldvppPicDesc *decodeOutputDesc_; // 解码输出描述信息
uint32_t decodeDataSize_; // 解码输出内存大小

void *inDevBuffer_; // 解码输入内存地址
uint32_t inDevBuffersize_; // 解码输入内存大小
uint32_t inputwidth_; 
uint32_t inputHeight_;
2. jpegd.cpp
# include "acl/acl.h"
# include <iostream>
# include <jpegd.h>
# include <fstream>
# include <cstring>
# include <sys/stat.h>
# include "acl/ops/acl_dvpp.h"
using namespace std;

void CreateResource(); // 申请运行管理资源
void CreateDecodeResource(); // 创建图像数据处理通道

int main(){
	// ACL初始化
	const char* aclConfigPath = "../src/acl.json";
	aclInit(aclConfigPath);
	INFO_LOG("acl init success");

	// 申请运行管理资源
	CreateResource();
	// 创建图像数据处理通道
	CreateDecodeResource();
	
	// 预置的图片数据
	std::string dvppOutputfileName = "./dvpp_output_";
	PicDesc testPic[]= {
		{"../data/dog1_1024_683.jpg", 1024, 683} ,
		{"../data/dog2_1024_683.jpg", 1024, 683} ,
	};
	
	// 开始循环处理图片
	for (size_t index = 0; index < sizeof(testPic) / sizeof(testPic[0]);++index) {
		INFO_LOG ("start to process picture:%s",testPic[index], picName.c_str());
		// 将图片读入内存并复制到dvppMalloc申请的内存中
		uint32_t devPicBufferSize;
		void *picDevBuffer = GetDeviceBufferOfPicture(testPic[index], devPicBufferSize);
		if (picDevBuffer == nullptr){
			ERROR_LOG ("get pic device buffer failed , index is 8zu", index);
			return FAILED ;
		}
		SetInput(picDevBuffer, devPicBufferSize, testPic[index].width, testPic[index].height);

		// 计算解码输出图片对齐后的宽高
		uint32_t decodeOutWidthStride = (inputWidth_ + 127) / 128 * 128;
		uint32_t decodeOutHeightStride = (inputHeight_ + 15) / 16 * 16;
		
		// 申请解码输出内存
		aclError ret = acldvppMalloc(&decodeOutDevBuffer_, decodeDataSize_);  // 按计算的内存大小申请空间
        if (ret != ACL_ERROR_NONE) {
            ERROR_LOG("acldvppMalloc jpegOutBufferDev failed, ret = %d", ret);
            return FAILED;
        }
        
		// 创建解码输出图片描述信息
		decodeOutputDesc_ = acldvppCreatePicDesc();
        if (decodeOutputDesc_ != ACL_ERROR_NONE) {
            ERROR_LOG("acldvppCreatePicDesc decodeOutputDesc failed");
            return FAILED;
        }

		// 添加详细的图片描述信息
        acldvppSetPicDescData(decodeOutputDesc_, decodeOutDevBuffer_); // 解码图片内存地址
		acldvppSetPicDescFormat(decodeOutputDesc_,PIXEL_FORMAT_YUV_SEMIPLANAR_420); // 色域
		acldvppSetPicDescWidth(decodeOutputDesc_, inputWidth_); // 输入图片宽
		acldvppSetPicDescHeight(decodeOutputDesc_, inputHeight_);
		acldvppSetPicDeseWidthStride(decodeOutputDesc_, decodeOutWidthStride); // 输出宽对齐
		acldvppSetPicDescHeightStride(decodeOutputDesc_, decodeOutHeightStride); // 输出高对齐
		acldvppSetPicDescSize(decodeOutputDese_, decodeDataSize_); // 数据大小
		
		/* ========================= 数据准备完毕 ================================== */
		// 执行解码动作
		ret = acldvppJpegDecodeAsync(dvppChannelDesc_, inDevBuffer_, inDevBufferSize_, decodeOutputDesc_, stream_);
		if (ret != ACL_ERROR_NONE){
			ERROR_LOG("acldvppJpegDecodeAsync failed, ret=%d", ret);
			return FAILED;
		}
	
		// 同步等待
		// 		使得程序阻塞在此,直到上一步执行解码动作完成
		ret = aclrtSynchronizeStream(stream_);
		if (ret != ACL_ERROR_NONE){
			ERROR_LOG("aclrtSynchronizeStream failed, ret=%d", ret);
			return FAILED;
		}
		
		// 解码结束后  释放输入数据内存
		(void)acldvppFree(picDevBuffer);
		picDevBuffer = nullptr;
		
		// 解码数据写入磁盘
		void *dvppOutputBuffer = nullptr;
		int dvppOutputSize;
		GetOutput(&dvppOutputBuffer, dvppOutputSize);
		std::string dvppOutputfileNameCur = dvppOutputfileName + std::to_string(index);
		ret = SaveDvppOutputData(dvppOutputfileNameCur.c_str(), dvppOutputBuffer, dvppOutputSize);
		if (ret != SUCCESS){
			ERROR_LOG("save dvpp output data failed");
		}
		
		// 销毁本张图片描述信息
		acldvppDestroyPicDesc(decodeOutputDesc_);
	}
	// 销毁解码channel
	DestroyDecodeResource();
	// 销毁运行资源
	DestroyResource();
	
	INFO_LOG("execute sample sucecss");
	return SUCCESS;
}
// 函数定义

void CreatResource(){
	aclrtSetDevice(deviceId_);
	INFO_LOG("open device %d success", deviceId_);
	aclrtCreateContext(&context_, deviceId_);
	INFO_LOG("open context success");
	aclrtCreateStream(&stream_);
	INFO_LOG("open stream success");
}

void CreateDecodeResource(){
	dvppChannelDesc_ = acldvppCreateChannelDesc();
	acldvppCreateChannel(dvppChannelDesc_);	// Desc里就有了一个创建好的Channel了
	INFO_LOG("dvpp init resource success");
}

char* ReadBinFile(std::string fileName, uint32_t &fileSize){
	struct stat sBuf;
    int fileStatus = stat(fileName.data(), &sBuf);
    if (fileStatus == -1) {
        ERROR_LOG("failed to get file");
        return nullptr;
    }
    if (S_ISREG(sBuf.st_mode) == 0) {
        ERROR_LOG("%s is not a file, please enter a file", fileName.c_str());
        return nullptr;
    }

    std::ifstream binFile(fileName, std::ifstream::binary);
    if (binFile.is_open() == false) {
        ERROR_LOG("open file %s failed", fileName.c_str());
        return nullptr;
    }

    binFile.seekg(0, binFile.end);
    uint32_t binFileBufferLen = binFile.tellg();
    if (binFileBufferLen == 0) {
        ERROR_LOG("binfile is empty, filename is %s", fileName.c_str());
        binFile.close();
        return nullptr;
    }

    binFile.seekg(0, binFile.beg);

    void* binFileBufferData = nullptr;
    aclError ret = ACL_ERROR_NONE;
    if (!g_isDevice) {
        ret = aclrtMallocHost(&binFileBufferData, binFileBufferLen);
        if (binFileBufferData == nullptr) {
            ERROR_LOG("malloc binFileBufferData failed");
            binFile.close();
            return nullptr;
        }
    } else {
        ret = aclrtMalloc(&binFileBufferData, binFileBufferLen, ACL_MEM_MALLOC_NORMAL_ONLY);
        if (ret != ACL_ERROR_NONE) {
            ERROR_LOG("malloc device buffer failed. size is %u", binFileBufferLen);
            binFile.close();
            return nullptr;
        }
    }
    binFile.read(static_cast<char *>(binFileBufferData), binFileBufferLen);
    binFile.close();
    fileSize = binFileBufferLen;
    return binFileBufferData;
}

void* GetDeviceBufferOfPicture(const PicDesc &picDesc, uint32_t &devPicBufferSize){
	if (picDesc.picName.empty(){
		ERROR_LOG ( "picture file name is empty") ;
		return nullptr ;
	}
	
	// 从硬盘中读取
	uint32_t inputHostBuffSize = 0 ;
	// 将输入图片内存返回
	char* inputHostBuff = ReadBinFile(picDesc.picName, inputHostBuffSize);
	if (inputHostBuff == nullptr){
		return nullptr;
	}
	
	void *inBufferDev = nullptr ;
	uint32_t inBufferSize = inputHostBuffSize;
	aclError ret = acldvppMalloc(&inBufferDev, inBufferSize); // 申请DVPP内存,用于存放输入JPEG码流
	if (ret != ACL_ERROR_NONE) {
		ERROR_LOG("malloc device buffer failed. size is %u", inBufferSize);
		delete[] inputHostBuff;
		return nullptr;
	}
	// 将输入数据复制到DVPP申请的内存中
	ret = aclrtMemcpy(inBufferDev,inBufferSize,inputHostBuff,inputHostBuffSize, ACL_MEMCPY_HOST_TO_DEVICE);
	if (ret!=ACL_ERROR_NONE){
		ERROR_LOG ("memcpy failed, device buffer size is %u, input host buffer size is u",	inBufferSize, inputHostBuffSize) ;
		acldvppFree (inBufferDev);
		delete[] inputHostBuff;
		return nullptr ;
	}

	// 用DVPP自带接口计算解码后数据内存大小
	ret = acldvppJpegPredictDecSize(inputHostBuff, inputHostBuffSize, PIXEL_FORMAT_YUV_SEMIPLANAR_420, &decodeDataSize_);
	if (ret != ACL_ERROR_NONE){
		ERROR_LOG("acldvppJpegPredictDecSize failed, ret = %d", ret);
	}

	// 计算H, W, C
	uint32_t picWidth=0;
	uint32_t picHeight=0;
	int32_t picComponents=0;
	ret = acldvppJpegGetImageInfo(inputHostBuff, inputHostBuffSize, &picWidth, &picHeight, &picComponents);
	
	if (ret != ACL_ERROR_NONE){
		ERROR_LOG("acldvppJpegGetImageInfo failed, ret=%d", ret);
	}
	else{
		INFO_LOG("Picture: %s, widthL %d, heights: %d, channel: %d", picDesc.picName.c_str(), picWidth, picHeight, picComponents);
	}
	
	delete[] inputHostBuff;
	devPicBufferSize = inBufferSize;
	return inBufferDev;
}

// 将准备好的输入传递给内置对象(头文件中定义的)
void SetInput(void *inDevBuffer, int inDevBufferSize, int inputWidth, int inputHeight){
	inDevBuffer_ = inDevBuffer;
	inDevBufferSize_ = inDevBufferSize;
	inputWidth_ = inputWidth;
	inputHeight_ = inputHeight;	
}

void GetOutput(void **outputBuffer, int &outputSize){
	*outputBuffer = decodeOutDevBuffer_;
	outputSize = decodeDataSize_;
	decodeOutDevBuffer_ = nullptr;
	decodeDataSize_ = 0;
}

// 写入硬盘
Result SaveDvppOutputData(const char *fileName, const void *devPtr, uint32_t dataSize){
	// 将输出数据从Device拷到Host
	void *hostPtr = nullptr;
	aclError aclRet = aclrtMal1ocHost(&hostPtr, dataSize);
	if (aclRet != ACL_ERROR_NONE){
		ERROR_LOG("malloc host data buffer failed,aclRet is %d", aclRet);
		return FAILED;
	}

	aclRet = aclrtMemcpy(hostPtr, dataSize, devPtr, dataSize, ACL_MEMCPY_DEVICE_TO_HOST);
	if (aclRet != ACL_ERROR_NONE){
		ERROR_LOG("dvpp output memcpy to host failed, aclRet is %d", aclRet);
		(void)aclrtFreeHost(hostPtr);
		return FAILED;
	}
	
	// 打开文件指针
	FILE *outFileFp = fopen(fileName, "wb+");
	if (nullptr == outFi1eFp){
		ERROR_LOG("fopen out file %s failed. ", fileName);
		(void) ac1rtFreeHost(hostPtr) ;
		return FAILED;
	}
	
	// 内存写入
	size_t writeSize = fwrite(hostPtr, sizeof(char),dataSize,outFileFp);
	if(writeSize != dataSize){
		ERROR_LOG("need write %u bytes to %s,but only write 号zu bytes. ",
			dataSize, fileName, writeSize);
		(void)aclrtFreeHost(hostPtr);
		return FAILED ;
	}
	
	(void)aclrtFreeHost(hostPtr);
	fflush(outFileFp);
	fclose(outFileFp);
	return SUCCESS;
}

void DestroyDecodeResource()
{
	if (decodeOutputDesc_ != nullptr){
		acldvppDestroyChannel(dvppChannelDesc_);	// 销毁channel
		acldvppDestroyChannelDesc(dvppChannelDesc_); // 销毁desc
		decodeOutputDesc_ = nullptr;
	}
}

void DestroyResource (){
	aclError ret;
	if (stream_ != nullptr) {
		ret =aclrtDestroyStream(stream_);
		if (ret != ACL_ERROR_NONE) {
			ERROR_LOG("destroy stream failed");
		}
		stream_ = nullptr;
	}
	INFO_LOG("end to destroy stream");
	
	if (context_ != nullptr){
		ret =aclrtDestroyContext(context_) ;
		if (ret != ACL_ERROR_NONE){
			ERROR_LOG("destroy context failed");
		}
		context_ = nullptr;
	}
	INFO_LOG("end to destroy context");
	
	ret = aclrtResetDevice(deviceId_);
	if (ret !=ACL_ERROR_NONE){
		ERROR_LOG("reset device failed");
	}
	INFO_LOG("end to reset device is %d", deviceId_);
	
	ret = aclFinalize();
	if (ret !=ACL_ERROR_NONE){
		ERROR_LOG("finalize acl failed");
	}
	INFO_LOG("end to finalize acl");
}
3. 编译运行

使用dvpp时CMakeList有些不一样

# Copyright (c) Huawei Technologies Co., Ltd. 2019. All rights reserved.
# CMake lowest version requirement
cmake_minimum_required(VERSION 3.5.1)

# project information
project(ACL_HELLO_WORLD)

# Compile options
add_compile_options(-std=c++11)

add_definitions(-DENABLE_DVPP_INTERFACE)

# 指定生成路径
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY  "../out")
set(CMAKE_CXX_FLAGS_DEBUG "-fPIC -O0 -g -Wall")
set(CMAKE_CXX_FLAGS_RELEASE "-fPIC -O2 -Wall")

set(INC_PATH $ENV{DDK_PATH})

if (NOT DEFINED ENV{DDK_PATH})
    if (${CMAKE_HOST_SYSTEM_NAME} MATCHES "Windows")
        set(INC_PATH "C:/Program Files/HuaWei/Ascend")
    else ()
        set(INC_PATH "/usr/local/Ascend")
    endif ()
    message(STATUS "set default INC_PATH: ${INC_PATH}")
else ()
    message(STATUS "env INC_PATH: ${INC_PATH}")
endif ()

set(LIB_PATH $ENV{NPU_HOST_LIB})

# Dynamic libraries in the stub directory can only be used for compilation
if (NOT DEFINED ENV{NPU_HOST_LIB})
    if (${CMAKE_HOST_SYSTEM_NAME} MATCHES "Windows")
        set(LIB_PATH "C:/Program Files/HuaWei/Ascend/Acllib/lib64")
    else ()
        set(LIB_PATH "/usr/local/Ascend/acllib/lib64/stub/")
    endif ()
    message(STATUS "set default LIB_PATH: ${LIB_PATH}")
else ()
    message(STATUS "env LIB_PATH: ${LIB_PATH}")
endif ()

# Header path
# 引用除acl.h 需要在此添加头文件
include_directories(
    ${INC_PATH}/acllib/include/

)

if(target STREQUAL "Simulator_Function")
    add_compile_options(-DFUNC_SIM)
endif()

# add host lib path
link_directories(
    ${LIB_PATH}
)

# 可能变化的
# 想要编译的源文件
add_executable(jpegd  # 指生成的可执行文件
        jpged.cpp)  # 指待编译的源文件

target_link_libraries(jpegd
		ascendcl acl_cblas acl_dvpp stdc++	)

install(TARGETS main DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})

cmake -> make -> touch -> main

2.4 抠图和缩放

由VPC模块实现功能。可以实现抠图、缩放、叠加和拼接功能。
在这里插入图片描述

  • VPC要求输入宽高对齐

在这里插入图片描述
YUV420SP宽高要求对齐至16。

在这里插入图片描述

2.4.1 缩放接口

在这里插入图片描述

acldvppResizeConfig *acldvppCreateResizeConfig()创建图片缩放配置数据。
acldvppDestroyResizeConfig(acldvppResizeConfig *resizeConfig):销毁缩放配置

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值