写在前面:罗列了一些内容,试图从头到尾把JPEGD的开源样例过一遍,事无巨细,难免罗嗦,文中尚存一些不严谨、不正确的内容,欢迎各位新老朋友批评指教,多多交流
学习JPEG图片解码,看Gitee上的开源样例
https://gitee.com/ascend/samples/tree/master/cplusplus/level2_simple_inference/0_data_process/jpegd
先看开源样例的readme,总结信息如下:
jpegd的功能是将输入的jpeg图片转换为yuv图片
整体分为build和run两个步骤
build过程使用bash命令执行sample_build.sh文件
运行过后会显示
run过程使用bash命令执行sample_run.sh文件
运行过后会显示
最后,在out/output路径下就会看到yuv图片
那么,开始“问”
一步编译,一步运行,其中涉及到什么内容?
一、先看编译部分,打开sample_build.sh文件
sample_build第1行指定了使用bash命令执行
sample_build第2行定义了ScriptPath,不熟悉的话可以将双引号的内容复制到命令行里验证一下,指定ScriptPath为/scripts文件夹
sample_build第3行定义了DataPath为/data文件夹
sample_build第4行定义了common_script_dir,验证得知这是Ascend安装目录下的一个路径
sample_build第5行执行了sample_common.sh文件,查看该文件,其中只是定义了很多函数,后面涉及到再细看
sample_build第27行执行了main函数,再看7-26行的main函数包含什么内容
main第9行向屏幕打印[INFO]信息
main第11行执行了target_kernel函数,该函数是在上述sample_common.sh文件内定义的
根据该函数,若在屏幕上输入x86,则定义TargetKernel为“x86”,随后显示信息
回到sample_build.sh文件中的main函数
main第12-14行,如果上一条指令的返回值不等于零,则return 1,指的是如果上一条指令target_kernel如果没有执行成功,则该main函数返回1
main第16-18行,如果DataPath指定的/data目录下不存在dog1_1024_683.jpg文件,则使用wget命令从后面的网址下载文件并保存在/data目录下,所以首次运行之前样例文件中的/data目录下是没有待转换的jpg文件的,在sample_build.sh执行过程中完成了下载
main第20行执行了build函数,该函数是在上述sample_common.sh文件内定义的
build第99行定义了UserKernel为arch,这里我的理解是,device侧的Ascend310芯片是aarch64架构,对应这里的UserKernel,前面填入的x86是host侧,对应TargetKernel
build第100-111行指定了在TargetKernel为x86、UserKernel为arch的情况下,定义TargetCompiler为g++
build第112-114行,如果存在/build/intermediates/host文件夹,则将其删除,这是在删除上一次构建的结果
build第116-117行,创建/build/intermediates/host文件夹,并移动到该目录下
build第120行执行cmake指令进行编译,指定源文件在src文件夹,指定编译器为g++,指定跳过RPATH相关信息,其中src目录下的CMakeLists.txt有详细的参数设置
再看看src目录下的CMakeLists.txt文件
txt第4行指定了cmake的最小版本,如果系统cmake版本过低会在编译时提示
txt第7行定义了工程名称为JPEG
txt第10行指定加入编译选项-std=c++11
txt第12行设置宏定义-DENABLE_DVPP_INTERFACE,这一条是在使用dvpp功能时需要加上的
txt第15-17行指定了编译结果存放路径为/out文件夹、设置了编译debug版本时的编译选项、设置了编译release版本时的编译选项
txt第19-21行,如果环境变量中没有定义INSTALL_DIR,则返回报错信息,可以手动在.bashrc文件内查看该环境变量的定义
txt第24-27行,将/acllib/include添加到编译器头文件搜索路径下
txt第30-32行,指定第三方库搜索路径为/runtime/lib64/stub
txt第34-35行,执行编译,指定编译结果“可执行程序”名为main,指定待编译的源文件为main.cpp
txt第37-38行,链接动态库,将参数中的动态库链接到编译生成的可执行文件main上
txt第40行,使用install指令定义安装规则,参数DESTINATION制定了目标main的安装路径为txt第15行指定的位置
回到build函数
build第121-124行,如果前面的cmake没有执行成功,则该build函数返回1
build第125行执行make命令,开始编译
build第126-129行,如果前面的make没有执行成功,则打印[ERROR]信息,且该build函数返回1
build第130行,返回上一次路径,在build第117行用cd命令进入了/build/intermediates/host路径,现在返回到之前的路径下,即sample_build.sh所在的/scripts目录下;利用重定向,让cd命令的输出不打印在屏幕上
至此build函数结束,回到sample_build.sh文件中的main函数
main第21-23行,如果前面的build没有执行成功,则该main函数返回1
main第25行向屏幕打印[INFO]信息
至此sample_build.sh文件结束,编译过程结束,可以看到首次运行创建了/out目录,且out目录下包含了编译结果,即可执行文件main
二、看完编译部分,再看运行部分,打开sample_run.sh文件
sample_run第2-3行定义了ScriptPath和common_script_dir
sample_run第39行执行main函数
main第30行向屏幕打印[INFO]信息
main第32行定义running_command
main第34行执行running函数
main第35-37行,如果前面的running没有执行成功,则该main函数返回1
running第8-13行,定义Kernel,识别当前环境的信息,并定义Targetkernel
running第14-16行,进入/out路径、删除/output文件夹、新建/output文件夹
running第17行,执行running_command中描述的命令,即执行/out/main,执行时传入参数,参数为jpg文件路径
running第18-24行,根据main是否执行成功,向屏幕打印[INFO]信息或[ERROR]信息
至此sample_run.sh文件结束,可以看到最终的yuv文件存放在/out/output目录下
三、JPEGD的主要内容,打开源文件main.cpp,直奔main函数
main第207-210行:当正确输入上述running_command时,if分支不会触发,此时argc=2,argv[1]= ‘../data/dog1_1024_683.jpg’,argv[0]=’ ./main’;如果argc<2,即没有输全两个部分,或者argv[1]是个空指针,即没有输入图片路径,则会触发if分支并打印[ERROR]信息
main第211行,定义string类型变量image_path用于存储传入的图片路径
main第215-216行:初始化。首先定义指向char类型常量的指针aclConfigPath,将其作为参数调用aclInit()函数
这里为什么要把aclConfigPath定义成这个数据类型?
要从文档中查阅aclInit()函数的说明
“应用开发(C++)>AscendCL API参考>系统配置>aclInit”
从“函数功能”可知,aclInit()函数是用来进行初始化的
如果问“这个函数具体做了哪些初始化工作”,需要查看该函数的定义代码,这部分本文暂不涉及
如果问“调用这个函数进行初始化的理由是什么,写我自己的程序也必须调用这个函数吗”,需要查看文档中的JPEGD的全部工作流程,文档让怎么干,就怎么干,这部分本文计划看完代码之后再来总结,先回到main.cpp,先看这每一步都做了什么。
从“函数原型”可知,aclInit()函数的输入参数的数据类型,aclInit()函数的返回值的数据类型
根据“参数说明”以及main.cpp中定义的aclConfigPath可知,该参数是一个json配置文件的路径,打开样例代码src/acl.json可以发现,该文件几乎为空,即在当前样例中还不需要进行具体的配置
main第217行调用INFO_LOG函数打印信息
该函数在头文件main.h中涉及
main第219行,调用aclrtSetDevice()函数,指定要使用的Device
该函数传入int32_t类型的参数deviceId_,该参数在main.h中涉及
main第220行调用INFO_LOG函数打印信息
main第222行调用aclrtCreateContext()函数,显示创建一个Context
该函数同样传入了deviceId_,同时还需要传入一个指向aclrtContext类型变量的指针,变量context_同样在main.h中有涉及,我猜测context_本身就是一个空指针,这里传入的是指针的指针&context_,具体细节还要从aclrtContext数据类型的定义处核实;执行完该函数后,该指针将指向创建好的Context
main224行调用aclrtCreateStream()函数,创建一个Stream
该函数传入一个指向aclrtStream类型变量的指针,该参数在main.h中如下,执行完函数之后,该指针将指向创建好的Stream
main第225行调用aclrtGetRunMode()函数,获取运行模式
该函数传入一个指向aclrtMode类型变量的指针,该参数在main.h中如下,执行完函数之后,该指针将指向ACL_DEVICE,这是因为前面提到host为x86,device为aarch64
main第226行调用INFO_LOG函数打印信息
main第228-232行,创建了图片描述信息,PicDesc类型变量testPic,并打印了信息
数据类型PicDesc在main.h中有描述
结合打印信息可知,结构体testPic中的成员picName为前述image_path,借由c_str()函数将string类型的image_path转换为char*类型,详见c_str()函数
main第236-241行,调用GetDeviceBufferOfPicture()函数
GetDeviceBufferOfPicture第30-33行,检查是否传入有效的图片路径
GetDeviceBufferOfPicture第35-39行,以rb权限打开图片
GetDeviceBufferOfPicture第41-43行,首先将指针fp指向文件末尾,然后调用ftell()函数计算二进制文件的大小,最后将指针fp指向文件开始处
GetDeviceBufferOfPicture第45-47行,用new分配一个char数组,并返回指向第一个char的指针inputBuffer
GetDeviceBufferOfPicture第48行,调用fread()函数,从inputBuffer开始,以sizeof(char)为单位,在fp流中读取inputBuffSize个单位,返回成功读取的对象数给readSize
GetDeviceBufferOfPicture第49-55行,检查读取是否正常完成
GetDeviceBufferOfPicture第57-68行,调用acldvppJpegGetImageInfoV2()函数,读取jpeg图片的宽、高、通道数、编码格式,然后调用INFO_LOG函数向屏幕打印出这些信息
GetDeviceBufferOfPicture第72-78行,调用acldvppJpegPredictDecSize()函数,利用传入的jpg图片预测解码后yuv图片所需的内存,并检查是否完成计算
其中,输入参数PIXEL_FORMAT_YUV_SEMIPLANAR_420是定义好的
GetDeviceBufferOfPicture第80-87行,调用acldvppMalloc()函数,根据host侧存储数据时占用的缓存大小inputBufferSize,进行“内存首地址128对齐”,计算出需要给Device侧分配的内存大小,然后传给inBufferDev
GetDeviceBufferOfPicture第89-102行,根据runMode,调用aclrtMemcpy()函数,将数据从host侧复制到device侧。其中第一个参数inBufferDev为上一步创建好的device侧的缓存;第二个参数为device侧“目的内存地址的最大内存长度”,样例中用的是inputBufferSize,图片在host侧的内存占用长度。第三个参数inputBuffer为host侧的存放图片的首地址指针。第四个参数inputBufferSize为内存复制的长度,需要将图片的全部长度复制过去。第五个参数制定了内存复制类型,即从host向device复制
GetDeviceBufferOfPicture第104行,在完成复制之后,删除了host侧原本存放图片的内存
GetDeviceBufferOfPicture第105行,传出device侧图片内存占用长度devPicBufferSize
GetDeviceBufferOfPicture第106行,关闭用于读取图片文件的流fp
GetDeviceBufferOfPicture第107行,返回device存有图片的内存首地址指针inBufferDev
关于其中内存对齐要求,来源于文档中的约束说明
至此GetDeviceBufferOfPicture()函数结束,main函数237行通过调用该函数,得到了device侧的内存首地址指针picDevBuffer,得到了device侧的内存大小devPicBufferSize
继续回到main函数
main第243行,调用acldvppCreateChannelDesc()函数,创建通道描述符
main第244行,调用acldvppCreateChannel()函数,创建数据处理通道,创建后该通道描述符将对应实际存在的通道信息
main第245行,向屏幕打印信息
main第248行,调用SetInput()函数,定义得到了device侧内存首地址指针inDevBuffer_、device侧内存长度inDevBufferSize_、图片宽度inputWidth_、图片高度inputHeight_
main第251-252行,根据对齐规则,计算decodeOutWidthStride和decodeOutHeightStride,对齐规则参考文档中的约束说明
main第257-261行,调用acldvppMalloc()函数,根据图片描述信息结构体testPic的成员jpegDecodeSize,在device侧分配内存,并传给decodeOutDevBuffer_
main第263-267行,调用acldvppCreatePicDesc()函数,创建图片描述符,返回acldvppPicDesc类型的指针
main第269-276行调用acldvppSetPicDesc系列函数,设置Device上的数据内存、图片格式、原图宽高、对齐后宽高、解码后的内存大小
main第278行调用acldvppJepgDecodeAsync()函数,执行异步解码。第一个参数为通道描述符,第二个参数为device侧内存首地址指针,第三个参数device侧内存大小,第四个参数为解码输出图片描述符,第五个参数为stream
main第285行调用aclrtSynchronizeStream()函数,阻塞应用程序的运行,直到传入的Stream执行完毕
main第291行调用acldvppGetPicDescSize()函数,传入解码后的图片描述符,返回图片数据的内存的实际大小,赋值给decodeDataSize_
main第293-294行调用acldvppFree()函数,释放device侧用于存放解码前原图的内存,然后将指向该内存的指针改为空指针
main第296行使用string类型的find函数,定义dir_tail_index为“/data”在image_path中的位置,当初传入的image_path为“../data/dog1_1024_683.jpg”,位置为2
main第297行定义string类型变量outfile_dir,使用string类型的substr函数,从位置0开始,向后一共选取dir_tail_index=2个,返回这些字符,与后面的“/”和“out/output/”连起来,最终得到“../out/output/”
main第298-299行定义string类型变量outfile_path,先使用string类型的rfind函数,从后往前,返回“.jpg”在字符串image_path中的位置,然后使用substr函数返回“dog1_1024_683”,最后再利用输入图片描述信息中的宽高信息,共同组成outfile_path
main第300行调用INFO_LOG函数将其打印在屏幕上
main第303-307行调用SaveDvppOutputData()函数
SaveDvppOutputData第120-124行以wb+权限打开fileName,即yuv文件,并检查是否正常
SaveDvppOutputData第125行因RunMode==ACL_HOST进入分支
SaveDvppOutputData第126-132行,调用aclrtMallocHost()函数,在host上运行时申请host侧内存,将申请到的内存的指针的指针赋值给参数hostPtr,申请内存的大小依据解码后的图片内存大小decodeDataSize_
SaveDvppOutputData第134-140行,调用aclrtMemcpy()函数,将解码后的图片数据从device侧复制到host侧
SaveDvppOutputData第141-148行,调用fwrite()函数,第一个参数hostPtr为host侧用于存放解码后文件的地址指针,第二个参数sizeof(char)为“写入单位”,第三个参数dataSize为写入单位数量,第四个参数outFileFp为用于存放host侧解码后图片yuv文件的流
SaveDvppOutputData第149行,调用aclrtFreeHost()函数,释放host侧用于存放解码后二进制文件的内存
SaveDvppOutputData第160行调用fflush函数进行输出刷新,将流outFileFp中未被写入文件的信息强行写入
SaveDvppOutputData第161行调用fclose函数关闭流
至此SaveDvppOutputData函数结束,该函数主要将device侧的缓存复制到了host侧,然后将其写入到了yuv格式的文件中
再回到main函数
main第309行调用DestroyResource()函数
DestroyResource第169-172行,调用acldvppDestroyPicDesc()函数释放输出图片描述符
DestroyResource第174-180行,调用aclrtDestroyStream()函数释放Stream
DestroyResource第181行,调用INFO_LOG函数向屏幕打印信息
DestroyResource第183-190行,调用aclrtDestroyContext()函数释放Context,调用INFO_LOG函数向屏幕打印信息
DestroyResource第192-196行,调用aclrtResetDevice ()函数释放Device,调用INFO_LOG函数向屏幕打印信息
DestroyResource第198-202行,调用aclFinalize ()函数去初始化,调用INFO_LOG函数向屏幕打印信息
至此DestoryResource()函数结束
再回到main函数
main第311行打印信息
至此main.cpp结束
四、总结main.cpp做了什么事情
参看文档“应用开发(C++)>接口调用流程>媒体数据处理V1>JPEGD图片解码”