3
0

ImageConvert.cpp 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <Processing/PixelFormatInfo.h>
  9. #include <Processing/ImageToProcess.h>
  10. #include <Processing/ImageConvert.h>
  11. #include <Processing/ImageAssetProducer.h>
  12. #include <Processing/ImageFlags.h>
  13. #include <Processing/Utils.h>
  14. #include <Converters/FIR-Weights.h>
  15. #include <Converters/Cubemap.h>
  16. #include <Converters/PixelOperation.h>
  17. #include <Converters/Histogram.h>
  18. #include <ImageLoader/ImageLoaders.h>
  19. #include <BuilderSettings/BuilderSettingManager.h>
  20. #include <BuilderSettings/PresetSettings.h>
  21. #include <AzCore/std/time.h>
  22. #include <AzCore/StringFunc/StringFunc.h>
  23. #include <Atom/RHI.Reflect/Format.h>
  24. #include <AzToolsFramework/API/EditorAssetSystemAPI.h>
  25. // for texture splitting
  26. // minimum number of low level mips will be saved in the base file.
  27. #define MinPersistantMips 3
  28. // minimum texture size to be splitted. A texture will only be split when the size is larger than this number
  29. #define MinSizeToSplit 1 << 5
  30. #if defined(AZ_TOOLS_EXPAND_FOR_RESTRICTED_PLATFORMS)
  31. #if defined(TOOLS_SUPPORT_JASPER)
  32. #include AZ_RESTRICTED_FILE_EXPLICIT(ImageProcess, Jasper)
  33. #endif
  34. #if defined(TOOLS_SUPPORT_PROVO)
  35. #include AZ_RESTRICTED_FILE_EXPLICIT(ImageProcess, Provo)
  36. #endif
  37. #if defined(TOOLS_SUPPORT_SALEM)
  38. #include AZ_RESTRICTED_FILE_EXPLICIT(ImageProcess, Salem)
  39. #endif
  40. #endif
  41. namespace ImageProcessingAtom
  42. {
  43. enum ConvertStep
  44. {
  45. StepValidateInput = 0,
  46. StepConvertToLinear,
  47. StepSwizzle,
  48. StepCubemapLayout,
  49. StepPreNormalize,
  50. StepGenerateIBL,
  51. StepMipmap,
  52. StepAverageColor,
  53. StepGlossFromNormal,
  54. StepPostNormalize,
  55. StepConvertOutputColorSpace,
  56. StepConvertPixelFormat,
  57. StepSaveToFile,
  58. StepAll
  59. };
  60. [[maybe_unused]] const char ProcessStepNames[StepAll][64] =
  61. {
  62. "ValidateInput",
  63. "ConvertToLinear",
  64. "Swizzle",
  65. "CubemapLayout",
  66. "PreNormalize",
  67. "GenerateIBL",
  68. "Mipmap",
  69. "AverageColor",
  70. "GlossFromNormal",
  71. "PostNormalize",
  72. "ConvertOutputColorSpace",
  73. "ConvertPixelFormat",
  74. "SaveToFile",
  75. };
  76. const char* SpecularCubemapSuffix = "_iblspecular";
  77. const char* DiffuseCubemapSuffix = "_ibldiffuse";
  78. IImageObjectPtr ImageConvertProcess::GetOutputImage()
  79. {
  80. if (m_image)
  81. {
  82. return m_image->Get();
  83. }
  84. return nullptr;
  85. }
  86. IImageObjectPtr ImageConvertProcess::GetOutputIBLSpecularCubemap()
  87. {
  88. return m_iblSpecularCubemapImage;
  89. }
  90. IImageObjectPtr ImageConvertProcess::GetOutputIBLDiffuseCubemap()
  91. {
  92. return m_iblDiffuseCubemapImage;
  93. }
  94. void ImageConvertProcess::GetAppendOutputProducts(AZStd::vector<AssetBuilderSDK::JobProduct>& outProducts)
  95. {
  96. for (const auto& path : m_jobProducts)
  97. {
  98. outProducts.push_back(path);
  99. }
  100. }
  101. const ImageConvertProcessDescriptor* ImageConvertProcess::GetInputDesc() const
  102. {
  103. return m_input.get();
  104. }
  105. ImageConvertProcess::ImageConvertProcess(AZStd::unique_ptr<ImageConvertProcessDescriptor>&& descriptor)
  106. : m_image(nullptr)
  107. , m_progressStep(0)
  108. , m_isFinished(false)
  109. , m_isSucceed(false)
  110. , m_processTime(0)
  111. {
  112. m_input = AZStd::move(descriptor);
  113. }
  114. ImageConvertProcess::~ImageConvertProcess()
  115. {
  116. delete m_image;
  117. }
  118. bool ImageConvertProcess::IsConvertToCubemap()
  119. {
  120. return m_input->m_presetSetting.m_cubemapSetting != nullptr;
  121. }
  122. bool ImageConvertProcess::IsPreconvolvedCubemap()
  123. {
  124. AZStd::unique_ptr<CubemapSettings>& cubemapSettings = m_input->m_presetSetting.m_cubemapSetting;
  125. return (cubemapSettings != nullptr && cubemapSettings->m_requiresConvolve == false);
  126. }
  127. void ImageConvertProcess::UpdateProcess()
  128. {
  129. if (m_isFinished)
  130. {
  131. return;
  132. }
  133. auto stepStartTime = AZStd::GetTimeUTCMilliSecond();
  134. switch (m_progressStep)
  135. {
  136. case StepValidateInput:
  137. // validate
  138. if (!ValidateInput())
  139. {
  140. m_isSucceed = false;
  141. break;
  142. }
  143. // set start time
  144. m_startTime = AZStd::GetTimeUTCMilliSecond();
  145. // Volume Textures are special. They are saved in the asset catalog
  146. // as-is. They are expected to have the mipmaps precalculated and we don't do any kind
  147. // of processing on them.
  148. if (m_input->m_inputImage->HasImageFlags(EIF_Volumetexture))
  149. {
  150. m_image = new ImageToProcess(m_input->m_inputImage);
  151. // And go straight into the final step next time UpdateProcess() is called.
  152. m_progressStep = StepSaveToFile - 1;
  153. break;
  154. }
  155. // identify the alpha content of input image if gloss from normal wasn't set
  156. m_alphaContent = m_input->m_inputImage->GetAlphaContent();
  157. // Create image for process.
  158. // If this is not a pre-convolved cubemap we only copy the highest mip until we figure out what to do with input's mipmaps.
  159. {
  160. uint32 mipsToClone = IsPreconvolvedCubemap() ? (std::numeric_limits<uint32>::max)() : 1;
  161. m_image = new ImageToProcess(IImageObjectPtr(m_input->m_inputImage->Clone(mipsToClone)));
  162. }
  163. break;
  164. case StepConvertToLinear:
  165. // convert to linear space and the output image pixel format should be rgba32f
  166. ConvertToLinear();
  167. break;
  168. case StepSwizzle:
  169. {
  170. // swizzle if swizzle was set or decard alpha
  171. bool swizzleWasSet = m_input->m_presetSetting.m_swizzle.size() >= 4;
  172. if (swizzleWasSet || m_input->m_presetSetting.m_discardAlpha)
  173. {
  174. AZStd::string swizzle = "rgba";
  175. if (swizzleWasSet)
  176. {
  177. swizzle = m_input->m_presetSetting.m_swizzle.substr(0, 4);
  178. }
  179. if (m_input->m_presetSetting.m_discardAlpha)
  180. {
  181. swizzle[3] = '1';
  182. }
  183. m_image->Get()->Swizzle(swizzle.c_str());
  184. if (m_input->m_presetSetting.m_discardAlpha)
  185. {
  186. m_alphaContent = EAlphaContent::eAlphaContent_Absent;
  187. }
  188. else
  189. {
  190. m_alphaContent = m_image->Get()->GetAlphaContent();
  191. }
  192. }
  193. }
  194. break;
  195. case StepCubemapLayout:
  196. // convert cubemap image's layout to vertical strip used in game.
  197. if (IsConvertToCubemap())
  198. {
  199. if (!m_image->ConvertCubemapLayout(CubemapLayoutVertical))
  200. {
  201. m_image->Set(nullptr);
  202. }
  203. }
  204. break;
  205. case StepPreNormalize:
  206. // normalize base image before mipmap generation if glossfromnormals is enabled and require normalize
  207. if (m_input->m_presetSetting.m_isMipRenormalize && m_input->m_presetSetting.m_glossFromNormals)
  208. {
  209. // Normalize the base mip map. This has to be done explicitly because we need to disable mip renormalization to
  210. // preserve the normal length when deriving the normal variance
  211. m_image->Get()->NormalizeVectors(0, 1);
  212. }
  213. break;
  214. case StepGenerateIBL:
  215. if (IsConvertToCubemap())
  216. {
  217. // check and generate IBL specular and diffuse, if necessary
  218. AZStd::unique_ptr<CubemapSettings>& cubemapSettings = m_input->m_presetSetting.m_cubemapSetting;
  219. if (cubemapSettings->m_generateIBLSpecular && !cubemapSettings->m_iblSpecularPreset.IsEmpty())
  220. {
  221. bool success = CreateIBLCubemap(cubemapSettings->m_iblSpecularPreset, SpecularCubemapSuffix, m_iblSpecularCubemapImage);
  222. if (!success)
  223. {
  224. m_isSucceed = false;
  225. m_isFinished = true;
  226. break;
  227. }
  228. }
  229. if (cubemapSettings->m_generateIBLDiffuse && !cubemapSettings->m_iblDiffusePreset.IsEmpty())
  230. {
  231. bool success = CreateIBLCubemap(cubemapSettings->m_iblDiffusePreset, DiffuseCubemapSuffix, m_iblDiffuseCubemapImage);
  232. if (!success)
  233. {
  234. m_isSucceed = false;
  235. m_isFinished = true;
  236. break;
  237. }
  238. }
  239. }
  240. if (m_input->m_presetSetting.m_generateIBLOnly)
  241. {
  242. // this preset doesn't output an image of its own, just the IBL cubemaps
  243. m_isSucceed = true;
  244. m_isFinished = true;
  245. }
  246. break;
  247. case StepMipmap:
  248. // generate mipmaps
  249. if (IsConvertToCubemap())
  250. {
  251. if (m_input->m_presetSetting.m_cubemapSetting->m_requiresConvolve)
  252. {
  253. bool success = FillCubemapMipmaps();
  254. if (!success)
  255. {
  256. m_isSucceed = false;
  257. m_isFinished = true;
  258. }
  259. }
  260. }
  261. else
  262. {
  263. FillMipmaps();
  264. }
  265. // add image flag
  266. if (m_input->m_presetSetting.m_suppressEngineReduce || m_input->m_textureSetting.m_suppressEngineReduce)
  267. {
  268. m_image->Get()->AddImageFlags(EIF_SupressEngineReduce);
  269. }
  270. break;
  271. case StepAverageColor:
  272. // Compute and cache the (alpha-weighted) average color.
  273. // We can typically get away with using a lower quality mip (small deviations from the
  274. // true 'mip=0' average may be possible with nontrivial alpha channels or non-power-of-2
  275. // image sizes, but they are usually insignificant).
  276. {
  277. AZ::u32 preferredMip = 2; // set to 0 for exact average
  278. AZ::u32 mip = AZStd::min(preferredMip, m_image->Get()->GetMipCount() - 1);
  279. SetAverageColor(mip);
  280. }
  281. break;
  282. case StepGlossFromNormal:
  283. // get gloss from normal for all mipmaps and save to alpha channel
  284. if (m_input->m_presetSetting.m_glossFromNormals)
  285. {
  286. bool hasAlpha = Utils::NeedAlphaChannel(m_alphaContent);
  287. m_image->Get()->GlossFromNormals(hasAlpha);
  288. // set alpha content so it won't be ignored later.
  289. m_alphaContent = EAlphaContent::eAlphaContent_Greyscale;
  290. }
  291. break;
  292. case StepPostNormalize:
  293. // normalize all the other mipmaps
  294. if (!IsConvertToCubemap() && m_input->m_presetSetting.m_isMipRenormalize)
  295. {
  296. if (m_input->m_presetSetting.m_glossFromNormals)
  297. {
  298. // normalize other mips except first mip
  299. m_image->Get()->NormalizeVectors(1, 100);
  300. }
  301. else
  302. {
  303. // normalize all mips
  304. m_image->Get()->NormalizeVectors(0, 100);
  305. }
  306. m_image->Get()->AddImageFlags(EIF_RenormalizedTexture);
  307. }
  308. break;
  309. case StepConvertOutputColorSpace:
  310. // convert image from linear space to desired output color space
  311. ConvertToOuputColorSpace();
  312. break;
  313. case StepConvertPixelFormat:
  314. // convert pixel format
  315. ConvertPixelformat();
  316. break;
  317. case StepSaveToFile:
  318. // save to file if required
  319. if (!m_input->m_isPreview && m_input->m_shouldSaveFile)
  320. {
  321. m_isSucceed = SaveOutput();
  322. }
  323. else
  324. {
  325. m_isSucceed = true;
  326. }
  327. break;
  328. }
  329. auto stepEndTime = AZStd::GetTimeUTCMilliSecond();
  330. if (stepEndTime - stepStartTime > 1000)
  331. {
  332. AZ_TracePrintf("Image Processing", "Step [%s] took %f seconds\n", ProcessStepNames[m_progressStep],
  333. (stepEndTime - stepStartTime) / 1000.0);
  334. }
  335. m_progressStep++;
  336. if (m_image == nullptr || m_image->Get() == nullptr || m_progressStep >= StepAll)
  337. {
  338. m_isFinished = true;
  339. AZStd::sys_time_t endTime = AZStd::GetTimeUTCMilliSecond();
  340. m_processTime = static_cast<double>(endTime - m_startTime) / 1000.0;
  341. }
  342. // output conversion log
  343. if (m_isSucceed && m_isFinished)
  344. {
  345. [[maybe_unused]] IImageObjectPtr imageObj = m_image->Get();
  346. [[maybe_unused]] const uint32 sizeTotal = imageObj->GetTextureMemory();
  347. if (m_input->m_isPreview)
  348. {
  349. AZ_TracePrintf("Image Processing", "Image (%d bytes) converted in %f seconds\n", sizeTotal, m_processTime);
  350. }
  351. else if (m_input->m_presetSetting.m_generateIBLOnly)
  352. {
  353. AZ_TracePrintf("Image Processing", "Image (IBL Only) processed in %f seconds\n", m_processTime);
  354. }
  355. else
  356. {
  357. [[maybe_unused]] const PixelFormatInfo* formatInfo = CPixelFormats::GetInstance().GetPixelFormatInfo(imageObj->GetPixelFormat());
  358. [[maybe_unused]] const AZ::RHI::Format rhiFormat = Utils::PixelFormatToRHIFormat(imageObj->GetPixelFormat(), imageObj->HasImageFlags(EIF_SRGBRead));
  359. AZ_TracePrintf("Image Processing", "Image [%dx%d] [%s] converted with preset [%s] [%s] and saved to [%s] (%d bytes) taking %f seconds\n",
  360. imageObj->GetWidth(0), imageObj->GetHeight(0), AZ::RHI::ToString(rhiFormat),
  361. m_input->m_presetSetting.m_name.GetCStr(),
  362. m_input->m_filePath.c_str(),
  363. m_input->m_outputFolder.c_str(), sizeTotal, m_processTime);
  364. }
  365. }
  366. }
  367. void ImageConvertProcess::ProcessAll()
  368. {
  369. while (!m_isFinished)
  370. {
  371. UpdateProcess();
  372. }
  373. }
  374. float ImageConvertProcess::GetProgress()
  375. {
  376. return m_progressStep / (float)StepAll;
  377. }
  378. bool ImageConvertProcess::IsFinished()
  379. {
  380. return m_isFinished;
  381. }
  382. bool ImageConvertProcess::IsSucceed()
  383. {
  384. return m_isSucceed;
  385. }
  386. // function to get desired output image extent
  387. void GetOutputExtent(AZ::u32 inputWidth, AZ::u32 inputHeight, AZ::u32& outWidth, AZ::u32& outHeight, AZ::u32& outReduce,
  388. const TextureSettings* textureSettings, const PresetSettings* presetSettings)
  389. {
  390. AZ_Assert(&outWidth != &outHeight, "outWidth and outHeight shouldn't use same address");
  391. outWidth = inputWidth;
  392. outHeight = inputHeight;
  393. if (textureSettings == nullptr || presetSettings == nullptr)
  394. {
  395. return;
  396. }
  397. // get suitable size for dest pixel format
  398. CPixelFormats::GetInstance().GetSuitableImageSize(presetSettings->m_pixelFormat, inputWidth, inputHeight,
  399. outWidth, outHeight);
  400. // desired reduce level. 1 means reduce one level
  401. uint sizeReduceLevel = textureSettings->m_sizeReduceLevel;
  402. outReduce = 0;
  403. // reduce to not exceed max texture size
  404. if (presetSettings->m_maxTextureSize > 0)
  405. {
  406. while (outWidth > presetSettings->m_maxTextureSize || outHeight > presetSettings->m_maxTextureSize)
  407. {
  408. outWidth >>= 1;
  409. outHeight >>= 1;
  410. outReduce++;
  411. }
  412. }
  413. // if it requires to reduce more and the result size will still larger than min texture size, then reduce
  414. while (outReduce < sizeReduceLevel &&
  415. (outWidth >= presetSettings->m_minTextureSize * 2 && outHeight >= presetSettings->m_minTextureSize * 2))
  416. {
  417. outWidth >>= 1;
  418. outHeight >>= 1;
  419. outReduce++;
  420. }
  421. // resize to min texture size if it's smaller
  422. if (outWidth < presetSettings->m_minTextureSize)
  423. {
  424. outWidth = presetSettings->m_minTextureSize;
  425. }
  426. if (outHeight < presetSettings->m_minTextureSize)
  427. {
  428. outHeight = presetSettings->m_minTextureSize;
  429. }
  430. }
  431. bool ImageConvertProcess::ConvertToLinear()
  432. {
  433. // de-gamma only if the input is sRGB. this will convert other uncompressed format to RGBA32F
  434. return m_image->GammaToLinearRGBA32F(m_input->m_presetSetting.m_srcColorSpace == ColorSpace::sRGB);
  435. }
  436. // mipmap generation
  437. bool ImageConvertProcess::FillMipmaps()
  438. {
  439. //this function only works with pixel format rgba32f
  440. const EPixelFormat srcPixelFormat = m_image->Get()->GetPixelFormat();
  441. if (srcPixelFormat != ePixelFormat_R32G32B32A32F)
  442. {
  443. AZ_Assert(false, "%s only works with pixel format rgba32f", __FUNCTION__);
  444. return false;
  445. }
  446. // only if the src image has one mip
  447. if (m_image->Get()->GetMipCount() != 1)
  448. {
  449. AZ_Assert(false, "%s called for a mipmapped image. ", __FUNCTION__);
  450. return false;
  451. }
  452. // get output image size
  453. uint32 outWidth;
  454. uint32 outHeight;
  455. uint32 outReduce = 0;
  456. GetOutputExtent(m_image->Get()->GetWidth(0), m_image->Get()->GetHeight(0), outWidth, outHeight, outReduce, &m_input->m_textureSetting,
  457. &m_input->m_presetSetting);
  458. // max mipmap count
  459. uint32 mipCount = UINT32_MAX;
  460. if (m_input->m_presetSetting.m_mipmapSetting == nullptr || !m_input->m_textureSetting.m_enableMipmap)
  461. {
  462. mipCount = 1;
  463. }
  464. // create new new output image with proper side
  465. IImageObjectPtr outImage(IImageObject::CreateImage(outWidth, outHeight, mipCount, ePixelFormat_R32G32B32A32F));
  466. // filter setting for mip map generation
  467. float blurH = 0;
  468. float blurV = 0;
  469. // fill mipmap data for uncompressed output image
  470. for (uint32 mip = 0; mip < outImage->GetMipCount(); mip++)
  471. {
  472. FilterImage(m_input->m_textureSetting.m_mipGenType, m_input->m_textureSetting.m_mipGenEval, blurH, blurV, m_image->Get(), 0, outImage, mip, nullptr, nullptr);
  473. }
  474. // transfer alpha coverage
  475. if (m_input->m_textureSetting.m_maintainAlphaCoverage)
  476. {
  477. outImage->TransferAlphaCoverage(&m_input->m_textureSetting, m_image->Get());
  478. }
  479. // set back to image
  480. m_image->Set(outImage);
  481. return true;
  482. }
  483. // Set (alpha-weighted) average color computed from given mip
  484. bool ImageConvertProcess::SetAverageColor(AZ::u32 mip)
  485. {
  486. // We only work with pixel format rgba32f
  487. const EPixelFormat srcPixelFormat = m_image->Get()->GetPixelFormat();
  488. if (srcPixelFormat != ePixelFormat_R32G32B32A32F)
  489. {
  490. AZ_Assert(false, "I only work with pixel format rgba32f");
  491. return false;
  492. }
  493. // ...and we require a linear (non-sRGB) color space
  494. if (m_image->Get()->HasImageFlags(EIF_SRGBRead))
  495. {
  496. AZ_Assert(false, "I only work with a linear (non-sRGB) color space");
  497. return false;
  498. }
  499. IPixelOperationPtr pixelOp = CreatePixelOperation(srcPixelFormat);
  500. AZ::u32 pixelBytes = CPixelFormats::GetInstance().GetPixelFormatInfo(srcPixelFormat)->bitsPerBlock / 8;
  501. AZ::u8* pixelBuf;
  502. AZ::u32 pitch;
  503. m_image->Get()->GetImagePointer(mip, pixelBuf, pitch);
  504. const AZ::u32 pixelCount = m_image->Get()->GetPixelCount(mip);
  505. // Accumulate weighted pixel colors and alpha
  506. float weightedRgbSum[3] = {0.0f, 0.0f, 0.0f};
  507. float alphaSum = 0.0f;
  508. for (AZ::u32 i = 0; i < pixelCount; ++i, pixelBuf += pixelBytes)
  509. {
  510. float R,G,B,A;
  511. pixelOp->GetRGBA(pixelBuf, R, G, B, A);
  512. // Alpha-weighted sum for the R,G,B channels:
  513. weightedRgbSum[0] += A * R;
  514. weightedRgbSum[1] += A * G;
  515. weightedRgbSum[2] += A * B;
  516. // Simple sum for the A channel:
  517. alphaSum += A;
  518. }
  519. AZ::Color avgColor(0.0f);
  520. if (alphaSum != 0)
  521. {
  522. avgColor.SetR(weightedRgbSum[0] / alphaSum);
  523. avgColor.SetG(weightedRgbSum[1] / alphaSum);
  524. avgColor.SetB(weightedRgbSum[2] / alphaSum);
  525. avgColor.SetA(alphaSum / pixelCount);
  526. }
  527. m_image->Get()->SetAverageColor(avgColor);
  528. return true;
  529. }
  530. // pixel format conversion
  531. bool ImageConvertProcess::ConvertPixelformat()
  532. {
  533. //set up compress option
  534. ICompressor::EQuality quality;
  535. if (m_input->m_isPreview)
  536. {
  537. quality = ICompressor::eQuality_Preview;
  538. }
  539. else
  540. {
  541. quality = ICompressor::eQuality_Normal;
  542. }
  543. // set the compression options
  544. m_image->GetCompressOption().compressQuality = quality;
  545. m_image->GetCompressOption().rgbWeight = m_input->m_presetSetting.GetColorWeight();
  546. m_image->GetCompressOption().discardAlpha = m_input->m_presetSetting.m_discardAlpha;
  547. // Convert to a pixel format based on the desired handling
  548. // The default behavior will choose the output format specified by the preset
  549. EPixelFormat outputFormat;
  550. switch (m_input->m_presetSetting.m_outputTypeHandling)
  551. {
  552. case PresetSettings::OutputTypeHandling::UseInputFormat:
  553. outputFormat = m_input->m_inputImage->GetPixelFormat();
  554. break;
  555. case PresetSettings::OutputTypeHandling::UseSpecifiedOutputType:
  556. default:
  557. outputFormat = m_input->m_presetSetting.m_pixelFormat;
  558. break;
  559. }
  560. m_image->ConvertFormat(outputFormat);
  561. return true;
  562. }
  563. // convert color space from linear to sRGB space if it's necessary
  564. bool ImageConvertProcess::ConvertToOuputColorSpace()
  565. {
  566. if (m_input->m_presetSetting.m_destColorSpace == ColorSpace::sRGB)
  567. {
  568. m_image->LinearToGamma();
  569. }
  570. else if (m_input->m_presetSetting.m_destColorSpace == ColorSpace::autoSelect)
  571. {
  572. // check the compressor's colorspace preference
  573. const EPixelFormat sourceFormat = m_image->Get()->GetPixelFormat();
  574. const EPixelFormat destinationFormat = m_input->m_presetSetting.m_pixelFormat;
  575. const bool isSourceFormatUncompressed = CPixelFormats::GetInstance().IsPixelFormatUncompressed(sourceFormat);
  576. const bool isDestinationFormatUncompressed = CPixelFormats::GetInstance().IsPixelFormatUncompressed(destinationFormat);
  577. // compression is only required if either the source or destination is uncompressed
  578. if (isSourceFormatUncompressed != isDestinationFormatUncompressed)
  579. {
  580. // find out if the process is compressing or decompressing
  581. const bool isCompressing = isSourceFormatUncompressed ? true : false;
  582. const EPixelFormat outputFormat = isCompressing ? destinationFormat : sourceFormat;
  583. ICompressorPtr compressor = ICompressor::FindCompressor(outputFormat, m_input->m_presetSetting.m_destColorSpace, isCompressing);
  584. // find out if the compressor has a preference to any specific colorspace
  585. const ColorSpace compressorColorSpace = compressor->GetSupportedColorSpace(outputFormat);
  586. if (compressorColorSpace == ColorSpace::sRGB)
  587. {
  588. m_image->LinearToGamma();
  589. return true;
  590. }
  591. else if (compressorColorSpace == ColorSpace::linear)
  592. {
  593. return true;
  594. }
  595. }
  596. // convert to sRGB color space if it's dark image (converting bright images decreases image quality)
  597. bool bThresholded = false;
  598. {
  599. Histogram<256> histogram;
  600. if (ComputeLuminanceHistogram(m_image->Get(), histogram))
  601. {
  602. const size_t medianBinIndex = 116;
  603. float percentage = histogram.getPercentage(medianBinIndex, 255);
  604. // The image has significant amount of dark pixels, it's good to use sRGB
  605. bThresholded = (percentage < 50.0f);
  606. }
  607. }
  608. if (bThresholded)
  609. {
  610. bool convertToSRGB = true;
  611. // if the image is BC1 compressible, additionally estimate the conversion error
  612. // to only convert if it doesn't introduce error
  613. if (CPixelFormats::GetInstance().IsImageSizeValid(ePixelFormat_BC1, m_image->Get()->GetWidth(0),
  614. m_image->Get()->GetHeight(0), false))
  615. {
  616. //get image in RGB space
  617. ImageToProcess imageProcess(m_image->Get());
  618. imageProcess.LinearToGamma();
  619. ICompressor::CompressOption option;
  620. option.compressQuality = ICompressor::eQuality_Preview;
  621. option.rgbWeight = m_input->m_presetSetting.GetColorWeight();
  622. float errorLinearBC1;
  623. float errorSrgbBC1;
  624. GetBC1CompressionErrors(m_image->Get(), errorLinearBC1, errorSrgbBC1, option);
  625. // Don't convert if it would lower the image quality when saved as sRGB according to GetDXT1GammaCompressionError()
  626. if (errorSrgbBC1 >= errorLinearBC1)
  627. {
  628. convertToSRGB = false;
  629. }
  630. }
  631. // our final conclusion: if the texture had a significant percentage of dark pixels and,
  632. // if applicable, it was BC1 compressible and gamma compression wouldn't introduce error,
  633. // then we convert it to sRGB
  634. if (convertToSRGB)
  635. {
  636. m_image->LinearToGamma();
  637. }
  638. }
  639. }
  640. return true;
  641. }
  642. bool ImageConvertProcess::ValidateInput()
  643. {
  644. // validate the input image and output settings here.
  645. uint32 dwWidth, dwHeight;
  646. dwWidth = m_input->m_inputImage->GetWidth(0);
  647. dwHeight = m_input->m_inputImage->GetHeight(0);
  648. EPixelFormat dstFmt = m_input->m_presetSetting.m_pixelFormat;
  649. // check if whether input image can be a cubemap
  650. if (m_input->m_presetSetting.m_cubemapSetting)
  651. {
  652. // check requirements for pre-convolved cubemaps
  653. // note: only check formatting if there are multiple mip levels in the source cubemap,
  654. // since some of the conversion functions should not be used when mips are present
  655. if (IsPreconvolvedCubemap() && m_input->m_inputImage->GetMipCount() > 1)
  656. {
  657. if (m_input->m_presetSetting.m_srcColorSpace != ColorSpace::linear)
  658. {
  659. AZ_Error("Image Processing", false, "Pre-convolved environment map image must use linear colorspace");
  660. return false;
  661. }
  662. if (m_input->m_inputImage->GetPixelFormat() != ePixelFormat_R32G32B32A32F
  663. && m_input->m_inputImage->GetPixelFormat() != ePixelFormat_R16G16B16A16F)
  664. {
  665. AZ_Error("Image Processing", false, "Pre-convolved environment map image must be R32G32B32A32F or R16G16B16A16F");
  666. return false;
  667. }
  668. CubemapLayoutInfo* layoutInfo = CubemapLayout::GetCubemapLayoutInfo(m_input->m_inputImage);
  669. if (IsValidLatLongMap(m_input->m_inputImage) || layoutInfo->m_type != CubemapLayoutVertical)
  670. {
  671. AZ_Error("Image Processing", false, "Pre-convolved environment map image with multiple mips must be in Vertical layout format");
  672. return false;
  673. }
  674. }
  675. else if (CubemapLayout::GetCubemapLayoutInfo(m_input->m_inputImage) == nullptr && !IsValidLatLongMap(m_input->m_inputImage))
  676. {
  677. AZ_Error("Image Processing", false, "Environment map image size %dx%d is invalid. Requires power of two with 6x1, 1x6, 4x3 or 3x4 layouts"
  678. " or 2x1 latitude-longitude map", dwWidth, dwHeight);
  679. return false;
  680. }
  681. }
  682. else if (!CPixelFormats::GetInstance().IsImageSizeValid(dstFmt, dwWidth, dwHeight, false))
  683. {
  684. AZ_TracePrintf("Image processing", "Image size will be scaled for pixel format %s\n", CPixelFormats::GetInstance().GetPixelFormatInfo(dstFmt)->szName);
  685. }
  686. #if defined(AZ_TOOLS_EXPAND_FOR_RESTRICTED_PLATFORMS)
  687. #define AZ_RESTRICTED_PLATFORM_EXPANSION(CodeName, CODENAME, codename, PrivateName, PRIVATENAME, privatename, PublicName, PUBLICNAME, publicname, PublicAuxName1, PublicAuxName2, PublicAuxName3) \
  688. if (ImageProcess##PrivateName::DoesSupport(m_input->m_platform)) \
  689. { \
  690. if (!ImageProcess##PrivateName::IsPixelFormatSupported(m_input->m_presetSetting.m_pixelFormat)) \
  691. { \
  692. AZ_Error("Image Processing", false, "Unsupported pixel format %s for %s", \
  693. CPixelFormats::GetInstance().GetPixelFormatInfo(dstFmt)->szName, m_input->m_platform.c_str()); \
  694. return false; \
  695. } \
  696. }
  697. AZ_TOOLS_EXPAND_FOR_RESTRICTED_PLATFORMS
  698. #undef AZ_RESTRICTED_PLATFORM_EXPANSION
  699. #endif //AZ_TOOLS_EXPAND_FOR_RESTRICTED_PLATFORMS
  700. return true;
  701. }
  702. bool ImageConvertProcess::SaveOutput()
  703. {
  704. // if the folder wasn't specified, skip
  705. if (m_input->m_outputFolder.empty())
  706. {
  707. AZ_Error("Image Processing", false, "No output folder provided for saving");
  708. return false;
  709. }
  710. // [GFX TODO] [ATOM-781] Platform related image prepare need to be reworked on.
  711. // Disabled for now since it's not working properly for atom
  712. #if IMAGEBUILDER_ENABLE_PLATFORM_EXPORT_PREPARE
  713. #if defined(AZ_TOOLS_EXPAND_FOR_RESTRICTED_PLATFORMS)
  714. #define AZ_RESTRICTED_PLATFORM_EXPANSION(CodeName, CODENAME, codename, PrivateName, PRIVATENAME, privatename, PublicName, PUBLICNAME, publicname, PublicAuxName1, PublicAuxName2, PublicAuxName3) \
  715. if (ImageProcess##PrivateName::DoesSupport(m_input->m_platform)) \
  716. { \
  717. ImageProcess##PrivateName::PrepareImageForExport(m_image->Get()); \
  718. }
  719. AZ_TOOLS_EXPAND_FOR_RESTRICTED_PLATFORMS
  720. #undef AZ_RESTRICTED_PLATFORM_EXPANSION
  721. #endif //AZ_TOOLS_EXPAND_FOR_RESTRICTED_PLATFORMS
  722. #endif
  723. // cubemaps can have a specific subId, standard images use the subId specified in StreamingImageAsset
  724. uint32_t subId = IsConvertToCubemap() ? m_input->m_presetSetting.m_cubemapSetting->m_subId : RPI::StreamingImageAsset::GetImageAssetSubId();
  725. // Save the image to atom image assets
  726. ImageAssetProducer assetProducer(
  727. m_image->Get(),
  728. m_input->m_outputFolder,
  729. m_input->m_sourceAssetId,
  730. m_input->m_imageName,
  731. m_input->m_presetSetting.m_numResidentMips,
  732. subId,
  733. m_input->m_textureSetting.m_tags
  734. );
  735. if (assetProducer.BuildImageAssets())
  736. {
  737. m_jobProducts = assetProducer.GetJobProducts();
  738. return true;
  739. }
  740. AZ_Error("Image Processing", false, "Failed to generate StreamingImageAsset");
  741. return false;
  742. }
  743. ImageConvertProcess* CreateImageConvertProcess(const AZStd::string& imageFilePath, const AZStd::string& exportDir
  744. , const PlatformName& platformName, AZStd::vector<AssetBuilderSDK::JobProduct>& jobProducts, AZ::SerializeContext* context)
  745. {
  746. AZStd::unique_ptr<ImageConvertProcessDescriptor> desc = AZStd::make_unique<ImageConvertProcessDescriptor>();
  747. TextureSettings& textureSettings = desc->m_textureSetting;
  748. MultiplatformTextureSettings multiTextureSetting;
  749. bool canOverridePreset = false;
  750. multiTextureSetting = TextureSettings::GetMultiplatformTextureSetting(imageFilePath, canOverridePreset, context);
  751. if (multiTextureSetting.size() == 0)
  752. {
  753. AZ_Error("Image Processing", false, "Failed to generate texture setting");
  754. return nullptr;
  755. }
  756. if (multiTextureSetting.find(platformName) != multiTextureSetting.end())
  757. {
  758. textureSettings = multiTextureSetting[platformName];
  759. }
  760. else
  761. {
  762. PlatformName defaultPlatform = BuilderSettingManager::s_defaultPlatform;
  763. if (multiTextureSetting.find(defaultPlatform) != multiTextureSetting.end())
  764. {
  765. textureSettings = multiTextureSetting[defaultPlatform];
  766. }
  767. else
  768. {
  769. textureSettings = (*multiTextureSetting.begin()).second;
  770. }
  771. }
  772. // Load image. Do it earlier so GetSuggestedPreset function could use the information of file to choose better preset
  773. IImageObjectPtr srcImage(LoadImageFromFile(imageFilePath));
  774. if (srcImage == nullptr)
  775. {
  776. AZ_Error("Image Processing", false, "Load image file %s failed", imageFilePath.c_str());
  777. return nullptr;
  778. }
  779. // if get textureSetting failed, use the default texture setting, and find suitable preset for this file
  780. // in very rare user case, an old texture setting file may not have a preset. We fix it over here too.
  781. if (textureSettings.m_preset.IsEmpty())
  782. {
  783. textureSettings.m_preset = BuilderSettingManager::Instance()->GetSuggestedPreset(imageFilePath);
  784. }
  785. // Get preset
  786. AZStd::string_view filePath;
  787. const PresetSettings* preset = BuilderSettingManager::Instance()->GetPreset(textureSettings.m_preset, platformName, &filePath);
  788. if (preset == nullptr)
  789. {
  790. AZ_Assert(false, "%s cannot find image preset %s.", imageFilePath.c_str(), textureSettings.m_preset.GetCStr());
  791. return nullptr;
  792. }
  793. desc->m_presetSetting = *preset;
  794. desc->m_platform = platformName;
  795. desc->m_filePath = filePath;
  796. desc->m_inputImage = srcImage;
  797. desc->m_isPreview = false;
  798. desc->m_isStreaming = BuilderSettingManager::Instance()->GetBuilderSetting(platformName)->m_enableStreaming;
  799. desc->m_outputFolder = exportDir;
  800. desc->m_jobProducts = &jobProducts;
  801. AZ::StringFunc::Path::GetFullFileName(imageFilePath.c_str(), desc->m_imageName);
  802. // Get source asset id. Create random id if it's not found which is useful if this functions wasn't called under asset builder environment. For example, unit test.
  803. AZStd::string watchFolder;
  804. AZ::Data::AssetInfo catalogAssetInfo;
  805. bool sourceInfoFound = false;
  806. AzToolsFramework::AssetSystemRequestBus::BroadcastResult(sourceInfoFound, &AzToolsFramework::AssetSystemRequestBus::Events::GetSourceInfoBySourcePath,
  807. imageFilePath.c_str(), catalogAssetInfo, watchFolder);
  808. desc->m_sourceAssetId = sourceInfoFound ? catalogAssetInfo.m_assetId : AZ::Data::AssetId(AZ::Uuid::CreateRandom());
  809. // Create convert process
  810. ImageConvertProcess* process = new ImageConvertProcess(AZStd::move(desc));
  811. return process;
  812. }
  813. bool ImageConvertProcess::CreateIBLCubemap(PresetName preset, const char* fileNameSuffix, IImageObjectPtr& cubemapImage)
  814. {
  815. const AZStd::string& platformId = m_input->m_platform;
  816. AZStd::string_view filePath;
  817. const PresetSettings* presetSettings = BuilderSettingManager::Instance()->GetPreset(preset, platformId, &filePath);
  818. if (presetSettings == nullptr)
  819. {
  820. AZ_Error("Image Processing", false, "Couldn't find preset for IBL cubemap generation");
  821. return false;
  822. }
  823. // generate export file name
  824. AZStd::string fileName;
  825. AZ::StringFunc::Path::GetFileName(m_input->m_imageName.c_str(), fileName);
  826. fileName += fileNameSuffix;
  827. AZStd::string extension;
  828. AZ::StringFunc::Path::GetExtension(m_input->m_imageName.c_str(), extension);
  829. fileName += extension;
  830. AZStd::string outProductPath;
  831. AZ::StringFunc::Path::Join(m_input->m_outputFolder.c_str(), fileName.c_str(), outProductPath, true, true);
  832. // the diffuse irradiance cubemap is generated with a separate ImageConvertProcess
  833. TextureSettings textureSettings = m_input->m_textureSetting;
  834. textureSettings.m_preset = preset;
  835. AZStd::unique_ptr<ImageConvertProcessDescriptor> desc = AZStd::make_unique<ImageConvertProcessDescriptor>();
  836. desc->m_presetSetting = *presetSettings;
  837. desc->m_textureSetting = textureSettings;
  838. desc->m_platform = platformId;
  839. desc->m_filePath = filePath;
  840. desc->m_inputImage = m_input->m_inputImage;
  841. desc->m_isPreview = false;
  842. desc->m_isStreaming = m_input->m_isStreaming;
  843. desc->m_outputFolder = m_input->m_outputFolder;
  844. desc->m_imageName = fileName;
  845. desc->m_sourceAssetId = m_input->m_sourceAssetId;
  846. AZStd::unique_ptr<ImageConvertProcess> imageConvertProcess = AZStd::make_unique<ImageConvertProcess>(AZStd::move(desc));
  847. if (!imageConvertProcess)
  848. {
  849. AZ_Error("Image Processing", false, "Failed to create image convert process for the IBL cubemap");
  850. return false;
  851. }
  852. imageConvertProcess->ProcessAll();
  853. if (!imageConvertProcess->IsSucceed())
  854. {
  855. AZ_Error("Image Processing", false, "Image convert process for the IBL cubemap failed");
  856. return false;
  857. }
  858. // append the output products to the job's product list
  859. imageConvertProcess->GetAppendOutputProducts(*m_input->m_jobProducts);
  860. // store the output cubemap so it can be accessed by unit tests
  861. cubemapImage = imageConvertProcess->m_image->Get();
  862. return true;
  863. }
  864. bool ConvertImageFile(const AZStd::string& imageFilePath, const AZStd::string& exportDir,
  865. const PlatformName& platformName, AZ::SerializeContext* context, AZStd::vector<AssetBuilderSDK::JobProduct>& outProducts)
  866. {
  867. bool result = false;
  868. ImageConvertProcess* process = CreateImageConvertProcess(imageFilePath, exportDir, platformName, outProducts, context);
  869. if (process)
  870. {
  871. process->ProcessAll();
  872. result = process->IsSucceed();
  873. if (result)
  874. {
  875. process->GetAppendOutputProducts(outProducts);
  876. }
  877. delete process;
  878. }
  879. return result;
  880. }
  881. IImageObjectPtr ConvertImageForPreview(IImageObjectPtr image)
  882. {
  883. if (!image)
  884. {
  885. return IImageObjectPtr();
  886. }
  887. ImageToProcess imageToProcess(image);
  888. imageToProcess.ConvertFormat(ePixelFormat_R8G8B8A8);
  889. IImageObjectPtr previewImage = imageToProcess.Get();
  890. return previewImage;
  891. }
  892. IImageObjectPtr GetUncompressedLinearImage(IImageObjectPtr ddsImage)
  893. {
  894. if (ddsImage)
  895. {
  896. ImageToProcess processImage(ddsImage);
  897. if (!CPixelFormats::GetInstance().IsPixelFormatUncompressed(ddsImage->GetPixelFormat()))
  898. {
  899. processImage.ConvertFormat(ePixelFormat_R32G32B32A32F);
  900. }
  901. if (ddsImage->HasImageFlags(EIF_SRGBRead))
  902. {
  903. processImage.GammaToLinearRGBA32F(true);
  904. }
  905. return processImage.Get();
  906. }
  907. return nullptr;
  908. }
  909. float GetErrorBetweenImages(IImageObjectPtr inputImage1, IImageObjectPtr inputImage2)
  910. {
  911. // First make sure images are in uncompressed format and linear space
  912. // Convert them if necessary
  913. IImageObjectPtr image1 = GetUncompressedLinearImage(inputImage1);
  914. IImageObjectPtr image2 = GetUncompressedLinearImage(inputImage2);
  915. const float errorValue = FLT_MAX;
  916. if (!image1 || !image2)
  917. {
  918. AZ_Warning("Image Processing", false, "Invalid images passed into %s function", __FUNCTION__);
  919. return errorValue;
  920. }
  921. // Two images should share same size
  922. if (image1->GetWidth(0) != image2->GetWidth(0) || image1->GetHeight(0) != image2->GetHeight(0))
  923. {
  924. AZ_Warning("Image Processing", false, "%s function only can get error between two images with same size", __FUNCTION__);
  925. return errorValue;
  926. }
  927. //create pixel operation function
  928. IPixelOperationPtr pixelOp1 = CreatePixelOperation(image1->GetPixelFormat());
  929. IPixelOperationPtr pixelOp2 = CreatePixelOperation(image2->GetPixelFormat());
  930. //get count of bytes per pixel
  931. AZ::u32 pixelBytes1 = CPixelFormats::GetInstance().GetPixelFormatInfo(image1->GetPixelFormat())->bitsPerBlock / 8;
  932. AZ::u32 pixelBytes2 = CPixelFormats::GetInstance().GetPixelFormatInfo(image2->GetPixelFormat())->bitsPerBlock / 8;
  933. float color1[4];
  934. float color2[4];
  935. AZ::u8* mem1;
  936. AZ::u8* mem2;
  937. uint32 pitch1, pitch2;
  938. float sumDeltaSqLinear = 0;
  939. //only process the highest mip
  940. image1->GetImagePointer(0, mem1, pitch1);
  941. image2->GetImagePointer(0, mem2, pitch2);
  942. const uint32 pixelCount = image1->GetPixelCount(0);
  943. for (uint32 i = 0; i < pixelCount; ++i)
  944. {
  945. pixelOp1->GetRGBA(mem1, color1[0], color1[1], color1[2], color1[3]);
  946. pixelOp2->GetRGBA(mem2, color2[0], color2[1], color2[2], color2[3]);
  947. sumDeltaSqLinear += (color1[0] - color2[0]) * (color1[0] - color2[0])
  948. + (color1[1] - color2[1]) * (color1[1] - color2[1])
  949. + (color1[2] - color2[2]) * (color1[2] - color2[2]);
  950. mem1 += pixelBytes1;
  951. mem2 += pixelBytes2;
  952. }
  953. return sumDeltaSqLinear / pixelCount;
  954. }
  955. void GetBC1CompressionErrors(IImageObjectPtr originImage, float& errorLinear, float& errorSrgb,
  956. ICompressor::CompressOption option)
  957. {
  958. errorLinear = 0;
  959. errorSrgb = 0;
  960. if (originImage->HasImageFlags(EIF_SRGBRead))
  961. {
  962. AZ_Assert(false, "The input origin image of %s function need be in linear color space", __FUNCTION__);
  963. return;
  964. }
  965. //compress and decompress in linear space
  966. ImageToProcess processLinear(originImage);
  967. processLinear.SetCompressOption(option);
  968. processLinear.ConvertFormat(ePixelFormat_BC1);
  969. processLinear.ConvertFormat(ePixelFormat_R32G32B32A32F);
  970. errorLinear = GetErrorBetweenImages(originImage, processLinear.Get());
  971. //compress and decompress in sRGB space, then convert back to linear space to compare to original image
  972. ImageToProcess processSrgb(originImage);
  973. processSrgb.SetCompressOption(option);
  974. processSrgb.LinearToGamma();
  975. processSrgb.ConvertFormat(ePixelFormat_BC1);
  976. processSrgb.ConvertFormat(ePixelFormat_R32G32B32A32F);
  977. processSrgb.GammaToLinearRGBA32F(true);
  978. errorSrgb = GetErrorBetweenImages(originImage, processSrgb.Get());
  979. }
  980. }// namespace ImageProcessingAtom