From 19b009c8dfa000cc83bfd278a13e5aae517cb48b Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 29 Jan 2024 16:45:06 -0500 Subject: [PATCH] ENH: ITKImportImageStack - Add option to convert images to grayscale 'on-the-fly'. (#832) Signed-off-by: Michael Jackson --- .../data/ImageStack/rgb_0.png | Bin 0 -> 2601 bytes .../data/ImageStack/rgb_1.png | Bin 0 -> 2579 bytes .../data/ImageStack/rgb_2.png | Bin 0 -> 2580 bytes .../Filters/ITKImportImageStack.cpp | 151 ++++++++++++++--- .../Filters/ITKImportImageStack.hpp | 3 +- .../test/ITKImportImageStackTest.cpp | 160 ++++++++++++++++-- 6 files changed, 275 insertions(+), 39 deletions(-) create mode 100644 src/Plugins/ITKImageProcessing/data/ImageStack/rgb_0.png create mode 100644 src/Plugins/ITKImageProcessing/data/ImageStack/rgb_1.png create mode 100644 src/Plugins/ITKImageProcessing/data/ImageStack/rgb_2.png diff --git a/src/Plugins/ITKImageProcessing/data/ImageStack/rgb_0.png b/src/Plugins/ITKImageProcessing/data/ImageStack/rgb_0.png new file mode 100644 index 0000000000000000000000000000000000000000..6641d2e7ffe4671b89d64919d309be3baf3d2ab7 GIT binary patch literal 2601 zcmbtWdpy+X8h>XD8X7T^nw3=ij!Emd>=L>eOw44M+}cjAW!v}&V@6WQ&n+{2)Q&Fh z8wwSd=q#H?lH9r&=STX~?x{WJbIxDS?|nbd`@GNdeZSB5`EhqT z+ToNmlmGxY8kOP<0D36^sN+k}$WA)RF#<6dwv81XWekjq?&T)}fR)fFR<7Y*&&A@* zd|P6IQ-IcsxwCCCSeqXM?cK?46+KSYy0mLbWosG-Uxq(|FYnNM)p+%6XPM<~d2(R# z50RHnk4xtyW^)FICda)vn9!lOH5ZZt2`TIQqb2ZnfukWuWv(gjjNBaz{fjK4izw)+ zedl*MWp&oazGaCt&K;VOmnzH6yiRZ0A_?1%Zmk@&=xpcl#X%?M^yg4Q*}aycG(x;! z$_e@@IL=JY-2NFRqK_#NoW?~HVbp_BIn}7DS}YuZ#{z90So_iBpBxoqjEt@oMcjZ; z9+`bz^&~uq9&U=I0#z=C>3_nej)WI>w{6b*N;zmuk^FtCgs!C_mqNfg>MZNnwz4Z^ zTiQ2Pl_lBP{@ds?s@8EO`B7sdyCusJ7&OaKKU)siCyS@OY$e3#pw^%e&Ej`CI{iBE ztxm<#z>#BZ_KKRp)2ki8#O*_PK+#5#L$6RLA_$yg(X8Bn-XsjjGH2ql+Y4OVzM%ot z3C9{Xl-;fg5 zE!7-u@Bk)~t2n08DdbUSvr&g?4if;6hK4V4sr*?$&dTI8opJ$s%U3JsaWZ73{!LlM<9hPH?40v26TK%}eWw_u*CAASkQb-7v3>~;?5@a+?@sU~EHue(fu!k2&cX}m7Z5V; zjmK~?!lYiUI)j>6R-5-Kn30a~OTeY*HNRE>ic%fCi&iLFLXAQw=Q5$j9eygfF#_yZ zVFci`<`f1Lss6e5K3hlrTNX;FQkW>>U-I*&0Y z>>rU%QYWSO%)jBUgp2Lp3NV{Y%VMo3n!hb|e@VYS5oYeK>K7x0_Ec!ZbtiyCV}wx_ zx*rZN#&~b6q6(I$CEp-=(Czk*?`(dzKh&5&Vi~VC`Q5at$K~$6!-T5AtExfLi}z%Lp|$GU@kpiX2ssZN($p(Ch|_7@9k;s!q@25X64Eq$eFyLsyQphz$fO> z`q+(BNTQ>aDl+9_4(1X(kY2FM*1v07e$%9B@=k{aFITQv1!W;yNcq z;85lB-Yj<&A9~kgHTRm}bO<)~w^5%@>r2Nwhp7D9gmOYwnu0h@`SG|66_Fn7z9WS9 zVq@~(+2ZelUqPsyozEu51Q(LJ=X%4R$xbBUvWI4eO5XJNOTHSMJX126l?nOmPKgfL zCOc4kReZ2RTDD6%sE#G}NRP+8+|sC2qPzFdz^*poAZ`U)(p zrr)4?|IM+rcu;@YE!Ir))Vz{PFp81#g-SfI=Yn<$aq1pESGkNd@L;`ZGla_aFJiMU zA|H#Y`_xPDwOk|v$_+-ECUz?_5b4ARSs#!hw+Ss_u5;OjG|_8(DzS(S1Jhij^36Jz9mQA{<|ag+yf(;kTlc}MHa);554WG9q?n^Da&bLA_Xm> zw;Q_QMh_pAQCB&}$op6p=a>f&w$38dgjeL8N$i_r4+>4mgoj&(eeclnm)CZ}G86uQ z1~YYL+Q(k6c=HUHdF0329dAVf)tSpo&TrqBC;Ana8`agqzT=Ark;A`oMI>&Dg{F!Q`wiF+I(VIzLuA0`O>HhBGPfXw(XGb=?+#uI%6 z2`kq}w@uWraNhmr@yWvvpBy#Y^?FO_frf%@GtZ0PHhw!VZ%lEvbDdsC{oX01bSSf{ z)9&(UDT7F}`+7X0p=#WoG|G}!>6(ZM$jbX6W1U2sjz3tvXUQ6fc+c{>HqB>TH}p5; P-w)8XJ5oxmePaIw?#<=j literal 0 HcmV?d00001 diff --git a/src/Plugins/ITKImageProcessing/data/ImageStack/rgb_1.png b/src/Plugins/ITKImageProcessing/data/ImageStack/rgb_1.png new file mode 100644 index 0000000000000000000000000000000000000000..13ed8c8ae244ea3d7979dd954bc52a045b7bec77 GIT binary patch literal 2579 zcmai0dpwls9)I51>7zjnV@kOUrinHxYBeqi8EYnHqCHx}^E%rv`^T*}CZL#ETtGN@rxI>6l0H_uU$?+eoA4X)dh;B0W ztcOj%BK~mOiuKN#A&7x7^4*Shb^czxkg4!LwmeP@yaSa6^UlBb8`>E4{=wdpu2==* z9T40&YM#1jdW*+pvT&y5o@ZS={cxR~YVaw)fDPi2vewC%l^j4T9Nzw@KLzP7lQ?6W z_$R%%zz~Cy$DPxx(L@ziAH-k}?du?bWH$^T;UNbCT6QJ=Rzf@ZeI1#<=C)%!!7*IDT+Q3o%wlKQEyz@S{Q-*X1$^eED zjh0F4wNdA9xnHIv(A_FIo%<1o=Yrqo#GlEzZN(SZ4TOXSV8zay>16P{e81B&PQlo}XrXG&&&iXZM)#7$bz;(%xndZzbHC=uOQYeT`M|9NFH)3Xe z7;R~n?XD1pG*E>bCfrCC5tm=uYI zdO*BuG|?`9lmD~|!aa$ir0-g@Wske)ck)BpR%@v!f{u$stgie9BltRxP3MI*9!EaYPV@X5;~)yzWb^-Bw3?-)i@qvrXamjV0u9ooAL62gm1^F1f~91z}plLRpB zJrRq`feH3?Q<--H<|99!VD?dgtm2BDJ!ft@;USIlF$1@?@X)w#WndOeSLIZfItQ?d z9uaoI#tpDUH(e;Gn4BiVRLOg1%tUP_5~wM-&EwiP_BepQnzN+ZMe!~bt5UK3V4%UG z%bBgvXO9;3Y-nIdL>^L5brh#}Ox#F-LKr(8vP{Iz1oquQQA+Z>=AZnwGCUX$4O4g0 zM585`(X0P#Drg*z@6(w*RPgxGEW{3NZdk8$Q5j{4?a#Oc+wY`(O&Drjn0pXcOSZm9 zLEPA}Kg^KB_9mR(4@X04$@Ou3zr%u9!yI{*CZh+8;HaVzq2)eZ;OR^k-}}nEGsDbv zCrpp!!Ka;9f)W{dSybmbWBAjxd}w*-Eu!*ifs+vMnYFzfAKEOJf*6o?L^ss{4wyNd zF>RvMhql*pnSUV;;;vV`#1;AGlZC_wy9SU_V~il#CnzFx6rncHf;Sd*gbwjX{vvNe z3i}VjAw6>{1IS;0An(vIW)H~yS=p5c7h?1kt!3@jSjXVd#pr(U9i zB+`GCnzBdAUt;*^r)aUR9o+!VuI_<~dU5TlIq?+t^=($q?qrdTe; z%t|I|*{t3yC=$mPQ!$`e-_~uxkb1*^uIEYpW!_^946iD7jqoWey_Eok)m|MtkQB(8 zneHelH8dqXYkbN3&gxGh+V?@Zic5drn=rC(Ur_6`$=3J30ILo&jMSH#^=?D4nw()u zy6>@0e&c`__nMm!?A~r0!WS5OocH4aHNQBuvQ@6Y_1u$E{`!<08Zg_b-q4P>$Y(@6 zKm}BlIzRi7iU_bvzY0j&xdh;q_QhE#xOXdl*xFo)zJiG`-2Ndk1jN?vFg)zapgtYt z)nopG@~KJjQ0DX0RvTEc%^ydhRe`J{g!C+~Y!IrvYom+`SmXTX;k90@i$n_W7150@ zYb_7uJIQAmf9!*YQJAB`dAQ$&wLJb4#;Ve=uzAOeEzmZQwlY5O^~4_~X95xYA^Wxm WF`ACA!&&ex4^W(GB>tX&g#QD!AJ^po literal 0 HcmV?d00001 diff --git a/src/Plugins/ITKImageProcessing/data/ImageStack/rgb_2.png b/src/Plugins/ITKImageProcessing/data/ImageStack/rgb_2.png new file mode 100644 index 0000000000000000000000000000000000000000..05cc4f9335958f6af28aeac94f57e20b5bbdcf01 GIT binary patch literal 2580 zcmai0eLPfY8-C6)Oh*ypEwoyi=AEfEerY8oH6xQ|!h}>v8d4Dr4O>E-d^OXTP*US7 z`zus4l`>3Wr_!plLM@Vy$s*-*Um{|>XVkWN>)rl2_wPBs^E~(cT-SYF_YpX<*J`NG zQ3n8Mu$T;I0N@z_K=IRH>`f|#9gYQDu#L4nv=}VTsmVSJ08+tXSi6LJ_B2%|wMu5k zJDqwn9JQk|PDgXcD1Ht#+|IL$c=}`>D_$)sz-?(yScxZ)TTEOr1n2}G2NIb;OMo+t zuXXp|Grk~QP&QP2%FDR0u;!1JvC)J8HVLiC@ZpSXULOgyymzC?>bif37lm5P6_4lx z8&|U0;VbC^?37>vU=SfXiJ(=u8Qy2Hcb~-Rt_~Tg>2nf3;F=s7_pWxi9=u;AuRK$}yZ^6ErPPuhi5^NC1QV6Yb>7`sNo5*M%8yY$iMu z-_1|?WoX49zpBG3lODLDNp(mOBzY3GS96h>{2}gEgWvwAJ+2*DL#1cpKI#ptQz^!odyzd=O{@nn>dtoda1U%sBfNHB`91!X#v5*Ig z%qfRBT;iR>+yhleYN}3M1ovUg8AnR5E?qUbB_&2|P&~Rg^UqSTVI9>1(mG4Z60fo|~&9ysM_GIM%`!>NPg^*7zWU_iWiNxUKKGF#lSO+_ zpa4z*Q?M=tD@u^7VmJ^$YG)KfmYRjCTTTzvJBr+IdbSgF+ z-u?Zq&S^~9SV(ljw}tanHGTP#!x0tKHjFw+<`ZPOZC-fH3dDvgXhpks5|AU9CP&?9 zK__1{WVbIuE!Wu-Dpx_zT!;jqe59m;-TA7s(cCf;M0X%U-DM_AY%vr{y@!W{y#sRT z96k${yfpidPDa{Z0WRYHVegKeQlviSC#kj4AKEYeKy83k1q{S)UI?6P`{BY51pa`C z2|)?{#*xvQrlM@T{NSr}Qn!U_=_St6EaeT?httz$Mqde5rvwoX`ynQjNsxGgY{kSL1 z@Sr^*gnQPzEnS}{2240Up+ltBGcVK%<$1h%U3u*2-w4a+C=!isC z+h&Q#L+$3Zoj0m*kloq2(#sT&9jP=C%yoHZ!xD`8HI$&S;m`Q`hTY5h8-Fb_Oa$cf zIxG1&)8ck+ZLeQb%$7KnFH}5L(|@nLU3!nt=%$n6Ln>m}V5^|4zOhWV^O5K-^btpc zC$`7(u_*+Cc5S?R!=zeBs?S&-7aa@}C7rmw4g7pYbdO13Xka+`+Q6>#9qKJ3 zw9zzXRorUR3*trH@0UJ%9g7is;>@L>ck>!ZP|!6OzWU^lKv^ydD}{&qhII*jEE1}J zYzu!)-1k?g>+_7o(7GbetD@8O_94bx|2-Wn@4@x6W$(px{&DxZhBawHA_X2}dZR(J zw4C9vf@P&}g5F*BqX_QB%rHIVhiSBQbF2O6rcHMCX5-ah=(^=ewmB;omPszIq(CzK zrc6nWJNMQ?p0&EH^Wr%Y*UnEJx+ci0WXXgcwkm<=b|;zA`!(axVYl0OqkK$)|2%qS z3vbpoCbp|!hTPWt7qL@)56$lG%7K+6f8oS`&rFWh?XCW=EfF;FN<-CqJ;jYkfO<7i ze3BzmNkA>w_|6-k=z^b}!5?^qt#23%w=66hX22Y)ZI$@G$r%n(t#h8QCIRxF^@8%X zibu0uQPxe#c>M1@QES0PuaDD$5&dNRZEJcHm!%f4hV-tAR}?i-G|{n0Wt}#%)Cibf ao`8H_F4+|$ZKYwqd4RQs%@D8VME@5y@Zrw@ literal 0 HcmV?d00001 diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImportImageStack.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImportImageStack.cpp index bd694bdaa4..ca69c7b742 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImportImageStack.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImportImageStack.cpp @@ -3,6 +3,7 @@ #include "ITKImageProcessing/Common/ITKArrayHelper.hpp" #include "ITKImageProcessing/Filters/ITKImageReader.hpp" +#include "simplnx/Common/TypesUtility.hpp" #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/DataPath.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" @@ -57,6 +58,8 @@ const ChoicesParameter::ValueType k_FlipAboutYAxis = 2; const Uuid k_SimplnxCorePluginId = *Uuid::FromString("05cc618b-781f-4ac0-b9ac-43f26ce1854f"); const Uuid k_RotateSampleRefFrameFilterId = *Uuid::FromString("d2451dc1-a5a1-4ac2-a64d-7991669dcffc"); const FilterHandle k_RotateSampleRefFrameFilterHandle(k_RotateSampleRefFrameFilterId, k_SimplnxCorePluginId); +const Uuid k_ColorToGrayScaleFilterId = *Uuid::FromString("d938a2aa-fee2-4db9-aa2f-2c34a9736580"); +const FilterHandle k_ColorToGrayScaleFilterHandle(k_ColorToGrayScaleFilterId, k_SimplnxCorePluginId); // Make sure we can instantiate the RotateSampleRefFrame Filter std::unique_ptr CreateRotateSampleRefFrameFilter() @@ -132,7 +135,8 @@ namespace cxITKImportImageStack { template Result<> ReadImageStack(DataStructure& dataStructure, const DataPath& imageGeomPath, const std::string& cellDataName, const DataPath& imageDataPath, const std::vector& files, - ChoicesParameter::ValueType transformType, const IFilter::MessageHandler& messageHandler, const std::atomic_bool& shouldCancel) + ChoicesParameter::ValueType transformType, bool convertToGrayscale, VectorFloat32Parameter::ValueType luminosityValues, const IFilter::MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel) { auto& imageGeom = dataStructure.getDataRefAs(imageGeomPath); @@ -147,13 +151,21 @@ Result<> ReadImageStack(DataStructure& dataStructure, const DataPath& imageGeomP // Variables for the progress Reporting usize slice = 0; + auto* filterListPtr = Application::Instance()->getFilterList(); + + if(convertToGrayscale && !filterListPtr->containsPlugin(k_SimplnxCorePluginId)) + { + return MakeErrorResult(-18542, "SimplnxCore was not instantiated in this instance, so color to grayscale is not a valid option."); + } + auto grayScaleFilter = filterListPtr->createFilter(k_ColorToGrayScaleFilterHandle); + Result<> outputResult = {}; + // Loop over all the files importing them one by one and copying the data into the data array for(const auto& filePath : files) { messageHandler(IFilter::Message::Type::Info, fmt::format("Importing: {}", filePath)); DataStructure importedDataStructure; - { // Create a sub-filter to read each image, although for preflight we are going to read the first image in the // list and hope the rest are correct. @@ -171,6 +183,50 @@ Result<> ReadImageStack(DataStructure& dataStructure, const DataPath& imageGeomP return executeResult.result; } } + + // ======================= Convert to GrayScale Section =================== + bool validInputForGrayScaleConversion = importedDataStructure.getDataRefAs(imageDataPath).getDataType() == DataType::uint8; + if(convertToGrayscale && validInputForGrayScaleConversion && nullptr != grayScaleFilter.get()) + { + + // This same filter was used to preflight so as long as nothing changes on disk this really should work.... + Arguments colorToGrayscaleArgs; + colorToGrayscaleArgs.insertOrAssign("conversion_algorithm", std::make_any(0)); + colorToGrayscaleArgs.insertOrAssign("color_weights", std::make_any(luminosityValues)); + colorToGrayscaleArgs.insertOrAssign("input_data_array_vector", std::make_any>(std::vector{imageDataPath})); + colorToGrayscaleArgs.insertOrAssign("output_array_prefix", std::make_any("gray")); + + // Run grayscale filter and process results and messages + auto result = grayScaleFilter->execute(importedDataStructure, colorToGrayscaleArgs).result; + if(result.invalid()) + { + return result; + } + + // deletion of non-grayscale array + DataObject::IdType id; + { // scoped for safety since this reference will be nonexistent in a moment + auto& oldArray = importedDataStructure.getDataRefAs(imageDataPath); + id = oldArray.getId(); + } + importedDataStructure.removeData(id); + + // rename grayscale array to reflect original + { + auto& gray = importedDataStructure.getDataRefAs(imageDataPath.getParent().createChildPath("gray" + imageDataPath.getTargetName())); + if(!gray.canRename(imageDataPath.getTargetName())) + { + return MakeErrorResult(-64543, fmt::format("Unable to rename the internal grayscale array to {}", imageDataPath.getTargetName())); + } + gray.rename(imageDataPath.getTargetName()); + } + } + else if(convertToGrayscale && !validInputForGrayScaleConversion) + { + outputResult.warnings().emplace_back(Warning{ + -74320, fmt::format("The array ({}) resulting from reading the input image file is not a UInt8Array. The input image will not be converted to grayscale.", imageDataPath.getTargetName())}); + } + // Check the ImageGeometry of the imported Image matches the destination const auto& importedImageGeom = importedDataStructure.getDataRefAs(imageGeomPath); SizeVec3 importedDims = importedImageGeom.getDimensions(); @@ -206,11 +262,11 @@ Result<> ReadImageStack(DataStructure& dataStructure, const DataPath& imageGeomP // Check to see if the filter got canceled. if(shouldCancel) { - return {}; + return outputResult; } } - return {}; + return outputResult; } } // namespace cxITKImportImageStack @@ -256,6 +312,10 @@ Parameters ITKImportImageStack::parameters() const params.insert(std::make_unique(k_Spacing_Key, "Spacing", "The spacing of the 3D volume", std::vector{1.0F, 1.0F, 1.0F}, std::vector{"X", "y", "Z"})); params.insertLinkableParameter(std::make_unique(k_ImageTransformChoice_Key, "Optional Slice Operations", "Operation that is performed on each slice. 0=None, 1=Flip about X, 2=Flip about Y", 0, k_SliceOperationChoices)); + params.insertLinkableParameter( + std::make_unique(k_ConvertToGrayScale_Key, "Convert To GrayScale", "The filter will show an error if the images are already in grayscale format", false)); + params.insert(std::make_unique(k_ColorWeights_Key, "Color Weighting", "RGB weights for the grayscale conversion using the luminosity algorithm.", + std::vector{0.2125f, 0.7154f, 0.0721f}, std::vector({"Red", "Green", "Blue"}))); params.insertSeparator(Parameters::Separator{"File List"}); params.insert( @@ -266,6 +326,8 @@ Parameters ITKImportImageStack::parameters() const params.insert(std::make_unique(k_CellDataName_Key, "Cell Data Name", "The name of the created cell attribute matrix", ImageGeom::k_CellDataName)); params.insert(std::make_unique(k_ImageDataArrayPath_Key, "Created Image Data", "The path to the created image data array", "ImageData")); + params.linkParameters(k_ConvertToGrayScale_Key, k_ColorWeights_Key, true); + return params; } @@ -283,11 +345,17 @@ IFilter::PreflightResult ITKImportImageStack::preflightImpl(const DataStructure& auto origin = filterArgs.value(k_Origin_Key); auto spacing = filterArgs.value(k_Spacing_Key); auto imageGeomPath = filterArgs.value(k_ImageGeometryPath_Key); - auto imageDataName = filterArgs.value(k_ImageDataArrayPath_Key); + auto pImageDataArrayNameValue = filterArgs.value(k_ImageDataArrayPath_Key); auto cellDataName = filterArgs.value(k_CellDataName_Key); auto imageTransformValue = filterArgs.value(k_ImageTransformChoice_Key); + auto pConvertToGrayScaleValue = filterArgs.value(k_ConvertToGrayScale_Key); + auto pColorWeightsValue = filterArgs.value(k_ColorWeights_Key); - const DataPath imageDataPath = imageGeomPath.createChildPath(cellDataName).createChildPath(imageDataName); + PreflightResult preflightResult; + nx::core::Result resultOutputActions = {}; + std::vector preflightUpdatedValues; + + const DataPath imageDataPath = imageGeomPath.createChildPath(cellDataName).createChildPath(pImageDataArrayNameValue); if(imageTransformValue != k_NoImageTransform) { @@ -314,7 +382,7 @@ IFilter::PreflightResult ITKImportImageStack::preflightImpl(const DataStructure& imageReaderArgs.insertOrAssign(ITKImageReader::k_FileName_Key, std::make_any(files.at(0))); const ITKImageReader imageReader; - PreflightResult imageReaderResult = imageReader.preflight(dataStructure, imageReaderArgs, messageHandler); + PreflightResult imageReaderResult = imageReader.preflight(dataStructure, imageReaderArgs, messageHandler, shouldCancel); if(imageReaderResult.outputActions.invalid()) { return imageReaderResult; @@ -326,7 +394,7 @@ IFilter::PreflightResult ITKImportImageStack::preflightImpl(const DataStructure& const auto* createImageGeomActionPtr = dynamic_cast(action0Ptr); if(createImageGeomActionPtr == nullptr) { - throw std::runtime_error("ITKImportImageStack: Expected CreateImageGeometryAction at index 0"); + throw std::runtime_error(fmt::format("{}: Expected CreateImageGeometryAction at index 0", this->humanName())); } // The second action should be the array creation @@ -334,7 +402,7 @@ IFilter::PreflightResult ITKImportImageStack::preflightImpl(const DataStructure& const auto* createArrayActionPtr = dynamic_cast(action1Ptr); if(createArrayActionPtr == nullptr) { - throw std::runtime_error("ITKImportImageStack: Expected CreateArrayAction at index 1"); + throw std::runtime_error(fmt::format("{}: Expected CreateArrayAction at index 1", this->humanName())); } // X Y Z @@ -344,11 +412,36 @@ IFilter::PreflightResult ITKImportImageStack::preflightImpl(const DataStructure& // Z Y X const std::vector arrayDims(dims.crbegin(), dims.crend()); - OutputActions outputActions; - outputActions.appendAction(std::make_unique(std::move(imageGeomPath), std::move(dims), std::move(origin), std::move(spacing), cellDataName)); - outputActions.appendAction(std::make_unique(createArrayActionPtr->type(), arrayDims, createArrayActionPtr->componentDims(), imageDataPath)); + // OutputActions outputActions; + resultOutputActions.value().appendAction(std::make_unique(std::move(imageGeomPath), std::move(dims), std::move(origin), std::move(spacing), cellDataName)); + + if(createArrayActionPtr->type() != DataType::uint8 && pConvertToGrayScaleValue) + { + return MakePreflightErrorResult(-23504, fmt::format("The input DataType is {} which cannot be converted to grayscale. Please turn off the 'Convert To Grayscale' option.", + nx::core::DataTypeToString(createArrayActionPtr->type()))); + } + + if(pConvertToGrayScaleValue) + { + auto* filterListPtr = Application::Instance()->getFilterList(); + if(!filterListPtr->containsPlugin(k_SimplnxCorePluginId)) + { + return MakePreflightErrorResult(-23501, "Color to GrayScale conversion is disabled because the 'SimplnxCore' plugin was not loaded."); + } + auto grayScaleFilter = filterListPtr->createFilter(k_ColorToGrayScaleFilterHandle); + if(nullptr == grayScaleFilter.get()) + { + return MakePreflightErrorResult(-23502, "Color to GrayScale conversion is disabled because the 'Color to GrayScale' filter is missing from the SimplnxCore plugin."); + } + resultOutputActions.value().appendAction(std::make_unique(createArrayActionPtr->type(), arrayDims, std::vector{1ULL}, imageDataPath)); + } + else + { + resultOutputActions.value().appendAction(std::make_unique(createArrayActionPtr->type(), arrayDims, createArrayActionPtr->componentDims(), imageDataPath)); + } - return {std::move(outputActions)}; + // Return both the resultOutputActions and the preflightUpdatedValues via std::move() + return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; } //------------------------------------------------------------------------------ @@ -362,6 +455,8 @@ Result<> ITKImportImageStack::executeImpl(DataStructure& dataStructure, const Ar auto imageDataName = filterArgs.value(k_ImageDataArrayPath_Key); auto cellDataName = filterArgs.value(k_CellDataName_Key); auto imageTransformValue = filterArgs.value(k_ImageTransformChoice_Key); + auto convertToGrayScaleValue = filterArgs.value(k_ConvertToGrayScale_Key); + auto colorWeightsValue = filterArgs.value(k_ColorWeights_Key); const DataPath imageDataPath = imageGeomPath.createChildPath(cellDataName).createChildPath(imageDataName); @@ -384,43 +479,53 @@ Result<> ITKImportImageStack::executeImpl(DataStructure& dataStructure, const Ar switch(*numericType) { case NumericType::uint8: { - readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, messageHandler, shouldCancel); + readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, convertToGrayScaleValue, colorWeightsValue, + messageHandler, shouldCancel); break; } case NumericType::int8: { - readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, messageHandler, shouldCancel); + readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, convertToGrayScaleValue, colorWeightsValue, + messageHandler, shouldCancel); break; } case NumericType::uint16: { - readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, messageHandler, shouldCancel); + readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, convertToGrayScaleValue, colorWeightsValue, + messageHandler, shouldCancel); break; } case NumericType::int16: { - readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, messageHandler, shouldCancel); + readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, convertToGrayScaleValue, colorWeightsValue, + messageHandler, shouldCancel); break; } case NumericType::uint32: { - readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, messageHandler, shouldCancel); + readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, convertToGrayScaleValue, colorWeightsValue, + messageHandler, shouldCancel); break; } case NumericType::int32: { - readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, messageHandler, shouldCancel); + readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, convertToGrayScaleValue, colorWeightsValue, + messageHandler, shouldCancel); break; } case NumericType::uint64: { - readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, messageHandler, shouldCancel); + readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, convertToGrayScaleValue, colorWeightsValue, + messageHandler, shouldCancel); break; } case NumericType::int64: { - readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, messageHandler, shouldCancel); + readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, convertToGrayScaleValue, colorWeightsValue, + messageHandler, shouldCancel); break; } case NumericType::float32: { - readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, messageHandler, shouldCancel); + readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, convertToGrayScaleValue, colorWeightsValue, + messageHandler, shouldCancel); break; } case NumericType::float64: { - readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, messageHandler, shouldCancel); + readResult = cxITKImportImageStack::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataPath, files, imageTransformValue, convertToGrayScaleValue, colorWeightsValue, + messageHandler, shouldCancel); break; } default: { diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImportImageStack.hpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImportImageStack.hpp index 0f0b56fc6b..7db20aa7c1 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImportImageStack.hpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImportImageStack.hpp @@ -31,7 +31,8 @@ class ITKIMAGEPROCESSING_EXPORT ITKImportImageStack : public IFilter static inline constexpr StringLiteral k_ImageDataArrayPath_Key = "image_data_array_path"; static inline constexpr StringLiteral k_CellDataName_Key = "cell_data_name"; static inline constexpr StringLiteral k_ImageTransformChoice_Key = "image_transform_choice"; - + static inline constexpr StringLiteral k_ConvertToGrayScale_Key = "convert_to_gray_scale"; + static inline constexpr StringLiteral k_ColorWeights_Key = "color_weights"; /** * @brief Reads SIMPL json and converts it simplnx Arguments. * @param json diff --git a/src/Plugins/ITKImageProcessing/test/ITKImportImageStackTest.cpp b/src/Plugins/ITKImageProcessing/test/ITKImportImageStackTest.cpp index 1b383e8c94..03e572971f 100644 --- a/src/Plugins/ITKImageProcessing/test/ITKImportImageStackTest.cpp +++ b/src/Plugins/ITKImageProcessing/test/ITKImportImageStackTest.cpp @@ -14,6 +14,7 @@ #include using namespace nx::core; +using namespace nx::core::UnitTest; namespace fs = std::filesystem; @@ -46,8 +47,8 @@ void ExecuteImportImageStackXY(DataStructure& dataStructure, const std::string& { // Filter needs RotateSampleRefFrameFilter to run Application::GetOrCreateInstance()->loadPlugins(unit_test::k_BuildDir.view(), true); - auto* filterList = nx::core::Application::Instance()->getFilterList(); - REQUIRE(filterList != nullptr); + auto* filterListPtr = nx::core::Application::Instance()->getFilterList(); + REQUIRE(filterListPtr != nullptr); // Define Shared parameters std::vector k_Origin = {0.0f, 0.0f, 0.0f}; @@ -69,7 +70,7 @@ void ExecuteImportImageStackXY(DataStructure& dataStructure, const std::string& // Run generated X flip { - auto importImageStackFilter = filterList->createFilter(::k_ImportImageStackFilterHandle); + auto importImageStackFilter = filterListPtr->createFilter(::k_ImportImageStackFilterHandle); REQUIRE(nullptr != importImageStackFilter); Arguments args; @@ -89,7 +90,7 @@ void ExecuteImportImageStackXY(DataStructure& dataStructure, const std::string& // Run generated Y flip { - auto importImageStackFilter = filterList->createFilter(::k_ImportImageStackFilterHandle); + auto importImageStackFilter = filterListPtr->createFilter(::k_ImportImageStackFilterHandle); REQUIRE(nullptr != importImageStackFilter); Arguments args; @@ -169,7 +170,7 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStack: NoInput", "[ITKImageProcessi Arguments args; auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions) } TEST_CASE("ITKImageProcessing::ITKImportImageStack: NoImageGeometry", "[ITKImageProcessing][ITKImportImageStack]") @@ -185,7 +186,7 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStack: NoImageGeometry", "[ITKImage args.insertOrAssign(ITKImportImageStack::k_InputFileListInfo_Key, std::make_any(fileListInfo)); auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions) } TEST_CASE("ITKImageProcessing::ITKImportImageStack: NoFiles", "[ITKImageProcessing][ITKImportImageStack]") @@ -209,7 +210,7 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStack: NoFiles", "[ITKImageProcessi args.insertOrAssign(ITKImportImageStack::k_ImageGeometryPath_Key, std::make_any(k_ImageGeomPath)); auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions) } TEST_CASE("ITKImageProcessing::ITKImportImageStack: FileDoesNotExist", "[ITKImageProcessing][ITKImportImageStack]") @@ -233,11 +234,14 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStack: FileDoesNotExist", "[ITKImag args.insertOrAssign(ITKImportImageStack::k_ImageGeometryPath_Key, std::make_any(k_ImageGeomPath)); auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions) } TEST_CASE("ITKImageProcessing::ITKImportImageStack: CompareImage", "[ITKImageProcessing][ITKImportImageStack]") { + auto app = Application::GetOrCreateInstance(); + app->loadPlugins(unit_test::k_BuildDir.view()); + ITKImportImageStack filter; DataStructure dataStructure; Arguments args; @@ -267,12 +271,12 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStack: CompareImage", "[ITKImagePro auto executeResult = filter.execute(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) - const auto* imageGeom = dataStructure.getDataAs(k_ImageGeomPath); - REQUIRE(imageGeom != nullptr); + const auto* imageGeomPtr = dataStructure.getDataAs(k_ImageGeomPath); + REQUIRE(imageGeomPtr != nullptr); - SizeVec3 imageDims = imageGeom->getDimensions(); - FloatVec3 imageOrigin = imageGeom->getOrigin(); - FloatVec3 imageSpacing = imageGeom->getSpacing(); + SizeVec3 imageDims = imageGeomPtr->getDimensions(); + FloatVec3 imageOrigin = imageGeomPtr->getOrigin(); + FloatVec3 imageSpacing = imageGeomPtr->getSpacing(); std::array dims = {524, 390, 3}; @@ -288,8 +292,8 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStack: CompareImage", "[ITKImagePro REQUIRE(imageSpacing[1] == Approx(spacing[1])); REQUIRE(imageSpacing[2] == Approx(spacing[2])); - const auto* imageData = dataStructure.getDataAs(k_ImageDataPath); - REQUIRE(imageData != nullptr); + const auto* imageDataPtr = dataStructure.getDataAs(k_ImageDataPath); + REQUIRE(imageDataPtr != nullptr); const std::string md5Hash = ITKTestBase::ComputeMd5Hash(dataStructure, k_ImageDataPath); REQUIRE(md5Hash == "2620b39f0dcaa866602c2591353116a4"); @@ -382,3 +386,129 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStack: Flipped Image Odd-Odd X/Y", // Compare against exemplars ::CompareXYFlippedGeometries(dataStructure); } + +TEST_CASE("ITKImageProcessing::ITKImportImageStack: RGB_To_Grayscale", "[ITKImageProcessing][ITKImportImageStack]") +{ + auto app = Application::GetOrCreateInstance(); + app->loadPlugins(unit_test::k_BuildDir.view()); + + ITKImportImageStack filter; + DataStructure dataStructure; + Arguments args; + + GeneratedFileListParameter::ValueType fileListInfo; + fileListInfo.inputPath = k_ImageStackDir; + fileListInfo.startIndex = 0; + fileListInfo.endIndex = 2; + fileListInfo.incrementIndex = 1; + fileListInfo.fileExtension = ".png"; + fileListInfo.filePrefix = "rgb_"; + fileListInfo.fileSuffix = ""; + fileListInfo.paddingDigits = 1; + fileListInfo.ordering = GeneratedFileListParameter::Ordering::LowToHigh; + + std::vector origin = {1.0f, 4.0f, 8.0f}; + std::vector spacing = {0.3f, 0.2f, 0.9f}; + + args.insertOrAssign(ITKImportImageStack::k_InputFileListInfo_Key, std::make_any(fileListInfo)); + args.insertOrAssign(ITKImportImageStack::k_Origin_Key, std::make_any>(origin)); + args.insertOrAssign(ITKImportImageStack::k_Spacing_Key, std::make_any>(spacing)); + args.insertOrAssign(ITKImportImageStack::k_ImageGeometryPath_Key, std::make_any(k_ImageGeomPath)); + args.insertOrAssign(ITKImportImageStack::k_ConvertToGrayScale_Key, std::make_any(true)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + const auto* imageGeomPtr = dataStructure.getDataAs(k_ImageGeomPath); + REQUIRE(imageGeomPtr != nullptr); + + SizeVec3 imageDims = imageGeomPtr->getDimensions(); + FloatVec3 imageOrigin = imageGeomPtr->getOrigin(); + FloatVec3 imageSpacing = imageGeomPtr->getSpacing(); + + std::array dims = {524, 390, 3}; + + REQUIRE(imageDims[0] == dims[0]); + REQUIRE(imageDims[1] == dims[1]); + REQUIRE(imageDims[2] == dims[2]); + + REQUIRE(imageOrigin[0] == Approx(origin[0])); + REQUIRE(imageOrigin[1] == Approx(origin[1])); + REQUIRE(imageOrigin[2] == Approx(origin[2])); + + REQUIRE(imageSpacing[0] == Approx(spacing[0])); + REQUIRE(imageSpacing[1] == Approx(spacing[1])); + REQUIRE(imageSpacing[2] == Approx(spacing[2])); + + const auto* imageDataPtr = dataStructure.getDataAs(k_ImageDataPath); + REQUIRE(imageDataPtr != nullptr); + + const std::string md5Hash = ITKTestBase::ComputeMd5Hash(dataStructure, k_ImageDataPath); + REQUIRE(md5Hash == "2620b39f0dcaa866602c2591353116a4"); +} + +TEST_CASE("ITKImageProcessing::ITKImportImageStack: RGB", "[ITKImageProcessing][ITKImportImageStack]") +{ + auto app = Application::GetOrCreateInstance(); + app->loadPlugins(unit_test::k_BuildDir.view()); + + ITKImportImageStack filter; + DataStructure dataStructure; + Arguments args; + + GeneratedFileListParameter::ValueType fileListInfo; + fileListInfo.inputPath = k_ImageStackDir; + fileListInfo.startIndex = 0; + fileListInfo.endIndex = 2; + fileListInfo.incrementIndex = 1; + fileListInfo.fileExtension = ".png"; + fileListInfo.filePrefix = "rgb_"; + fileListInfo.fileSuffix = ""; + fileListInfo.paddingDigits = 1; + fileListInfo.ordering = GeneratedFileListParameter::Ordering::LowToHigh; + + std::vector origin = {1.0f, 4.0f, 8.0f}; + std::vector spacing = {0.3f, 0.2f, 0.9f}; + + args.insertOrAssign(ITKImportImageStack::k_InputFileListInfo_Key, std::make_any(fileListInfo)); + args.insertOrAssign(ITKImportImageStack::k_Origin_Key, std::make_any>(origin)); + args.insertOrAssign(ITKImportImageStack::k_Spacing_Key, std::make_any>(spacing)); + args.insertOrAssign(ITKImportImageStack::k_ImageGeometryPath_Key, std::make_any(k_ImageGeomPath)); + args.insertOrAssign(ITKImportImageStack::k_ConvertToGrayScale_Key, std::make_any(false)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + const auto* imageGeomPtr = dataStructure.getDataAs(k_ImageGeomPath); + REQUIRE(imageGeomPtr != nullptr); + + SizeVec3 imageDims = imageGeomPtr->getDimensions(); + FloatVec3 imageOrigin = imageGeomPtr->getOrigin(); + FloatVec3 imageSpacing = imageGeomPtr->getSpacing(); + + std::array dims = {524, 390, 3}; + + REQUIRE(imageDims[0] == dims[0]); + REQUIRE(imageDims[1] == dims[1]); + REQUIRE(imageDims[2] == dims[2]); + + REQUIRE(imageOrigin[0] == Approx(origin[0])); + REQUIRE(imageOrigin[1] == Approx(origin[1])); + REQUIRE(imageOrigin[2] == Approx(origin[2])); + + REQUIRE(imageSpacing[0] == Approx(spacing[0])); + REQUIRE(imageSpacing[1] == Approx(spacing[1])); + REQUIRE(imageSpacing[2] == Approx(spacing[2])); + + const auto* imageDataPtr = dataStructure.getDataAs(k_ImageDataPath); + REQUIRE(imageDataPtr != nullptr); + + const std::string md5Hash = ITKTestBase::ComputeMd5Hash(dataStructure, k_ImageDataPath); + REQUIRE(md5Hash == "8b0b0393d6779156c88544bc4d75d3fc"); +}