diff --git a/data/dataset.xml b/data/dataset.xml new file mode 100644 index 000000000..f86feddc5 --- /dev/null +++ b/data/dataset.xml @@ -0,0 +1,506 @@ + + + . + + + dataset.n5/ + + + + 0 + 0 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 187 + 0 + + + + 1 + 1 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 188 + 0 + + + + 2 + 2 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 189 + 0 + + + + 3 + 3 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 200 + 0 + + + + 4 + 4 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 201 + 0 + + + + 5 + 5 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 202 + 0 + + + + 6 + 6 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 213 + 0 + + + + 7 + 7 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 214 + 0 + + + + 8 + 8 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 215 + 0 + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 187 + 187 + + + 188 + 188 + + + 189 + 189 + + + 200 + 200 + + + 201 + 201 + + + 202 + 202 + + + 213 + 213 + + + 214 + 214 + + + 215 + 215 + + + + + 0 + 0 + + + + + 0 + + + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 + + + Stitching Transform + 1.0 0.0 0.0 0.4554158176323426 0.0 1.0 0.0 -0.13328548693164066 0.0 0.0 1.0 1.039028825990859 + + + Translation to Regular Grid + 1.0 0.0 0.0 199.68 0.0 1.0 0.0 102.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 0.9996881730582423 1.0178764373025146E-4 9.91613051380682E-5 0.296854107149204 -0.0014206520238486176 1.000352820575225 5.493263412035365E-5 0.4978096475705985 -4.038421786319725E-4 1.2989941922307108E-4 1.0000050536353644 0.012322662821996552 + + + Stitching Transform + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 + + + Translation to Regular Grid + 1.0 0.0 0.0 -256.0 0.0 1.0 0.0 102.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 0.9999774841758134 -2.0323268523581142E-4 5.914840097558037E-5 0.41302203806414345 -6.595441757221898E-4 1.0005468325585005 1.0422412713430604E-4 0.86847428183128 -3.8743282755243446E-4 6.197381562939316E-4 0.9999038186675331 -0.1051593717912322 + + + Stitching Transform + 1.0 0.0 0.0 -0.2804882451294475 0.0 1.0 0.0 0.15177465825263425 0.0 0.0 1.0 -0.8673431810167168 + + + Translation to Regular Grid + 1.0 0.0 0.0 -711.6800000000001 0.0 1.0 0.0 102.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 0.9996509004376033 0.0011789057647756168 6.625470971101606E-5 -0.4380062409126003 -5.886838131605057E-6 0.9993980714516947 8.378533632140301E-4 0.21066285389381184 1.1914663433102846E-4 -0.0017577181435329034 0.9993905308006584 -0.10732512771106033 + + + Stitching Transform + 1.0 0.0 0.0 -0.8442948117588571 0.0 1.0 0.0 7.4417831077170336 0.0 0.0 1.0 -8.789096260073075 + + + Translation to Regular Grid + 1.0 0.0 0.0 199.68 0.0 1.0 0.0 -160.0 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 0.9993691627809789 6.213286683890014E-4 9.303551548369867E-5 -0.5698358880199796 -0.0012153705700172891 0.9994415380582407 8.680618674785683E-4 0.916909696194346 -1.5083252038943456E-4 -0.0011408852434367028 0.9994660587646744 0.2820529225572497 + + + Stitching Transform + 1.0 0.0 0.0 -0.9089191675130128 0.0 1.0 0.0 7.4256749717825095 0.0 0.0 1.0 -10.328335265676746 + + + Translation to Regular Grid + 1.0 0.0 0.0 -256.0 0.0 1.0 0.0 -160.0 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 0.9995958883424029 1.1960194298687558E-4 1.3045003320240472E-4 -0.5288425017288729 -4.5021629309568165E-4 0.9995801176422028 9.375713906809582E-4 1.7071573589623064 -2.5232116962087617E-4 -5.217759052761294E-4 0.9995631970154002 0.266678832277585 + + + Stitching Transform + 1.0 0.0 0.0 -1.1369886493304193 0.0 1.0 0.0 7.212246076053502 0.0 0.0 1.0 -11.06907324837572 + + + Translation to Regular Grid + 1.0 0.0 0.0 -711.6800000000001 0.0 1.0 0.0 -160.0 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 1.001169864797628 0.00527491302811622 1.4584877195363756E-4 -0.15773640915627343 -0.004686469222584484 0.9996923176367982 -0.002087984887367355 1.2420606082221906 -6.295077531490363E-4 0.005598468303174577 1.0011602899054004 1.9104004620529875 + + + Stitching Transform + 1.0 0.0 0.0 -0.2626855827553527 0.0 1.0 0.0 -6.7600081391679225 0.0 0.0 1.0 12.896136661870298 + + + Translation to Regular Grid + 1.0 0.0 0.0 199.68 0.0 1.0 0.0 -422.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 1.0009007956899272 0.0053216640997549155 1.371415077664928E-4 0.31426019006657296 -0.005800996293005373 0.9993339550765709 -0.0019903541975732236 0.5557333216408067 -4.823233633192067E-4 0.003500523337610362 1.0009341748352047 1.2744275908022205 + + + Stitching Transform + 1.0 0.0 0.0 -0.7700626936842326 0.0 1.0 0.0 -7.432744962459225 0.0 0.0 1.0 11.579421273826847 + + + Translation to Regular Grid + 1.0 0.0 0.0 -256.0 0.0 1.0 0.0 -422.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 1.0012387020759832 0.005337931651988534 5.916593012930141E-5 0.6833023552548335 -0.0048548568558627265 0.9989044018136208 -0.0019280609543394106 -0.26569722464665557 -3.915489271687177E-4 0.0016823628452441262 1.0009459560649534 0.679636329046033 + + + Stitching Transform + 1.0 0.0 0.0 -1.2414564955437868 0.0 1.0 0.0 -8.130736624699523 0.0 0.0 1.0 10.981609779561097 + + + Translation to Regular Grid + 1.0 0.0 0.0 -711.6800000000001 0.0 1.0 0.0 -422.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + tpId_0_viewSetupId_0/beads + tpId_0_viewSetupId_1/beads + tpId_0_viewSetupId_2/beads + tpId_0_viewSetupId_3/beads + tpId_0_viewSetupId_4/beads + tpId_0_viewSetupId_5/beads + tpId_0_viewSetupId_6/beads + tpId_0_viewSetupId_7/beads + tpId_0_viewSetupId_8/beads + + + + + + 1.0 0.0 0.0 0.32830997041742194 0.0 1.0 0.0 -0.2780143304040763 0.0 0.0 1.0 0.8627255545425214 + 0.9872797935430719 + -19840.08 + -256.0 102.0 -1236.0 -200.0 422.0 1236.0 + + + 1.0 0.0 0.0 0.31408745860667864 0.0 1.0 0.0 -0.21794513304740804 0.0 0.0 1.0 1.0669536191755924 + 0.9890595837012269 + -13460.56 + 199.0 102.0 -1236.0 255.0 422.0 1236.0 + + + 1.0 0.0 0.0 0.3219589445005795 0.0 1.0 0.0 -0.2912676156699092 0.0 0.0 1.0 0.7868782074069713 + 0.989534384651222 + -23513.68 + -256.0 -160.0 -1236.0 -200.0 159.0 1236.0 + + + 1.0 0.0 0.0 0.32849982720156845 0.0 1.0 0.0 -0.299139087662013 0.0 0.0 1.0 1.1721464806346376 + 0.9875872479386237 + -17134.16 + 199.0 -160.0 -1236.0 255.0 159.0 1236.0 + + + 1.0 0.0 0.0 0.2950324994815787 0.0 1.0 0.0 1.3873944732041537 0.0 0.0 1.0 0.8156317724537985 + 0.9350710728845969 + -27187.28 + -256.0 -423.0 -1236.0 -200.0 -103.0 1236.0 + + + 1.0 0.0 0.0 0.28614119325933984 0.0 1.0 0.0 1.3775777622511782 0.0 0.0 1.0 1.3157733733060013 + 0.9339835065140643 + -20807.760000000002 + 199.0 -423.0 -1236.0 255.0 -103.0 1236.0 + + + 1.0 0.0 0.0 -0.022274627811640357 0.0 1.0 0.0 7.064784613790181 0.0 0.0 1.0 -9.66095411700826 + 0.9267757024498638 + -23251.28 + -256.0 102.0 -1236.0 -200.0 159.0 1236.0 + + + 1.0 0.0 0.0 -0.09643953149634399 0.0 1.0 0.0 7.007672103883749 0.0 0.0 1.0 -9.036354555841172 + 0.9321167659568985 + -16871.76 + 199.0 102.0 -1236.0 255.0 159.0 1236.0 + + + 1.0 0.0 0.0 0.31868072554544824 0.0 1.0 0.0 -14.060647711779382 0.0 0.0 1.0 22.925437346686294 + 0.9354709581463394 + -26924.88 + -256.0 -160.0 -1236.0 -200.0 -103.0 1236.0 + + + 1.0 0.0 0.0 0.42744418243105997 0.0 1.0 0.0 -14.049728759960828 0.0 0.0 1.0 23.294215724982678 + 0.9410862556623087 + -20545.36 + 199.0 -160.0 -1236.0 255.0 -103.0 1236.0 + + + 1.0 0.0 0.0 -1.5104784240610343 0.0 1.0 0.0 7.395826789692137 0.0 0.0 1.0 -9.997150408536527 + 0.9162468747756625 + -29175.120000000003 + -712.0 102.0 -1236.0 -200.0 159.0 1236.0 + + + 1.0 0.0 0.0 -1.9429844985963882 0.0 1.0 0.0 7.478610381875683 0.0 0.0 1.0 -9.992593031616707 + 0.9304678530064963 + -22795.6 + -256.0 102.0 -1236.0 255.0 159.0 1236.0 + + + 1.0 0.0 0.0 -2.0782351526913487 0.0 1.0 0.0 7.41086456502083 0.0 0.0 1.0 -9.69656279859555 + 0.9357936880330161 + -16416.08 + 199.0 102.0 -1236.0 711.0 159.0 1236.0 + + + 1.0 0.0 0.0 -0.3287299635215959 0.0 1.0 0.0 -14.747678576491978 0.0 0.0 1.0 21.81115327961561 + 0.9138108463084885 + -32848.72 + -712.0 -160.0 -1236.0 -200.0 -103.0 1236.0 + + + 1.0 0.0 0.0 0.34904618022437717 0.0 1.0 0.0 -15.354558165871083 0.0 0.0 1.0 21.82603817266454 + 0.8990291118916934 + -26469.2 + -256.0 -160.0 -1236.0 255.0 -103.0 1236.0 + + + 1.0 0.0 0.0 1.0216345489996854 0.0 1.0 0.0 -15.042586536834278 0.0 0.0 1.0 21.61643113924515 + 0.8887321660239942 + -20089.68 + 199.0 -160.0 -1236.0 711.0 -103.0 1236.0 + + + 1.0 0.0 0.0 -0.6616285141958542 0.0 1.0 0.0 7.55184164375774 0.0 0.0 1.0 -11.190099606327976 + 0.9526419402381631 + -28719.440000000002 + -256.0 102.0 -1236.0 -200.0 159.0 1236.0 + + + 1.0 0.0 0.0 -0.7271388208706355 0.0 1.0 0.0 7.638504842226183 0.0 0.0 1.0 -11.471001585951399 + 0.9439136410878279 + -22339.92 + 199.0 102.0 -1236.0 255.0 159.0 1236.0 + + + 1.0 0.0 0.0 -0.284636513100736 0.0 1.0 0.0 -15.462312909779143 0.0 0.0 1.0 21.767295071746958 + 0.9233340706532531 + -32393.04 + -256.0 -160.0 -1236.0 -200.0 -103.0 1236.0 + + + 1.0 0.0 0.0 -0.13258697351179194 0.0 1.0 0.0 -14.947295037284562 0.0 0.0 1.0 19.954530783329574 + 0.9243536847543689 + -26013.52 + 199.0 -160.0 -1236.0 255.0 -103.0 1236.0 + + + + diff --git a/data/dataset_corrected.xml b/data/dataset_corrected.xml new file mode 100644 index 000000000..265e2fea3 --- /dev/null +++ b/data/dataset_corrected.xml @@ -0,0 +1,417 @@ + + + . + + + + + dataset.n5/ + + + + + dark_and_flatfields/setup187-flatfield (fixed by mirroring).tif + dark_and_flatfields/setup187-AVG_darkfield-fromdata.tif + + + dark_and_flatfields/setup188-flatfield (fixed by mirroring).tif + dark_and_flatfields/setup188-AVG_darkfield-fromdata.tif + + + dark_and_flatfields/setup189-flatfield.tif + dark_and_flatfields/setup189-AVG_darkfield-fromdata.tif + + + dark_and_flatfields/setup200-flatfield (fixed by mirroring).tif + dark_and_flatfields/setup200-AVG_darkfield-fromdata.tif + + + dark_and_flatfields/setup201-flatfield (fixed by mirroring).tif + dark_and_flatfields/setup201-AVG_darkfield-fromdata.tif + + + dark_and_flatfields/setup202-flatfield.tif + dark_and_flatfields/setup202-AVG_darkfield-fromdata.tif + + + dark_and_flatfields/setup213-flatfield (fixed by mirroring).tif + dark_and_flatfields/setup213-AVG_darkfield-fromdata.tif + + + dark_and_flatfields/setup214-flatfield.tif + dark_and_flatfields/setup214-AVG_darkfield-fromdata.tif + + + dark_and_flatfields/setup215-flatfield.tif + dark_and_flatfields/setup215-AVG_darkfield-fromdata.tif + + + + + + 0 + 0 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 187 + 0 + + + + 1 + 1 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 188 + 0 + + + + 2 + 2 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 189 + 0 + + + + 3 + 3 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 200 + 0 + + + + 4 + 4 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 201 + 0 + + + + 5 + 5 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 202 + 0 + + + + 6 + 6 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 213 + 0 + + + + 7 + 7 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 214 + 0 + + + + 8 + 8 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 215 + 0 + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 187 + 187 + + + 188 + 188 + + + 189 + 189 + + + 200 + 200 + + + 201 + 201 + + + 202 + 202 + + + 213 + 213 + + + 214 + 214 + + + 215 + 215 + + + + + 0 + 0 + + + + + 0 + + + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 + + + Stitching Transform + 1.0 0.0 0.0 0.4554158176323426 0.0 1.0 0.0 -0.13328548693164066 0.0 0.0 1.0 1.039028825990859 + + + Translation to Regular Grid + 1.0 0.0 0.0 199.68 0.0 1.0 0.0 102.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 0.9996881730582423 1.0178764373025146E-4 9.91613051380682E-5 0.296854107149204 -0.0014206520238486176 1.000352820575225 5.493263412035365E-5 0.4978096475705985 -4.038421786319725E-4 1.2989941922307108E-4 1.0000050536353644 0.012322662821996552 + + + Stitching Transform + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 + + + Translation to Regular Grid + 1.0 0.0 0.0 -256.0 0.0 1.0 0.0 102.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 0.9999774841758134 -2.0323268523581142E-4 5.914840097558037E-5 0.41302203806414345 -6.595441757221898E-4 1.0005468325585005 1.0422412713430604E-4 0.86847428183128 -3.8743282755243446E-4 6.197381562939316E-4 0.9999038186675331 -0.1051593717912322 + + + Stitching Transform + 1.0 0.0 0.0 -0.2804882451294475 0.0 1.0 0.0 0.15177465825263425 0.0 0.0 1.0 -0.8673431810167168 + + + Translation to Regular Grid + 1.0 0.0 0.0 -711.6800000000001 0.0 1.0 0.0 102.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 0.9996509004376033 0.0011789057647756168 6.625470971101606E-5 -0.4380062409126003 -5.886838131605057E-6 0.9993980714516947 8.378533632140301E-4 0.21066285389381184 1.1914663433102846E-4 -0.0017577181435329034 0.9993905308006584 -0.10732512771106033 + + + Stitching Transform + 1.0 0.0 0.0 -0.8442948117588571 0.0 1.0 0.0 7.4417831077170336 0.0 0.0 1.0 -8.789096260073075 + + + Translation to Regular Grid + 1.0 0.0 0.0 199.68 0.0 1.0 0.0 -160.0 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 0.9993691627809789 6.213286683890014E-4 9.303551548369867E-5 -0.5698358880199796 -0.0012153705700172891 0.9994415380582407 8.680618674785683E-4 0.916909696194346 -1.5083252038943456E-4 -0.0011408852434367028 0.9994660587646744 0.2820529225572497 + + + Stitching Transform + 1.0 0.0 0.0 -0.9089191675130128 0.0 1.0 0.0 7.4256749717825095 0.0 0.0 1.0 -10.328335265676746 + + + Translation to Regular Grid + 1.0 0.0 0.0 -256.0 0.0 1.0 0.0 -160.0 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 0.9995958883424029 1.1960194298687558E-4 1.3045003320240472E-4 -0.5288425017288729 -4.5021629309568165E-4 0.9995801176422028 9.375713906809582E-4 1.7071573589623064 -2.5232116962087617E-4 -5.217759052761294E-4 0.9995631970154002 0.266678832277585 + + + Stitching Transform + 1.0 0.0 0.0 -1.1369886493304193 0.0 1.0 0.0 7.212246076053502 0.0 0.0 1.0 -11.06907324837572 + + + Translation to Regular Grid + 1.0 0.0 0.0 -711.6800000000001 0.0 1.0 0.0 -160.0 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 1.001169864797628 0.00527491302811622 1.4584877195363756E-4 -0.15773640915627343 -0.004686469222584484 0.9996923176367982 -0.002087984887367355 1.2420606082221906 -6.295077531490363E-4 0.005598468303174577 1.0011602899054004 1.9104004620529875 + + + Stitching Transform + 1.0 0.0 0.0 -0.2626855827553527 0.0 1.0 0.0 -6.7600081391679225 0.0 0.0 1.0 12.896136661870298 + + + Translation to Regular Grid + 1.0 0.0 0.0 199.68 0.0 1.0 0.0 -422.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 1.0009007956899272 0.0053216640997549155 1.371415077664928E-4 0.31426019006657296 -0.005800996293005373 0.9993339550765709 -0.0019903541975732236 0.5557333216408067 -4.823233633192067E-4 0.003500523337610362 1.0009341748352047 1.2744275908022205 + + + Stitching Transform + 1.0 0.0 0.0 -0.7700626936842326 0.0 1.0 0.0 -7.432744962459225 0.0 0.0 1.0 11.579421273826847 + + + Translation to Regular Grid + 1.0 0.0 0.0 -256.0 0.0 1.0 0.0 -422.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 1.0012387020759832 0.005337931651988534 5.916593012930141E-5 0.6833023552548335 -0.0048548568558627265 0.9989044018136208 -0.0019280609543394106 -0.26569722464665557 -3.915489271687177E-4 0.0016823628452441262 1.0009459560649534 0.679636329046033 + + + Stitching Transform + 1.0 0.0 0.0 -1.2414564955437868 0.0 1.0 0.0 -8.130736624699523 0.0 0.0 1.0 10.981609779561097 + + + Translation to Regular Grid + 1.0 0.0 0.0 -711.6800000000001 0.0 1.0 0.0 -422.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + + + + + diff --git a/data/dataset_corrected_viewer.xml b/data/dataset_corrected_viewer.xml new file mode 100644 index 000000000..7593a7917 --- /dev/null +++ b/data/dataset_corrected_viewer.xml @@ -0,0 +1,417 @@ + + + . + + + + + dataset.n5/ + + + + + dark_and_flatfields/setup187-flatfield (fixed by mirroring).tif + dark_and_flatfields/setup187-AVG_darkfield-fromdata.tif + + + dark_and_flatfields/setup188-flatfield (fixed by mirroring).tif + dark_and_flatfields/setup188-AVG_darkfield-fromdata.tif + + + dark_and_flatfields/setup189-flatfield.tif + dark_and_flatfields/setup189-AVG_darkfield-fromdata.tif + + + dark_and_flatfields/setup200-flatfield (fixed by mirroring).tif + dark_and_flatfields/setup200-AVG_darkfield-fromdata.tif + + + dark_and_flatfields/setup201-flatfield (fixed by mirroring).tif + dark_and_flatfields/setup201-AVG_darkfield-fromdata.tif + + + dark_and_flatfields/setup202-flatfield.tif + dark_and_flatfields/setup202-AVG_darkfield-fromdata.tif + + + dark_and_flatfields/setup213-flatfield (fixed by mirroring).tif + dark_and_flatfields/setup213-AVG_darkfield-fromdata.tif + + + dark_and_flatfields/setup214-flatfield.tif + dark_and_flatfields/setup214-AVG_darkfield-fromdata.tif + + + dark_and_flatfields/setup215-flatfield.tif + dark_and_flatfields/setup215-AVG_darkfield-fromdata.tif + + + + + + 0 + 0 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 187 + 0 + + + + 1 + 1 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 188 + 0 + + + + 2 + 2 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 189 + 0 + + + + 3 + 3 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 200 + 0 + + + + 4 + 4 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 201 + 0 + + + + 5 + 5 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 202 + 0 + + + + 6 + 6 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 213 + 0 + + + + 7 + 7 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 214 + 0 + + + + 8 + 8 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 215 + 0 + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 187 + 187 + + + 188 + 188 + + + 189 + 189 + + + 200 + 200 + + + 201 + 201 + + + 202 + 202 + + + 213 + 213 + + + 214 + 214 + + + 215 + 215 + + + + + 0 + 0 + + + + + 0 + + + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 + + + Stitching Transform + 1.0 0.0 0.0 0.4554158176323426 0.0 1.0 0.0 -0.13328548693164066 0.0 0.0 1.0 1.039028825990859 + + + Translation to Regular Grid + 1.0 0.0 0.0 199.68 0.0 1.0 0.0 102.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 0.9996881730582423 1.0178764373025146E-4 9.91613051380682E-5 0.296854107149204 -0.0014206520238486176 1.000352820575225 5.493263412035365E-5 0.4978096475705985 -4.038421786319725E-4 1.2989941922307108E-4 1.0000050536353644 0.012322662821996552 + + + Stitching Transform + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 + + + Translation to Regular Grid + 1.0 0.0 0.0 -256.0 0.0 1.0 0.0 102.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 0.9999774841758134 -2.0323268523581142E-4 5.914840097558037E-5 0.41302203806414345 -6.595441757221898E-4 1.0005468325585005 1.0422412713430604E-4 0.86847428183128 -3.8743282755243446E-4 6.197381562939316E-4 0.9999038186675331 -0.1051593717912322 + + + Stitching Transform + 1.0 0.0 0.0 -0.2804882451294475 0.0 1.0 0.0 0.15177465825263425 0.0 0.0 1.0 -0.8673431810167168 + + + Translation to Regular Grid + 1.0 0.0 0.0 -711.6800000000001 0.0 1.0 0.0 102.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 0.9996509004376033 0.0011789057647756168 6.625470971101606E-5 -0.4380062409126003 -5.886838131605057E-6 0.9993980714516947 8.378533632140301E-4 0.21066285389381184 1.1914663433102846E-4 -0.0017577181435329034 0.9993905308006584 -0.10732512771106033 + + + Stitching Transform + 1.0 0.0 0.0 -0.8442948117588571 0.0 1.0 0.0 7.4417831077170336 0.0 0.0 1.0 -8.789096260073075 + + + Translation to Regular Grid + 1.0 0.0 0.0 199.68 0.0 1.0 0.0 -160.0 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 0.9993691627809789 6.213286683890014E-4 9.303551548369867E-5 -0.5698358880199796 -0.0012153705700172891 0.9994415380582407 8.680618674785683E-4 0.916909696194346 -1.5083252038943456E-4 -0.0011408852434367028 0.9994660587646744 0.2820529225572497 + + + Stitching Transform + 1.0 0.0 0.0 -0.9089191675130128 0.0 1.0 0.0 7.4256749717825095 0.0 0.0 1.0 -10.328335265676746 + + + Translation to Regular Grid + 1.0 0.0 0.0 -256.0 0.0 1.0 0.0 -160.0 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 0.9995958883424029 1.1960194298687558E-4 1.3045003320240472E-4 -0.5288425017288729 -4.5021629309568165E-4 0.9995801176422028 9.375713906809582E-4 1.7071573589623064 -2.5232116962087617E-4 -5.217759052761294E-4 0.9995631970154002 0.266678832277585 + + + Stitching Transform + 1.0 0.0 0.0 -1.1369886493304193 0.0 1.0 0.0 7.212246076053502 0.0 0.0 1.0 -11.06907324837572 + + + Translation to Regular Grid + 1.0 0.0 0.0 -711.6800000000001 0.0 1.0 0.0 -160.0 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 1.001169864797628 0.00527491302811622 1.4584877195363756E-4 -0.15773640915627343 -0.004686469222584484 0.9996923176367982 -0.002087984887367355 1.2420606082221906 -6.295077531490363E-4 0.005598468303174577 1.0011602899054004 1.9104004620529875 + + + Stitching Transform + 1.0 0.0 0.0 -0.2626855827553527 0.0 1.0 0.0 -6.7600081391679225 0.0 0.0 1.0 12.896136661870298 + + + Translation to Regular Grid + 1.0 0.0 0.0 199.68 0.0 1.0 0.0 -422.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 1.0009007956899272 0.0053216640997549155 1.371415077664928E-4 0.31426019006657296 -0.005800996293005373 0.9993339550765709 -0.0019903541975732236 0.5557333216408067 -4.823233633192067E-4 0.003500523337610362 1.0009341748352047 1.2744275908022205 + + + Stitching Transform + 1.0 0.0 0.0 -0.7700626936842326 0.0 1.0 0.0 -7.432744962459225 0.0 0.0 1.0 11.579421273826847 + + + Translation to Regular Grid + 1.0 0.0 0.0 -256.0 0.0 1.0 0.0 -422.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 1.0012387020759832 0.005337931651988534 5.916593012930141E-5 0.6833023552548335 -0.0048548568558627265 0.9989044018136208 -0.0019280609543394106 -0.26569722464665557 -3.915489271687177E-4 0.0016823628452441262 1.0009459560649534 0.679636329046033 + + + Stitching Transform + 1.0 0.0 0.0 -1.2414564955437868 0.0 1.0 0.0 -8.130736624699523 0.0 0.0 1.0 10.981609779561097 + + + Translation to Regular Grid + 1.0 0.0 0.0 -711.6800000000001 0.0 1.0 0.0 -422.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + + + + + diff --git a/data/dataset_corrected_viewer_s3.xml b/data/dataset_corrected_viewer_s3.xml new file mode 100644 index 000000000..a0cea32a1 --- /dev/null +++ b/data/dataset_corrected_viewer_s3.xml @@ -0,0 +1,121 @@ + + + . + + + + + https://s3.janelia.org/keller-lab/s12a/samples_for_stitching/fly_brain_3/basic_corrected/dataset.n5 + + + + + https://s3.janelia.org/keller-lab/s12a/samples_for_stitching/fly_brain_3/kittisopikulm/camera1/dark_field/s0/ + https://s3.janelia.org/keller-lab/s12a/samples_for_stitching/fly_brain_3/kittisopikulm/camera1/flat_field/s0/ + + + + + + 0 + 0 + 512 320 825 + + pixels + 1.0 1.0 3.0 + + + 0 + 0 + 187 + 0 + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 187 + 187 + + + 188 + 188 + + + 189 + 189 + + + 200 + 200 + + + 201 + 201 + + + 202 + 202 + + + 213 + 213 + + + 214 + 214 + + + 215 + 215 + + + + + 0 + 0 + + + + + 0 + + + + + + + AffineModel3D regularized with an RigidModel3D, lambda = 0.1 + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 + + + Stitching Transform + 1.0 0.0 0.0 0.4554158176323426 0.0 1.0 0.0 -0.13328548693164066 0.0 0.0 1.0 1.039028825990859 + + + Translation to Regular Grid + 1.0 0.0 0.0 199.68 0.0 1.0 0.0 102.40000000000003 0.0 0.0 1.0 -1236.0 + + + calibration + 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3.0 0.0 + + + + + + + + + diff --git a/pom.xml b/pom.xml index 3fe7e5bec..f88fa300d 100644 --- a/pom.xml +++ b/pom.xml @@ -113,12 +113,13 @@ like Selective Plane Illumination Microscopy (SPIM) Data. - 4.0.0-alpha-6 - 7.1.0-alpha-6 + 4.0.0-alpha-7 + 7.1.0-alpha-7 2.4.0-alpha-6 2.0.0-alpha-4 2.0.0-alpha-4 2.0.0-alpha-4 + 4.4.0-alpha-5 diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/explorer/popup/FlatFieldCorrectionPopup.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/explorer/popup/FlatFieldCorrectionPopup.java index 6fd4d69b8..1d821ad7a 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/explorer/popup/FlatFieldCorrectionPopup.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/explorer/popup/FlatFieldCorrectionPopup.java @@ -45,6 +45,7 @@ import net.preibisch.mvrecon.fiji.spimdata.explorer.ExplorerWindow; import net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield.DefaultFlatfieldCorrectionWrappedImgLoader; import net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield.FlatfieldCorrectionWrappedImgLoader; +import net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield.FlatfieldImageInfo; import net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield.LazyLoadingFlatFieldCorrectionMap; import net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield.MultiResolutionFlatfieldCorrectionWrappedImgLoader; @@ -101,10 +102,10 @@ public void actionPerformed(ActionEvent e) .isCached() : true ); - Map< ViewId, Pair< File, File > > fileMap = null; + Map> infoMap = null; if ( alreadyFF ) - fileMap = ( (LazyLoadingFlatFieldCorrectionMap< ImgLoader >) data.getSequenceDescription() - .getImgLoader() ).getFileMap(); + infoMap = ((LazyLoadingFlatFieldCorrectionMap) data.getSequenceDescription() + .getImgLoader()).getInfoMap(); for ( Channel c : channels ) for ( Illumination ill : illums ) @@ -120,13 +121,12 @@ public void actionPerformed(ActionEvent e) } ).findAny().orElseGet( null ); if ( anyViewId != null ) - if ( fileMap.containsKey( anyViewId ) ) - { - Pair< File, File > files = fileMap.get( anyViewId ); - if ( files.getA() != null ) - bright = files.getA().getAbsolutePath(); - if ( files.getB() != null ) - dark = files.getB().getAbsolutePath(); + if (infoMap.containsKey(anyViewId)) { + Pair infos = infoMap.get(anyViewId); + if (infos.getA() != null) + bright = new File(infos.getA().getUri()).getAbsolutePath(); + if (infos.getB() != null) + dark = new File(infos.getB().getUri()).getAbsolutePath(); } } gdp.addMessage( "Channel: " + c.getName() + ", Illumination: " + ill.getName() + ":" ); @@ -160,12 +160,16 @@ else if ( data.getSequenceDescription().getImgLoader() instanceof MultiResolutio File lightFile = !lightPath.equals( "" ) ? new File( lightPath ) : null; File darkFile = !darkPath.equals( "" ) ? new File( darkPath ) : null; + final File finalLightFile = lightFile; + final File finalDarkFile = darkFile; data.getSequenceDescription().getViewDescriptions().entrySet().forEach( el -> { if ( el.getValue().getViewSetup().getChannel() == c && el.getValue().getViewSetup().getIllumination() == ill ) { - ffIL.setBrightImage( el.getKey(), lightFile ); - ffIL.setDarkImage( el.getKey(), darkFile ); + if (finalLightFile != null) + ffIL.setBrightImage( el.getKey(), new FlatfieldImageInfo(finalLightFile.toURI(), null) ); + if (finalDarkFile != null) + ffIL.setDarkImage( el.getKey(), new FlatfieldImageInfo(finalDarkFile.toURI(), null) ); } } ); } diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/ConvertFlatfieldsToZarr.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/ConvertFlatfieldsToZarr.java new file mode 100644 index 000000000..66fa3377b --- /dev/null +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/ConvertFlatfieldsToZarr.java @@ -0,0 +1,258 @@ +/*- + * #%L + * Software for the reconstruction of multi-view microscopic acquisitions + * like Selective Plane Illumination Microscopy (SPIM) Data. + * %% + * Copyright (C) 2012 - 2025 Multiview Reconstruction developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.janelia.saalfeldlab.n5.Compression; +import org.janelia.saalfeldlab.n5.DataType; +import org.janelia.saalfeldlab.n5.N5Writer; +import org.janelia.saalfeldlab.n5.imglib2.N5Utils; +import org.janelia.saalfeldlab.n5.universe.StorageFormat; + +import ij.IJ; +import ij.ImagePlus; +import net.imglib2.img.Img; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.type.numeric.real.FloatType; +import org.janelia.scicomp.n5.zstandard.ZstandardCompression; +import util.URITools; + +/** + * Utility to convert TIFF-based flatfield images to Zarr v3 format. + * + * This creates single-shard Zarr containers for each flatfield image, + * suitable for cloud storage or local chunked access. + * + * Also generates a test XML file with the Zarr paths configured. + */ +public class ConvertFlatfieldsToZarr { + + /** + * Convert a single TIFF image to Zarr v3 format. + * + * @param inputTiff path to input TIFF file + * @param outputZarr path for output .zarr container + * @throws IOException if writing fails + */ + public static void convertTiffToZarr(final File inputTiff, final File outputZarr) throws IOException { + System.out.println("Converting: " + inputTiff.getName() + " -> " + outputZarr.getName()); + + // Load TIFF via ImageJ + final ImagePlus imp = IJ.openImage(inputTiff.getAbsolutePath()); + if (imp == null) + throw new IOException("Failed to load TIFF: " + inputTiff); + + final Img img = ImageJFunctions.convertFloat(imp); + + // Create Zarr v3 writer + final N5Writer writer = URITools.instantiateN5Writer(StorageFormat.ZARR, outputZarr.toURI()); + + // Use a single block/shard for the entire image (flatfields are typically small) + final int[] blockSize = new int[img.numDimensions()]; + for (int d = 0; d < img.numDimensions(); d++) + blockSize[d] = (int) img.dimension(d); + + // Save at root (empty string for Zarr v3) + // Save a block manually to work around a N5Utils.save issue for now + final Compression compression = new ZstandardCompression(); + writer.createDataset("/", img.dimensionsAsLongArray(), blockSize, DataType.FLOAT32, compression); + N5Utils.saveBlock(img, writer, "", new long[]{0, 0}); + + writer.close(); + System.out.println(" Created: " + outputZarr.getAbsolutePath()); + } + + /** + * Convert all flatfield TIFFs in a directory to Zarr format. + * + * @param inputDir directory containing TIFF files + * @param outputDir directory for output .zarr containers + * @return map of original filename (without extension) to output Zarr file + * @throws IOException if conversion fails + */ + public static Map convertDirectory(final File inputDir, final File outputDir) throws IOException { + if (!outputDir.exists()) + outputDir.mkdirs(); + + final Map converted = new HashMap<>(); + + final File[] tiffFiles = inputDir.listFiles((dir, name) -> + name.toLowerCase().endsWith(".tif") || name.toLowerCase().endsWith(".tiff")); + + if (tiffFiles == null || tiffFiles.length == 0) { + System.out.println("No TIFF files found in: " + inputDir); + return converted; + } + + for (final File tiff : tiffFiles) { + final String baseName = tiff.getName().replaceAll("\\.(tif|tiff)$", ""); + final File zarrOut = new File(outputDir, baseName + ".zarr"); + + convertTiffToZarr(tiff, zarrOut); + converted.put(baseName, zarrOut); + } + + return converted; + } + + /** + * Generate a test XML file with Zarr-based flatfield correction by copying + * an existing working XML and updating the flatfield paths to point to Zarr files. + * + * @param sourceXmlPath path to a working flatfield-corrected XML (with TIFF paths) + * @param outputXmlPath path for the new XML with Zarr flatfield paths + * @param zarrDir directory containing .zarr flatfield files + * @param convertedFiles map of base names to Zarr files from conversion + * @throws IOException if reading/writing fails + */ + public static void generateTestXml( + final String sourceXmlPath, + final String outputXmlPath, + final File zarrDir, + final Map convertedFiles) throws IOException { + + System.out.println("\n=== Generating Test XML ==="); + System.out.println("Source XML: " + sourceXmlPath); + System.out.println("Output XML: " + outputXmlPath); + + // Read the source XML + final StringBuilder content = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new FileReader(sourceXmlPath))) { + String line; + while ((line = reader.readLine()) != null) { + content.append(line).append("\n"); + } + } + + String xml = content.toString(); + + // Pattern to match BrightImg and DarkImg paths + final Pattern brightPattern = Pattern.compile("()([^<]+)()"); + final Pattern darkPattern = Pattern.compile("()([^<]+)()"); + + // Replace TIFF paths with Zarr paths + xml = replaceFlatfieldPaths(xml, brightPattern, zarrDir, convertedFiles); + xml = replaceFlatfieldPaths(xml, darkPattern, zarrDir, convertedFiles); + + // Write output XML + try (FileWriter writer = new FileWriter(outputXmlPath)) { + writer.write(xml); + } + + System.out.println("Created: " + outputXmlPath); + } + + /** + * Replace flatfield TIFF paths with Zarr paths in XML content. + */ + private static String replaceFlatfieldPaths( + String xml, + final Pattern pattern, + final File zarrDir, + final Map convertedFiles) { + + final Matcher matcher = pattern.matcher(xml); + final StringBuffer result = new StringBuffer(); + + while (matcher.find()) { + final String openTag = matcher.group(1); + final String oldPath = matcher.group(2); + final String closeTag = matcher.group(3); + + // Extract base name from old path (remove directory and extension) + String baseName = new File(oldPath).getName(); + baseName = baseName.replaceAll("\\.(tif|tiff)$", ""); + + // Find corresponding Zarr file + String newPath = oldPath; // default: keep original if not found + if (convertedFiles.containsKey(baseName)) { + // Use relative path from zarrDir + newPath = zarrDir.getName() + "/" + convertedFiles.get(baseName).getName(); + System.out.println(" " + oldPath + " -> " + newPath); + } else { + System.out.println(" WARNING: No Zarr found for: " + baseName); + } + + matcher.appendReplacement(result, Matcher.quoteReplacement(openTag + newPath + closeTag)); + } + matcher.appendTail(result); + + return result.toString(); + } + + /** + * Example main method demonstrating conversion and XML generation. + */ + public static void main(String[] args) throws Exception { + // Configuration - adjust these paths for your setup + final String basePath = "/Users/innerbergerm/Projects/janelia/multiview-reconstruction/"; + final String dataPath = basePath + "data/"; + + // Input: directory with TIFF flatfields + final File tiffDir = new File(basePath, "dark_and_flatfields"); + + // Output: directory for Zarr flatfields (in data folder, next to other data) + final File zarrDir = new File(basePath, "dark_and_flatfields_zarr"); + + // Step 1: Convert all TIFFs to Zarr + System.out.println("=== Step 1: Converting TIFFs to Zarr v3 ===\n"); + + Map convertedFiles; + if (tiffDir.exists()) { + convertedFiles = convertDirectory(tiffDir, zarrDir); + } else { + throw new IOException("Input TIFF directory does not exist: " + tiffDir.getAbsolutePath()); + } + + // Step 2: Generate test XML by copying from working TIFF-based XML + System.out.println("\n=== Step 2: Generating Test XML ===\n"); + + // Use the working flatfield-corrected XML as source + final String sourceXml = dataPath + "dataset_corrected_viewer.xml"; + final String outputXml = dataPath + "dataset_corrected_zarr.xml"; + + if (new File(sourceXml).exists()) { + generateTestXml( + sourceXml, + outputXml, + zarrDir, + convertedFiles + ); + } else { + System.out.println("Source XML not found: " + sourceXml); + System.out.println("Skipping XML generation."); + } + + System.out.println("\n=== Done! ==="); + System.out.println("To test, load: " + outputXml); + } +} diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/DefaultFlatfieldCorrectionWrappedImgLoader.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/DefaultFlatfieldCorrectionWrappedImgLoader.java index a2e482f97..15c5aa8ce 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/DefaultFlatfieldCorrectionWrappedImgLoader.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/DefaultFlatfieldCorrectionWrappedImgLoader.java @@ -33,7 +33,6 @@ import mpicbg.spim.data.sequence.ViewId; import mpicbg.spim.data.sequence.VoxelDimensions; import net.imglib2.Dimensions; -import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; import net.imglib2.converter.RealTypeConverters; import net.imglib2.img.Img; @@ -47,14 +46,13 @@ import net.imglib2.view.Views; import net.preibisch.mvrecon.fiji.plugin.queryXML.LoadParseQueryXML; import net.preibisch.mvrecon.fiji.spimdata.SpimData2; -import net.preibisch.mvrecon.fiji.spimdata.imgloaders.filemap2.FileMapImgLoaderLOCI2; import net.preibisch.mvrecon.process.fusion.FusionTools; public class DefaultFlatfieldCorrectionWrappedImgLoader extends LazyLoadingFlatFieldCorrectionMap< ImgLoader > implements ImgLoader { - private ImgLoader wrappedImgLoader; + private final ImgLoader wrappedImgLoader; private boolean active; private boolean cacheResult; @@ -106,23 +104,27 @@ class DefaultFlatfieldCorrectionWrappedSetupImgLoader & } @Override - public RandomAccessibleInterval< T > getImage(int timepointId, ImgLoaderHint... hints) - { - if (!active) - return (RandomAccessibleInterval< T >) wrappedImgLoader.getSetupImgLoader( setupId ).getImage( timepointId, - hints ); + public RandomAccessibleInterval< T > getImage(int timepointId, ImgLoaderHint... hints) { + if (!active) { + @SuppressWarnings("unchecked") final RandomAccessibleInterval img = + (RandomAccessibleInterval) wrappedImgLoader.getSetupImgLoader(setupId).getImage(timepointId, hints); + return img; + } @SuppressWarnings("unchecked") - RandomAccessibleInterval< T > rai = FlatFieldCorrectedRandomAccessibleIntervals.create( - (RandomAccessibleInterval< T >) wrappedImgLoader.getSetupImgLoader( setupId ).getImage( timepointId, - hints ), - getBrightImg( new ViewId( timepointId, setupId ) ), - getDarkImg( new ViewId( timepointId, setupId ) ) ); + RandomAccessibleInterval rai = FlatFieldCorrectedRandomAccessibleIntervals.create( + (RandomAccessibleInterval) wrappedImgLoader.getSetupImgLoader(setupId).getImage(timepointId, + hints), + getBrightImg(new ViewId(timepointId, setupId)), + getDarkImg(new ViewId(timepointId, setupId))); boolean loadCompletelyRequested = false; - for (ImgLoaderHint hint : hints) - if (hint == ImgLoaderHints.LOAD_COMPLETELY) + for (ImgLoaderHint hint : hints) { + if (hint == ImgLoaderHints.LOAD_COMPLETELY) { loadCompletelyRequested = true; + break; + } + } if (loadCompletelyRequested) { @@ -131,12 +133,13 @@ public RandomAccessibleInterval< T > getImage(int timepointId, ImgLoaderHint... numPx *= rai.dimension( d ); final ImgFactory< T > imgFactory; - if (Math.log(numPx) / Math.log( 2 ) < 31) - imgFactory = new ArrayImgFactory(); - else - imgFactory = new CellImgFactory(); + if (Math.log(numPx) / Math.log(2) < 31) { + imgFactory = new ArrayImgFactory<>(getImageType()); + } else { + imgFactory = new CellImgFactory<>(getImageType()); + } - Img< T > loadedImg = imgFactory.create( rai, getImageType() ); + Img loadedImg = imgFactory.create(rai); RealTypeConverters.copyFromTo( Views.extendZero( rai ), loadedImg ); rai = loadedImg; @@ -148,8 +151,8 @@ else if ( cacheResult ) Arrays.fill( cellSize, 1 ); for ( int d = 0; d < rai.numDimensions() - 1; d++ ) cellSize[d] = (int) rai.dimension( d ); - rai = FusionTools.cacheRandomAccessibleInterval( rai, Long.MAX_VALUE, - Views.iterable( rai ).firstElement().createVariable(), cellSize ); + rai = FusionTools.cacheRandomAccessibleInterval( + rai, Long.MAX_VALUE, rai.firstElement().createVariable(), cellSize); } return rai; @@ -161,8 +164,7 @@ public RandomAccessibleInterval< FloatType > getFloatImage(int timepointId, bool ImgLoaderHint... hints) { if (!active) - return (RandomAccessibleInterval< FloatType >) wrappedImgLoader.getSetupImgLoader( setupId ).getFloatImage( timepointId, - false, hints ); + return wrappedImgLoader.getSetupImgLoader(setupId).getFloatImage(timepointId, false, hints); @SuppressWarnings("unchecked") RandomAccessibleInterval< FloatType > rai = FlatFieldCorrectedRandomAccessibleIntervals.create( @@ -176,9 +178,12 @@ public RandomAccessibleInterval< FloatType > getFloatImage(int timepointId, bool RandomAccessibleInterval< FloatType > raiNormalized = new VirtuallyNormalizedRandomAccessibleInterval<>( rai ); boolean loadCompletelyRequested = false; - for (ImgLoaderHint hint : hints) - if (hint == ImgLoaderHints.LOAD_COMPLETELY) + for (ImgLoaderHint hint : hints) { + if (hint == ImgLoaderHints.LOAD_COMPLETELY) { loadCompletelyRequested = true; + break; + } + } if (loadCompletelyRequested) { @@ -187,13 +192,14 @@ public RandomAccessibleInterval< FloatType > getFloatImage(int timepointId, bool numPx *= raiNormalized.dimension( d ); final ImgFactory< FloatType > imgFactory; - if (Math.log(numPx) / Math.log( 2 ) < 31) - imgFactory = new ArrayImgFactory(); - else - imgFactory = new CellImgFactory(); + if (Math.log(numPx) / Math.log(2) < 31) { + imgFactory = new ArrayImgFactory<>(new FloatType()); + } else { + imgFactory = new CellImgFactory<>(new FloatType()); + } - Img< FloatType > loadedImg = imgFactory.create( raiNormalized, new FloatType() ); - FileMapImgLoaderLOCI2.copy(Views.extendZero( raiNormalized ), loadedImg); + Img loadedImg = imgFactory.create(raiNormalized); + RealTypeConverters.copyFromTo(Views.extendZero(raiNormalized), loadedImg); raiNormalized = loadedImg; } @@ -203,17 +209,20 @@ else if ( cacheResult ) Arrays.fill( cellSize, 1 ); for ( int d = 0; d < raiNormalized.numDimensions() - 1; d++ ) cellSize[d] = (int) raiNormalized.dimension( d ); - raiNormalized = FusionTools.cacheRandomAccessibleInterval( raiNormalized, Long.MAX_VALUE, - Views.iterable( rai ).firstElement().createVariable(), cellSize ); + raiNormalized = FusionTools.cacheRandomAccessibleInterval( + raiNormalized, Long.MAX_VALUE, rai.firstElement().createVariable(), cellSize); } rai = raiNormalized; } else { boolean loadCompletelyRequested = false; - for (ImgLoaderHint hint : hints) - if (hint == ImgLoaderHints.LOAD_COMPLETELY) + for (ImgLoaderHint hint : hints) { + if (hint == ImgLoaderHints.LOAD_COMPLETELY) { loadCompletelyRequested = true; + break; + } + } if (loadCompletelyRequested) { @@ -222,13 +231,14 @@ else if ( cacheResult ) numPx *= rai.dimension( d ); final ImgFactory< FloatType > imgFactory; - if (Math.log(numPx) / Math.log( 2 ) < 31) - imgFactory = new ArrayImgFactory(); - else - imgFactory = new CellImgFactory(); + if (Math.log(numPx) / Math.log(2) < 31) { + imgFactory = new ArrayImgFactory<>(new FloatType()); + } else { + imgFactory = new CellImgFactory<>(new FloatType()); + } - Img< FloatType > loadedImg = imgFactory.create( rai, new FloatType() ); - FileMapImgLoaderLOCI2.copy(Views.extendZero( rai ), loadedImg); + Img< FloatType > loadedImg = imgFactory.create(rai); + RealTypeConverters.copyFromTo(Views.extendZero(rai), loadedImg); rai = loadedImg; } @@ -238,8 +248,8 @@ else if ( cacheResult ) Arrays.fill( cellSize, 1 ); for ( int d = 0; d < rai.numDimensions() - 1; d++ ) cellSize[d] = (int) rai.dimension( d ); - rai = FusionTools.cacheRandomAccessibleInterval( rai, Long.MAX_VALUE, - Views.iterable( rai ).firstElement().createVariable(), cellSize ); + rai = FusionTools.cacheRandomAccessibleInterval( + rai, Long.MAX_VALUE, rai.firstElement().createVariable(), cellSize); } } @@ -275,7 +285,7 @@ public static void main(String[] args) ImgLoader il = data.getSequenceDescription().getImgLoader(); DefaultFlatfieldCorrectionWrappedImgLoader ffcil = new DefaultFlatfieldCorrectionWrappedImgLoader( il ); - ffcil.setDarkImage( new ViewId( 0, 0 ), new File( "/Users/david/desktop/ff.tif" ) ); + ffcil.setDarkImage(new ViewId(0, 0), new FlatfieldImageInfo(new File("/Users/david/desktop/ff.tif").toURI())); data.getSequenceDescription().setImgLoader( ffcil ); diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatFieldCorrectedRandomAccessibleInterval.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatFieldCorrectedRandomAccessibleInterval.java index 5e70dc356..021875215 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatFieldCorrectedRandomAccessibleInterval.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatFieldCorrectedRandomAccessibleInterval.java @@ -25,15 +25,13 @@ import net.imglib2.AbstractInterval; import net.imglib2.Cursor; import net.imglib2.Interval; -import net.imglib2.Point; +import net.imglib2.Localizable; import net.imglib2.RandomAccess; import net.imglib2.RandomAccessibleInterval; -import net.imglib2.Sampler; import net.imglib2.type.numeric.RealType; import net.imglib2.util.Pair; import net.imglib2.util.RealSum; import net.imglib2.util.ValuePair; -import net.imglib2.view.Views; /* * @@ -57,6 +55,9 @@ public FlatFieldCorrectedRandomAccessibleInterval(O outputType, RandomAccessible this.brightImg = brightImg; this.darkImg = darkImg; + if (brightImg.numDimensions() > sourceImg.numDimensions() || darkImg.numDimensions() > sourceImg.numDimensions()) + throw new IllegalArgumentException("Bright-/darkfield images have more dimensions than source image!"); + meanBrightCorrected = getMeanCorrected( brightImg, darkImg ); type = outputType; } @@ -79,68 +80,223 @@ public O getType() return type; } - private class FlatFieldCorrectedRandomAccess extends Point implements RandomAccess< O > + private class FlatFieldCorrectedRandomAccess implements RandomAccess { - /* - * TODO: manually implement move methods - */ - - private final RandomAccess< T > sourceRA; - private final RandomAccess< S > brightRA; - private final RandomAccess< R > darkRA; + private final RandomAccess sourceRA; + private final RandomAccess brightRA; + private final RandomAccess darkRA; private final O value; + private final int nDimSource; private final int nDimBright; private final int nDimDark; + // Cached min/max values to avoid virtual method calls in get() + private final double minValue; + private final double maxValue; + public FlatFieldCorrectedRandomAccess() { - super( sourceImg.numDimensions() ); sourceRA = sourceImg.randomAccess(); brightRA = brightImg.randomAccess(); darkRA = darkImg.randomAccess(); value = type.createVariable(); + nDimSource = sourceImg.numDimensions(); nDimBright = brightImg.numDimensions(); nDimDark = darkImg.numDimensions(); + minValue = value.getMinValue(); + maxValue = value.getMaxValue(); } @Override public O get() { - // NB: the flat field images seem to be 3D with 1 z slice - // if they were truly 2D, we would use position.length - 1 - final long[] positionBright = new long[ nDimBright ]; - final long[] positionDark = new long[ nDimDark ]; - // only copy position of n-1 dimensions - System.arraycopy( position, 0, positionBright, 0, nDimBright ); - System.arraycopy( position, 0, positionDark, 0, nDimDark ); - - sourceRA.setPosition( position ); - brightRA.setPosition( positionBright ); - darkRA.setPosition( positionDark ); - - final double corrBright = brightRA.get().getRealDouble() - darkRA.get().getRealDouble(); - final double corrImg = sourceRA.get().getRealDouble() - darkRA.get().getRealDouble(); + final double darkValue = darkRA.get().getRealDouble(); + final double corrBright = brightRA.get().getRealDouble() - darkValue; + final double corrImg = sourceRA.get().getRealDouble() - darkValue; if (corrBright == 0) - value.setReal( 0.0 ); + value.setReal(0.0); else { - final double corr = Math.min( Math.max( corrImg * meanBrightCorrected / corrBright, value.getMinValue() ), value.getMaxValue() ); - value.setReal( corr ); + final double corr = Math.min(Math.max(corrImg * meanBrightCorrected / corrBright, minValue), maxValue); + value.setReal(corr); } return value; } @Override - public RandomAccess< O > copy() + public void fwd(int d) + { + sourceRA.fwd(d); + if (d < nDimBright) brightRA.fwd(d); + if (d < nDimDark) darkRA.fwd(d); + } + + @Override + public void bck(int d) + { + sourceRA.bck(d); + if (d < nDimBright) brightRA.bck(d); + if (d < nDimDark) darkRA.bck(d); + } + + @Override + public void move(int distance, int d) + { + sourceRA.move(distance, d); + if (d < nDimBright) brightRA.move(distance, d); + if (d < nDimDark) darkRA.move(distance, d); + } + + @Override + public void move(long distance, int d) + { + sourceRA.move(distance, d); + if (d < nDimBright) brightRA.move(distance, d); + if (d < nDimDark) darkRA.move(distance, d); + } + + @Override + public void move(Localizable distance) + { + sourceRA.move(distance); + for (int d = 0; d < nDimBright; d++) + brightRA.move(distance.getLongPosition(d), d); + for (int d = 0; d < nDimDark; d++) + darkRA.move(distance.getLongPosition(d), d); + } + + @Override + public void move(int[] distance) + { + sourceRA.move(distance); + for (int d = 0; d < nDimBright; d++) + brightRA.move(distance[d], d); + for (int d = 0; d < nDimDark; d++) + darkRA.move(distance[d], d); + } + + @Override + public void move(long[] distance) { - final FlatFieldCorrectedRandomAccessibleInterval.FlatFieldCorrectedRandomAccess copy = new FlatFieldCorrectedRandomAccess(); - copy.setPosition( this ); + sourceRA.move(distance); + for (int d = 0; d < nDimBright; d++) + brightRA.move(distance[d], d); + for (int d = 0; d < nDimDark; d++) + darkRA.move(distance[d], d); + } + + @Override + public void setPosition(Localizable position) + { + sourceRA.setPosition(position); + for (int d = 0; d < nDimBright; d++) + brightRA.setPosition(position.getLongPosition(d), d); + for (int d = 0; d < nDimDark; d++) + darkRA.setPosition(position.getLongPosition(d), d); + } + + @Override + public void setPosition(int[] position) + { + sourceRA.setPosition(position); + for (int d = 0; d < nDimBright; d++) + brightRA.setPosition(position[d], d); + for (int d = 0; d < nDimDark; d++) + darkRA.setPosition(position[d], d); + } + + @Override + public void setPosition(long[] position) + { + sourceRA.setPosition(position); + for (int d = 0; d < nDimBright; d++) + brightRA.setPosition(position[d], d); + for (int d = 0; d < nDimDark; d++) + darkRA.setPosition(position[d], d); + } + + @Override + public void setPosition(int position, int d) + { + sourceRA.setPosition(position, d); + if (d < nDimBright) brightRA.setPosition(position, d); + if (d < nDimDark) darkRA.setPosition(position, d); + } + + @Override + public void setPosition(long position, int d) + { + sourceRA.setPosition(position, d); + if (d < nDimBright) brightRA.setPosition(position, d); + if (d < nDimDark) darkRA.setPosition(position, d); + } + + // Localizable - delegate to sourceRA + + @Override + public int numDimensions() + { + return nDimSource; + } + + @Override + public void localize(int[] position) + { + sourceRA.localize(position); + } + + @Override + public void localize(long[] position) + { + sourceRA.localize(position); + } + + @Override + public int getIntPosition(int d) + { + return sourceRA.getIntPosition(d); + } + + @Override + public long getLongPosition(int d) + { + return sourceRA.getLongPosition(d); + } + + @Override + public void localize(float[] position) + { + sourceRA.localize(position); + } + + @Override + public void localize(double[] position) + { + sourceRA.localize(position); + } + + @Override + public float getFloatPosition(int d) + { + return sourceRA.getFloatPosition(d); + } + + @Override + public double getDoublePosition(int d) + { + return sourceRA.getDoublePosition(d); + } + + @Override + public RandomAccess copy() + { + final FlatFieldCorrectedRandomAccessibleInterval.FlatFieldCorrectedRandomAccess copy = new FlatFieldCorrectedRandomAccess(); + copy.setPosition(sourceRA); return copy; } - } public static

, Q extends RealType< Q >> double getMeanCorrected(RandomAccessibleInterval< P > brightImg, RandomAccessibleInterval< Q > darkImg) @@ -148,7 +304,7 @@ public static

, Q extends RealType< Q >> double getMeanC final RealSum sum = new RealSum(); long count = 0; - final Cursor< P > brightCursor = Views.iterable( brightImg ).cursor(); + final Cursor< P > brightCursor = brightImg.cursor(); final RandomAccess< Q > darkRA = darkImg.randomAccess(); while (brightCursor.hasNext()) @@ -172,7 +328,7 @@ public static

> Pair getMinMax(RandomAcc double min = Double.MAX_VALUE; double max = - Double.MAX_VALUE; - for (final P pixel : Views.iterable( img )) + for (final P pixel : img) { double value = pixel.getRealDouble(); @@ -183,7 +339,7 @@ public static

> Pair getMinMax(RandomAcc min = value; } - return new ValuePair< Double, Double >( min, max ); + return new ValuePair<>( min, max ); } } diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatFieldCorrectedRandomAccessibleIntervals.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatFieldCorrectedRandomAccessibleIntervals.java index 4cb23038b..a2f1c2a31 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatFieldCorrectedRandomAccessibleIntervals.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatFieldCorrectedRandomAccessibleIntervals.java @@ -22,86 +22,145 @@ */ package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield; -import java.util.Arrays; - -import bdv.util.ConstantRandomAccessible; -import bdv.viewer.overlay.SourceInfoOverlayRenderer; -import net.imglib2.FinalInterval; import net.imglib2.RandomAccessibleInterval; +import net.imglib2.algorithm.blocks.BlockAlgoUtils; +import net.imglib2.algorithm.blocks.BlockSupplier; +import net.imglib2.cache.img.CachedCellImg; +import net.imglib2.converter.RealTypeConverters; +import net.imglib2.type.NativeType; import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.real.FloatType; import net.imglib2.view.Views; +/** + * Factory methods for creating flatfield-corrected images. + * + * All methods use efficient block-based processing internally via + * {@link FlatfieldCorrectionBlockSupplier}, backed by a {@link CachedCellImg}. + */ public class FlatFieldCorrectedRandomAccessibleIntervals { - public static < R extends RealType< R >, S extends RealType< S >, T extends RealType< T >> RandomAccessibleInterval< R > create( - RandomAccessibleInterval< R > sourceImg, - RandomAccessibleInterval< S > brightImg, - RandomAccessibleInterval< T > darkImg ) + /** Default cell size for block-based flatfield correction */ + private static final int[] DEFAULT_CELL_SIZE = new int[] {64, 64, 64}; + + /** + * Create a flatfield-corrected image with the same type as the source. + * Uses efficient block-based processing internally. + * + * @param sourceImg source image + * @param brightImg bright (flatfield) image, can be null + * @param darkImg dark image, can be null + * @return corrected image with same type as source + */ + public static & NativeType, S extends RealType, T extends RealType> + RandomAccessibleInterval create( + final RandomAccessibleInterval sourceImg, + final RandomAccessibleInterval brightImg, + final RandomAccessibleInterval darkImg) { - R type = Views.iterable( sourceImg ).firstElement().createVariable(); - return create( sourceImg, brightImg, darkImg, type ); + final R type = sourceImg.getType().createVariable(); + return create(sourceImg, brightImg, darkImg, type); } - public static , R extends RealType< R >, S extends RealType< S >, T extends RealType< T >> RandomAccessibleInterval< O > create( - RandomAccessibleInterval< R > sourceImg, - RandomAccessibleInterval< S > brightImg, - RandomAccessibleInterval< T > darkImg, - O outputType) + + /** + * Create a flatfield-corrected image with a specified output type. + * Uses efficient block-based processing internally. + * + * @param sourceImg source image + * @param brightImg bright (flatfield) image, can be null + * @param darkImg dark image, can be null + * @param outputType the desired output type + * @return corrected image with specified output type + */ + @SuppressWarnings("unchecked") + public static , R extends RealType & NativeType, S extends RealType, T extends RealType> + RandomAccessibleInterval create( + final RandomAccessibleInterval sourceImg, + final RandomAccessibleInterval brightImg, + final RandomAccessibleInterval darkImg, + final O outputType) { + // Create block-based corrected image (always FloatType internally) + final RandomAccessibleInterval correctedFloat = createBlockBased( + sourceImg, brightImg, darkImg, DEFAULT_CELL_SIZE); - // get intervals for bright and/or dark imgs: interval of source img, but only for dimensionality of bright/dark - final long[] minsBright; - final long[] maxsBright; - FinalInterval intervalBright = null; - if (brightImg != null) - { - minsBright = new long[brightImg.numDimensions()]; - maxsBright = new long[brightImg.numDimensions()]; - Arrays.fill( maxsBright, 1 ); - for (int d = 0; d < brightImg.numDimensions(); ++d) - { - minsBright[d] = sourceImg.min( d ); - maxsBright[d] = sourceImg.max( d ); - } - intervalBright = new FinalInterval( minsBright, maxsBright ); - } - final long[] minsDark; - final long[] maxsDark; - FinalInterval intervalDark = null; - if (darkImg != null) - { - minsDark = new long[darkImg.numDimensions()]; - maxsDark = new long[darkImg.numDimensions()]; - Arrays.fill( maxsDark, 1 ); - for (int d = 0; d < darkImg.numDimensions(); ++d) - { - minsDark[d] = sourceImg.min( d ); - maxsDark[d] = sourceImg.max( d ); - } - intervalDark = new FinalInterval( minsDark, maxsDark ); - } + // If output type is FloatType, return directly + if (outputType instanceof FloatType) + return (RandomAccessibleInterval) correctedFloat; + + // Otherwise, convert to the requested output type + return RealTypeConverters.convert(correctedFloat, outputType); + } + + /** + * Create a flatfield-corrected FloatType image using efficient block-based processing. + * The result is backed by a CachedCellImg that computes correction on-demand. + * + * @param sourceImg source image (can be any RealType) + * @param brightImg bright (flatfield) image, can be null + * @param darkImg dark image, can be null + * @return FloatType RandomAccessibleInterval with flatfield correction applied + */ + public static & NativeType, S extends RealType, T extends RealType> + RandomAccessibleInterval createBlockBased( + final RandomAccessibleInterval sourceImg, + final RandomAccessibleInterval brightImg, + final RandomAccessibleInterval darkImg) + { + return createBlockBased(sourceImg, brightImg, darkImg, DEFAULT_CELL_SIZE); + } + /** + * Create a flatfield-corrected FloatType image using efficient block-based processing. + * The result is backed by a CachedCellImg that computes correction on-demand. + * + * @param sourceImg source image (can be any RealType) + * @param brightImg bright (flatfield) image, can be null + * @param darkImg dark image, can be null + * @param cellSize cell size for the cached image + * @return FloatType RandomAccessibleInterval with flatfield correction applied + */ + public static & NativeType, S extends RealType, T extends RealType> + RandomAccessibleInterval createBlockBased( + final RandomAccessibleInterval sourceImg, + final RandomAccessibleInterval brightImg, + final RandomAccessibleInterval darkImg, + final int[] cellSize) + { + // Handle null bright/dark - if both null, just convert source to float if (brightImg == null && darkImg == null) { - // assume bright and dark images constant -> should return original - // TODO: 'optimize' by really returning sourceImg? - final ConstantRandomAccessible< FloatType > constantBright = new ConstantRandomAccessible( new FloatType(1.0f), sourceImg.numDimensions() ); - final ConstantRandomAccessible< FloatType > constantDark = new ConstantRandomAccessible( new FloatType(0.0f), sourceImg.numDimensions() ); - return new FlatFieldCorrectedRandomAccessibleInterval<>(outputType, sourceImg, Views.interval( constantBright, sourceImg ), Views.interval( constantDark, sourceImg ) ); - } - else if (brightImg == null) - { - // assume bright image == constant - final ConstantRandomAccessible< FloatType > constantBright = new ConstantRandomAccessible( new FloatType(1.0f), sourceImg.numDimensions() ); - return new FlatFieldCorrectedRandomAccessibleInterval<>(outputType, sourceImg, Views.interval( constantBright, sourceImg ), Views.interval( Views.extendBorder( darkImg ), intervalDark ) ); - } - else if (darkImg == null) - { - // assume dark image == constant == 0; - final ConstantRandomAccessible< FloatType > constantDark = new ConstantRandomAccessible( new FloatType(0.0f), sourceImg.numDimensions() ); - return new FlatFieldCorrectedRandomAccessibleInterval<>(outputType, sourceImg, Views.interval( Views.extendBorder( brightImg ), intervalBright ), Views.interval( constantDark, sourceImg ) ); + final BlockSupplier blocks = BlockSupplier + .of(Views.extendBorder(sourceImg)) + .andThen(net.imglib2.algorithm.blocks.convert.Convert.convert(new FloatType())); + return wrapWithOffset(blocks, sourceImg, cellSize); } - - return new FlatFieldCorrectedRandomAccessibleInterval<>(outputType, sourceImg, Views.interval( Views.extendBorder( brightImg ), intervalBright ), Views.interval( Views.extendBorder( darkImg ), intervalDark ) ); + + // Create the block supplier with flatfield correction + final BlockSupplier blocks = FlatfieldCorrectionBlockSupplier.of( + sourceImg, brightImg, darkImg); + + return wrapWithOffset(blocks, sourceImg, cellSize); + } + + /** + * Wrap a BlockSupplier in a CachedCellImg and translate to match source interval. + */ + private static > RandomAccessibleInterval wrapWithOffset( + final BlockSupplier blocks, + final RandomAccessibleInterval sourceImg, + final int[] cellSize) + { + // Create CachedCellImg (zero-min) + final CachedCellImg cellImg = BlockAlgoUtils.cellImg( + blocks.threadSafe(), + sourceImg.dimensionsAsLongArray(), + cellSize); + + // Translate to match source interval if not zero-min + if (Views.isZeroMin(sourceImg)) + return cellImg; + else + return Views.translate(cellImg, sourceImg.minAsLongArray()); } } diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldCorrectionBlockSupplier.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldCorrectionBlockSupplier.java new file mode 100644 index 000000000..abb68b330 --- /dev/null +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldCorrectionBlockSupplier.java @@ -0,0 +1,288 @@ +/*- + * #%L + * Software for the reconstruction of multi-view microscopic acquisitions + * like Selective Plane Illumination Microscopy (SPIM) Data. + * %% + * Copyright (C) 2012 - 2025 Multiview Reconstruction developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield; + +import static net.imglib2.type.PrimitiveType.FLOAT; +import static net.imglib2.util.Util.safeInt; + +import bdv.util.ConstantRandomAccessible; +import net.imglib2.FinalInterval; +import net.imglib2.Interval; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.algorithm.blocks.BlockSupplier; +import net.imglib2.algorithm.blocks.convert.Convert; +import net.imglib2.blocks.BlockInterval; +import net.imglib2.blocks.TempArray; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.RealType; +import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.util.Cast; +import net.imglib2.util.Intervals; +import net.imglib2.view.Views; + +/** + * Block-based flatfield correction for efficient fusion processing. + * + * Applies the correction formula: + * corrected = (source - dark) * meanBrightCorrected / (bright - dark) + * + * The bright and dark images are 2D (XY only), while the source may be 3D. + * For 3D blocks, the same 2D bright/dark data is reused for all Z slices, + * making this much more efficient than per-pixel RandomAccess. + */ +public class FlatfieldCorrectionBlockSupplier implements BlockSupplier { + private final BlockSupplier source; + private final BlockSupplier bright; + private final BlockSupplier dark; + private final double meanBrightCorrected; + private final int numDimensions; + + // Temp arrays for block data + private final TempArray srcTemp; + private final TempArray brightTemp; + private final TempArray darkTemp; + + /** + * Create a flatfield correction BlockSupplier. + * + * @param source source image BlockSupplier (can be 2D or 3D) + * @param bright bright (flatfield) image as BlockSupplier (2D) + * @param dark dark image as BlockSupplier (2D) + * @param meanBrightCorrected pre-computed mean of (bright - dark) + */ + public FlatfieldCorrectionBlockSupplier( + final BlockSupplier source, + final BlockSupplier bright, + final BlockSupplier dark, + final double meanBrightCorrected) + { + this.source = source; + this.bright = bright; + this.dark = dark; + this.meanBrightCorrected = meanBrightCorrected; + this.numDimensions = source.numDimensions(); + + this.srcTemp = TempArray.forPrimitiveType(FLOAT); + this.brightTemp = TempArray.forPrimitiveType(FLOAT); + this.darkTemp = TempArray.forPrimitiveType(FLOAT); + } + + private FlatfieldCorrectionBlockSupplier(final FlatfieldCorrectionBlockSupplier s) { + this.source = s.source.independentCopy(); + this.bright = s.bright.independentCopy(); + this.dark = s.dark.independentCopy(); + this.meanBrightCorrected = s.meanBrightCorrected; + this.numDimensions = s.numDimensions; + + this.srcTemp = TempArray.forPrimitiveType(FLOAT); + this.brightTemp = TempArray.forPrimitiveType(FLOAT); + this.darkTemp = TempArray.forPrimitiveType(FLOAT); + } + + /** + * Factory method to create a flatfield correction BlockSupplier from an existing FloatType source. + */ + public static BlockSupplier of( + final BlockSupplier source, + final RandomAccessibleInterval bright, + final RandomAccessibleInterval dark, + final double meanBrightCorrected) + { + return new FlatfieldCorrectionBlockSupplier( + source, + BlockSupplier.of(bright), + BlockSupplier.of(dark), + meanBrightCorrected); + } + + /** + * Factory method to create a flatfield correction BlockSupplier from RAIs. + * Handles interval adjustment for 2D bright/dark with 3D source, similar to + * {@link FlatFieldCorrectedRandomAccessibleIntervals#create}. + * + * @param sourceImg source image (can be any RealType, will be converted to FloatType) + * @param brightImg bright (flatfield) image, can be null + * @param darkImg dark image, can be null + * @return BlockSupplier that applies flatfield correction + */ + public static & NativeType, S extends RealType, R extends RealType> + BlockSupplier of( + final RandomAccessibleInterval sourceImg, + final RandomAccessibleInterval brightImg, + final RandomAccessibleInterval darkImg) + { + // Create source BlockSupplier and convert to float + final BlockSupplier source = BlockSupplier + .of(Views.extendBorder(sourceImg)) + .andThen(Convert.convert(new FloatType())); + + // Handle null bright/dark images + final RandomAccessibleInterval bright; + final RandomAccessibleInterval dark; + + if (brightImg == null && darkImg == null) { + // No correction needed, return source as-is + return source; + } else if (brightImg == null) { + // Assume bright == constant 1.0 + final ConstantRandomAccessible constantBright = + new ConstantRandomAccessible<>(new FloatType(1.0f), darkImg.numDimensions()); + bright = Views.interval(constantBright, createInterval(sourceImg, darkImg.numDimensions())); + dark = Views.interval(Views.extendBorder(convertToFloat(darkImg)), + createInterval(sourceImg, darkImg.numDimensions())); + } else if (darkImg == null) { + // Assume dark == constant 0.0 + final ConstantRandomAccessible constantDark = + new ConstantRandomAccessible<>(new FloatType(0.0f), brightImg.numDimensions()); + bright = Views.interval(Views.extendBorder(convertToFloat(brightImg)), + createInterval(sourceImg, brightImg.numDimensions())); + dark = Views.interval(constantDark, createInterval(sourceImg, brightImg.numDimensions())); + } else { + bright = Views.interval(Views.extendBorder(convertToFloat(brightImg)), + createInterval(sourceImg, brightImg.numDimensions())); + dark = Views.interval(Views.extendBorder(convertToFloat(darkImg)), + createInterval(sourceImg, darkImg.numDimensions())); + } + + final double meanBrightCorrected = FlatFieldCorrectedRandomAccessibleInterval.getMeanCorrected(bright, dark); + + return new FlatfieldCorrectionBlockSupplier( + source, + BlockSupplier.of(bright), + BlockSupplier.of(dark), + meanBrightCorrected); + } + + /** + * Create an interval matching the source's first N dimensions. + */ + private static FinalInterval createInterval(final Interval source, final int numDimensions) { + final long[] mins = new long[numDimensions]; + final long[] maxs = new long[numDimensions]; + for (int d = 0; d < numDimensions; d++) { + mins[d] = source.min(d); + maxs[d] = source.max(d); + } + return new FinalInterval(mins, maxs); + } + + /** + * Convert a RealType RAI to FloatType. + */ + @SuppressWarnings("unchecked") + private static > RandomAccessibleInterval convertToFloat( + final RandomAccessibleInterval img) + { + if (img.getType() instanceof FloatType) { + return (RandomAccessibleInterval) img; + } + + return net.imglib2.converter.RealTypeConverters.convert(img, new FloatType()); + } + + @Override + public void copy(final Interval interval, final Object dest) { + final BlockInterval blockInterval = BlockInterval.asBlockInterval(interval); + final int[] size = blockInterval.size(); + + final int len = safeInt(Intervals.numElements(size)); + final float[] srcArray = srcTemp.get(len); + + // Copy source block (full 3D or 2D) + source.copy(interval, srcArray); + + // Determine XY size for bright/dark + final int xyLen; + final int zSize; + if (numDimensions >= 3) { + xyLen = size[0] * size[1]; + zSize = size[2]; + } else { + xyLen = len; + zSize = 1; + } + + final float[] brightArray = brightTemp.get(xyLen); + final float[] darkArray = darkTemp.get(xyLen); + + // Create 2D interval for bright/dark (XY only) + final Interval interval2D; + if (numDimensions >= 3) { + interval2D = new FinalInterval( + new long[]{interval.min(0), interval.min(1)}, + new long[]{interval.max(0), interval.max(1)}); + } else { + interval2D = interval; + } + + // Copy bright/dark blocks (2D) + bright.copy(interval2D, brightArray); + dark.copy(interval2D, darkArray); + + // Apply correction + final float[] fdest = Cast.unchecked(dest); + final float meanBC = (float) meanBrightCorrected; + + for (int z = 0; z < zSize; z++) { + final int zOffset = z * xyLen; + for (int i = 0; i < xyLen; i++) { + final float darkVal = darkArray[i]; + final float corrBright = brightArray[i] - darkVal; + final float srcVal = srcArray[zOffset + i]; + final float corrImg = srcVal - darkVal; + + if (corrBright == 0) { + fdest[zOffset + i] = 0; + } else { + fdest[zOffset + i] = corrImg * meanBC / corrBright; + } + } + } + } + + @Override + public BlockSupplier independentCopy() { + return new FlatfieldCorrectionBlockSupplier(this); + } + + @Override + public BlockSupplier threadSafe() { + return new FlatfieldCorrectionBlockSupplier( + source.threadSafe(), + bright.threadSafe(), + dark.threadSafe(), + meanBrightCorrected); + } + + @Override + public int numDimensions() { + return numDimensions; + } + + private static final FloatType type = new FloatType(); + + @Override + public FloatType getType() { + return type; + } +} diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldCorrectionWrappedImgLoader.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldCorrectionWrappedImgLoader.java index 55666c3df..14e0f0464 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldCorrectionWrappedImgLoader.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldCorrectionWrappedImgLoader.java @@ -9,12 +9,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 2 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -22,18 +22,28 @@ */ package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield; -import java.io.File; - import mpicbg.spim.data.sequence.ImgLoader; import mpicbg.spim.data.sequence.ViewId; public interface FlatfieldCorrectionWrappedImgLoader extends ImgLoader { - public IL getWrappedImgLoder(); - public void setActive(boolean active); - public boolean isActive(); - public void setCached(boolean cached); - public boolean isCached(); - public void setBrightImage(ViewId vId, File imgFile); - public void setDarkImage(ViewId vId, File imgFile); + IL getWrappedImgLoder(); + void setActive(boolean active); + boolean isActive(); + void setCached(boolean cached); + boolean isCached(); + + /** + * Set the bright (flatfield) image for a view. + * @param vId view id + * @param info flatfield image info containing URI, format, and optional dataset path + */ + void setBrightImage(ViewId vId, FlatfieldImageInfo info); + + /** + * Set the dark (darkfield) image for a view. + * @param vId view id + * @param info flatfield image info containing URI, format, and optional dataset path + */ + void setDarkImage(ViewId vId, FlatfieldImageInfo info); } diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldImageInfo.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldImageInfo.java new file mode 100644 index 000000000..3f241d5be --- /dev/null +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldImageInfo.java @@ -0,0 +1,122 @@ +/*- + * #%L + * Software for the reconstruction of multi-view microscopic acquisitions + * like Selective Plane Illumination Microscopy (SPIM) Data. + * %% + * Copyright (C) 2012 - 2025 Multiview Reconstruction developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield; + +import java.net.URI; +import java.util.Objects; + +import org.janelia.saalfeldlab.n5.universe.StorageFormat; + +/** + * Data class holding flatfield image metadata: URI, format, and dataset path. + */ +public class FlatfieldImageInfo { + + private final URI uri; + private final StorageFormat format; + private final String dataset; + + /** + * Create a FlatfieldImageInfo with all parameters. + * + * @param uri the URI to the flatfield image + * @param format the storage format (TIF uses null, others use StorageFormat enum) + * @param dataset the dataset path within the container (null or empty for root) + */ + public FlatfieldImageInfo(final URI uri, final StorageFormat format, final String dataset) { + this.uri = uri; + this.format = format; + this.dataset = dataset; + } + + /** + * Create a FlatfieldImageInfo with URI, using TIF format and root dataset. + */ + public FlatfieldImageInfo(final URI uri) { + this(uri, null, null); + } + + /** + * Create a FlatfieldImageInfo with URI and format, using root dataset. + */ + public FlatfieldImageInfo(final URI uri, final StorageFormat format) { + this(uri, format, null); + } + + public URI getUri() { + return uri; + } + + /** + * Get the storage format. + * @return the format, or null for TIF format + */ + public StorageFormat getFormat() { + return format; + } + + /** + * Get the dataset path within the container. + * @return the dataset path, or null/empty for root + */ + public String getDataset() { + return dataset; + } + + /** + * Get the effective dataset path (empty string if null). + */ + public String getEffectiveDataset() { + return dataset == null ? "" : dataset; + } + + /** + * Check if this is a TIF format (format is null). + */ + public boolean isTif() { + return format == null; + } + + @Override + public String toString() { + return "FlatfieldImageInfo{uri=" + uri + ", format=" + format + ", dataset=" + dataset + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FlatfieldImageInfo that = (FlatfieldImageInfo) o; + if (!Objects.equals(uri, that.uri)) return false; + if (format != that.format) return false; + return Objects.equals(dataset, that.dataset); + } + + @Override + public int hashCode() { + int result = uri != null ? uri.hashCode() : 0; + result = 31 * result + (format != null ? format.hashCode() : 0); + result = 31 * result + (dataset != null ? dataset.hashCode() : 0); + return result; + } +} diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldImageLoader.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldImageLoader.java new file mode 100644 index 000000000..6704a545a --- /dev/null +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldImageLoader.java @@ -0,0 +1,210 @@ +/*- + * #%L + * Software for the reconstruction of multi-view microscopic acquisitions + * like Selective Plane Illumination Microscopy (SPIM) Data. + * %% + * Copyright (C) 2012 - 2025 Multiview Reconstruction developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield; + +import java.io.File; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import org.janelia.saalfeldlab.n5.N5Reader; +import org.janelia.saalfeldlab.n5.imglib2.N5Utils; +import org.janelia.saalfeldlab.n5.universe.StorageFormat; + +import ij.IJ; +import ij.ImagePlus; +import mpicbg.spim.data.sequence.ViewId; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.converter.RealTypeConverters; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.util.Cast; +import net.imglib2.util.Pair; +import net.imglib2.util.ValuePair; +import util.URITools; + +/** + * Helper class for loading flatfield correction images from various sources. + * + * This class handles: + * - Storage of bright/dark image info per view (URI, format, dataset path) + * - Lazy loading and caching of images + * - Support for TIF, N5, Zarr (v2/v3), and HDF5 formats + * - Support for cloud storage (S3, GCS) for N5/Zarr formats + */ +public class FlatfieldImageLoader { + + protected final Map> raiMap; + protected final Map> infoMap; + + private static final Pair NULL_PAIR = new ValuePair<>(null, null); + + public FlatfieldImageLoader() { + raiMap = new HashMap<>(); + infoMap = new HashMap<>(); + } + + public void setBrightImage(ViewId vId, FlatfieldImageInfo info) { + final Pair oldPair = infoMap.getOrDefault(vId, NULL_PAIR); + infoMap.put(vId, new ValuePair<>(info, oldPair.getB())); + } + + public void setDarkImage(ViewId vId, FlatfieldImageInfo info) { + final Pair oldPair = infoMap.getOrDefault(vId, NULL_PAIR); + infoMap.put(vId, new ValuePair<>(oldPair.getA(), info)); + } + + public RandomAccessibleInterval getBrightImg(ViewId vId) { + return getImg(vId, Pair::getA); + } + + public RandomAccessibleInterval getDarkImg(ViewId vId) { + return getImg(vId, Pair::getB); + } + + /** + * Get image for view id; the brightfield is stored in the A element of the pair, the darkfield in B + * @param vId view id + * @param infoSelector function to select info from pair + * @return image, or null if not set + */ + private RandomAccessibleInterval getImg(ViewId vId, Function, FlatfieldImageInfo> infoSelector) { + if (!infoMap.containsKey(vId)) + return null; + + final FlatfieldImageInfo info = infoSelector.apply(infoMap.get(vId)); + if (info == null) + return null; + + return loadImageIfNecessary(info); + } + + /** + * Load an image using the specified format. Supports: + * - TIF files (local only, via ImageJ) + * - N5 containers (local + cloud) + * - Zarr v2/v3 containers (local + cloud) + * - HDF5 files (local only) + * + * @param info the flatfield image info containing URI, format, and dataset path + * @return the loaded image as FloatType + */ + public RandomAccessibleInterval loadImageIfNecessary(FlatfieldImageInfo info) { + if (!raiMap.containsKey(info)) { + RandomAccessibleInterval img; + + if (info.isTif()) { + // TIF format via ImageJ + final File file = new File(info.getUri()); + final ImagePlus imp = IJ.openImage(file.getAbsolutePath()); + if (imp == null) + throw new RuntimeException("Failed to load TIF image from: " + info.getUri()); + img = ImageJFunctions.convertFloat(imp).copy(); + } else { + // N5, Zarr, or HDF5 via N5 API + final StorageFormat format = info.getFormat(); + final URI uri = info.getUri(); + + // Validate HDF5 is not used with cloud storage + if (format == StorageFormat.HDF5) { + final String scheme = uri.getScheme(); + if ("s3".equals(scheme) || "gs".equals(scheme)) { + throw new RuntimeException("HDF5 format does not support cloud storage (s3/gs). URI: " + uri); + } + } + + final N5Reader reader = URITools.instantiateN5Reader(format, uri); + final String dataset = info.getEffectiveDataset(); + final RandomAccessibleInterval raw = N5Utils.open(reader, dataset); + img = RealTypeConverters.convert(Cast.unchecked(raw), new FloatType()); + } + + raiMap.put(info, img); + } + return raiMap.get(info); + } + + /** + * Get the info map for bright/dark images per view. + * @return map from ViewId to (brightInfo, darkInfo) pair + */ + public Map> getInfoMap() { + return infoMap; + } + + // ==================== Format Parsing Utilities ==================== + + /** + * Parse a format string to StorageFormat. + * @param formatStr the format string (tif, n5, zarr, zarr2, hdf5) + * @return StorageFormat, or null for TIF format + * @throws IllegalArgumentException if format string is unknown + */ + public static StorageFormat parseFormat(String formatStr) { + if (formatStr == null || formatStr.isEmpty()) + throw new IllegalArgumentException("Format attribute is required"); + + switch (formatStr.toLowerCase()) { + case "tif": + case "tiff": + return null; // null indicates TIF format + case "n5": + return StorageFormat.N5; + case "zarr": + case "zarr3": + return StorageFormat.ZARR; + case "zarr2": + return StorageFormat.ZARR2; + case "hdf5": + case "h5": + return StorageFormat.HDF5; + default: + throw new IllegalArgumentException("Unknown flatfield format: " + formatStr + + ". Supported formats: tif, n5, zarr, zarr2, hdf5"); + } + } + + /** + * Convert StorageFormat to format string for XML serialization. + * @param format the StorageFormat (null for TIF) + * @return format string + */ + public static String formatToString(StorageFormat format) { + if (format == null) + return "tif"; + + switch (format) { + case N5: + return "n5"; + case ZARR: + return "zarr"; + case ZARR2: + return "zarr2"; + case HDF5: + return "hdf5"; + default: + throw new IllegalArgumentException("Unsupported format for flatfield: " + format); + } + } +} diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/LazyLoadingFlatFieldCorrectionMap.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/LazyLoadingFlatFieldCorrectionMap.java index 1c98182e6..8e43f2cdd 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/LazyLoadingFlatFieldCorrectionMap.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/LazyLoadingFlatFieldCorrectionMap.java @@ -9,12 +9,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 2 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -23,104 +23,57 @@ package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield; import java.io.File; -import java.util.HashMap; import java.util.Map; -import ij.IJ; -import ij.ImagePlus; import mpicbg.spim.data.sequence.ImgLoader; import mpicbg.spim.data.sequence.ViewId; import net.imglib2.RandomAccessibleInterval; import net.imglib2.img.display.imagej.ImageJFunctions; import net.imglib2.type.numeric.real.FloatType; import net.imglib2.util.Pair; -import net.imglib2.util.ValuePair; -public abstract class LazyLoadingFlatFieldCorrectionMap implements FlatfieldCorrectionWrappedImgLoader< IL > +public abstract class LazyLoadingFlatFieldCorrectionMap implements FlatfieldCorrectionWrappedImgLoader { - - protected final Map< File, RandomAccessibleInterval< FloatType > > raiMap; - protected final Map> fileMap; - + protected final FlatfieldImageLoader imageLoader; + public LazyLoadingFlatFieldCorrectionMap() { - raiMap = new HashMap<>(); - fileMap = new HashMap<>(); + imageLoader = new FlatfieldImageLoader(); } - - @Override - public void setBrightImage(ViewId vId, File imgFile) - { - if (!fileMap.containsKey( vId )) - fileMap.put( vId, new ValuePair< File, File >( null, null ) ); - final Pair< File, File > oldPair = fileMap.get( vId ); - fileMap.put( vId, new ValuePair< File, File >( imgFile, oldPair.getB() ) ); + @Override + public void setBrightImage(ViewId vId, FlatfieldImageInfo info) { + imageLoader.setBrightImage(vId, info); } @Override - public void setDarkImage(ViewId vId, File imgFile) - { - if (!fileMap.containsKey( vId )) - fileMap.put( vId, new ValuePair< File, File >( null, null ) ); - - final Pair< File, File > oldPair = fileMap.get( vId ); - fileMap.put( vId, new ValuePair< File, File >( oldPair.getA(), imgFile ) ); + public void setDarkImage(ViewId vId, FlatfieldImageInfo info) { + imageLoader.setDarkImage(vId, info); } - - protected RandomAccessibleInterval< FloatType > getBrightImg(ViewId vId) - { - if (!fileMap.containsKey( vId )) - return null; - - final File fileToLoad = fileMap.get( vId ).getA(); - - if (fileToLoad == null) - return null; - loadFileIfNecessary( fileToLoad ); - return raiMap.get( fileToLoad ); + protected RandomAccessibleInterval getBrightImg(ViewId vId) { + return imageLoader.getBrightImg(vId); } - protected RandomAccessibleInterval< FloatType > getDarkImg(ViewId vId) - { - if (!fileMap.containsKey( vId )) - return null; - - final File fileToLoad = fileMap.get( vId ).getB(); - - if (fileToLoad == null) - return null; - - loadFileIfNecessary( fileToLoad ); - return raiMap.get( fileToLoad ); + protected RandomAccessibleInterval getDarkImg(ViewId vId) { + return imageLoader.getDarkImg(vId); } - - protected void loadFileIfNecessary(File file) - { - if (raiMap.containsKey( file )) - return; - - final ImagePlus imp = IJ.openImage( file.getAbsolutePath() ); - final RandomAccessibleInterval< FloatType > img = ImageJFunctions.convertFloat( imp ).copy(); - - raiMap.put( file, img ); - } - + public static void main(String[] args) { - DefaultFlatfieldCorrectionWrappedImgLoader testImgLoader = new DefaultFlatfieldCorrectionWrappedImgLoader( null ); - testImgLoader.setBrightImage( new ViewId(0,0), new File("/Users/David/Desktop/ell2.tif" )); - RandomAccessibleInterval< FloatType > brightImg = testImgLoader.getBrightImg( new ViewId( 0, 0 ) ); - - ImageJFunctions.show( brightImg ); - + DefaultFlatfieldCorrectionWrappedImgLoader testImgLoader = new DefaultFlatfieldCorrectionWrappedImgLoader(null); + testImgLoader.setBrightImage(new ViewId(0, 0), new FlatfieldImageInfo(new File("/Users/David/Desktop/ell2.tif").toURI())); + RandomAccessibleInterval brightImg = testImgLoader.getBrightImg(new ViewId(0, 0)); + + ImageJFunctions.show(brightImg); } - public Map< ViewId, Pair< File, File > > getFileMap() + /** + * Get the info map for bright/dark images per view. + * @return map from ViewId to (brightInfo, darkInfo) pair + */ + public Map> getInfoMap() { - return fileMap; + return imageLoader.getInfoMap(); } - - } diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/MultiResolutionFlatfieldCorrectionWrappedImgLoader.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/MultiResolutionFlatfieldCorrectionWrappedImgLoader.java index 1edc09e1d..55c435434 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/MultiResolutionFlatfieldCorrectionWrappedImgLoader.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/MultiResolutionFlatfieldCorrectionWrappedImgLoader.java @@ -23,13 +23,13 @@ package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield; import java.io.File; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; -import bdv.export.WriteSequenceToHdf5; import ij.ImageJ; import mpicbg.spim.data.generic.sequence.ImgLoaderHint; import mpicbg.spim.data.generic.sequence.ImgLoaderHints; @@ -37,16 +37,17 @@ import mpicbg.spim.data.sequence.MultiResolutionSetupImgLoader; import mpicbg.spim.data.sequence.ViewId; import mpicbg.spim.data.sequence.VoxelDimensions; -import net.imglib2.Cursor; import net.imglib2.Dimensions; -import net.imglib2.FinalDimensions; -import net.imglib2.RandomAccess; -import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; +import net.imglib2.algorithm.blocks.BlockAlgoUtils; +import net.imglib2.algorithm.blocks.BlockSupplier; +import net.imglib2.algorithm.blocks.downsample.Downsample; import net.imglib2.converter.RealTypeConverters; import net.imglib2.img.Img; import net.imglib2.img.ImgFactory; import net.imglib2.img.array.ArrayImgFactory; +import net.imglib2.img.cell.AbstractCellImg; +import net.imglib2.img.cell.CellGrid; import net.imglib2.img.cell.CellImgFactory; import net.imglib2.img.display.imagej.ImageJFunctions; import net.imglib2.realtransform.AffineTransform3D; @@ -58,7 +59,6 @@ import net.imglib2.view.Views; import net.preibisch.mvrecon.fiji.plugin.queryXML.LoadParseQueryXML; import net.preibisch.mvrecon.fiji.spimdata.SpimData2; -import net.preibisch.mvrecon.fiji.spimdata.imgloaders.filemap2.FileMapImgLoaderLOCI2; import net.preibisch.mvrecon.process.fusion.FusionTools; @@ -66,12 +66,12 @@ public class MultiResolutionFlatfieldCorrectionWrappedImgLoader extends LazyLoadingFlatFieldCorrectionMap< MultiResolutionImgLoader > implements MultiResolutionImgLoader { - private MultiResolutionImgLoader wrappedImgLoader; + private final MultiResolutionImgLoader wrappedImgLoader; private boolean active; private boolean cacheResult; /* downsampled bright/dark images */ - private final Map< Pair< File, List< Integer > >, RandomAccessibleInterval< FloatType > > dsRaiMap; + private final Map>, RandomAccessibleInterval> dsRaiMap; public MultiResolutionFlatfieldCorrectionWrappedImgLoader(MultiResolutionImgLoader wrappedImgLoader) { @@ -89,55 +89,39 @@ public MultiResolutionFlatfieldCorrectionWrappedImgLoader(MultiResolutionImgLoad dsRaiMap = new HashMap<>(); } - protected RandomAccessibleInterval< FloatType > getOrCreateBrightImgDownsampled(ViewId vId, - int[] downsamplingFactors) - { - ArrayList< Integer > dsFactorList = new ArrayList< Integer >(); - for ( int i : downsamplingFactors ) - dsFactorList.add( i ); - - final ValuePair< File, List< Integer > > key = new ValuePair<>( fileMap.get( vId ).getA(), dsFactorList ); - - if ( !dsRaiMap.containsKey( key ) ) - { - final RandomAccessibleInterval< FloatType > brightImg = getBrightImg( vId ); - - if ( brightImg == null ) - return null; - - // NB: we add a singleton z-dimension here for downsampleHDF5 to - // work - final RandomAccessibleInterval< FloatType > downsampled = downsampleHDF5( - Views.addDimension( brightImg, 0, 0 ), downsamplingFactors ); - dsRaiMap.put( key, downsampled ); - } - - return dsRaiMap.get( key ); + protected RandomAccessibleInterval< FloatType > getOrCreateBrightImgDownsampled(ViewId vId, int[] downsamplingFactors) { + return getOrCreateDownsampledImg(vId, downsamplingFactors, Pair::getA, this::getBrightImg); } - protected RandomAccessibleInterval< FloatType > getOrCreateDarkImgDownsampled(ViewId vId, int[] downsamplingFactors) - { - ArrayList< Integer > dsFactorList = new ArrayList< Integer >(); - for ( int i : downsamplingFactors ) - dsFactorList.add( i ); - - final ValuePair< File, List< Integer > > key = new ValuePair<>( fileMap.get( vId ).getB(), dsFactorList ); - - if ( !dsRaiMap.containsKey( key ) ) - { - final RandomAccessibleInterval< FloatType > darkImg = getDarkImg( vId ); + protected RandomAccessibleInterval< FloatType > getOrCreateDarkImgDownsampled(ViewId vId, int[] downsamplingFactors) { + return getOrCreateDownsampledImg(vId, downsamplingFactors, Pair::getB, this::getDarkImg); + } - if ( darkImg == null ) + /** + * Generic method to get a downsampled image or do downsampling on the fly. The bright image + * is stored in the A element of the pair, the dark image in B. + */ + private RandomAccessibleInterval getOrCreateDownsampledImg( + ViewId vId, + int[] downsamplingFactors, + Function, FlatfieldImageInfo> infoSelector, + Function> imgGetter + ) { + // Convert to a list here to have a proper hash code for the map key + List dsFactorList = Arrays.stream(downsamplingFactors).boxed().collect(Collectors.toList()); + final ValuePair> key = new ValuePair<>(infoSelector.apply(getInfoMap().get(vId)), dsFactorList); + + if (!dsRaiMap.containsKey(key)) { + final RandomAccessibleInterval img = imgGetter.apply(vId); + + if (img == null) return null; - // NB: we add a singleton z-dimension here for downsampleHDF5 to - // work - final RandomAccessibleInterval< FloatType > downsampled = downsampleHDF5( - Views.addDimension( darkImg, 0, 0 ), downsamplingFactors ); - dsRaiMap.put( key, downsampled ); + final RandomAccessibleInterval downsampled = downsampleHDF5(img, downsamplingFactors); + dsRaiMap.put(key, downsampled); } - return dsRaiMap.get( key ); + return dsRaiMap.get(key); } @Override @@ -185,8 +169,11 @@ public RandomAccessibleInterval< T > getImage(int timepointId, int level, ImgLoa final MultiResolutionSetupImgLoader< ? > wrpSetupIL = wrappedImgLoader.getSetupImgLoader( setupId ); - if(!active) - return (RandomAccessibleInterval< T >) wrpSetupIL.getImage( timepointId, level, hints ); + if(!active) { + @SuppressWarnings("unchecked") + RandomAccessibleInterval image = (RandomAccessibleInterval) wrpSetupIL.getImage(timepointId, level, hints); + return image; + } final int n = wrpSetupIL.getImageSize( timepointId ).numDimensions(); @@ -205,9 +192,12 @@ public RandomAccessibleInterval< T > getImage(int timepointId, int level, ImgLoa getOrCreateDarkImgDownsampled( new ViewId( timepointId, setupId ), dsFactors ) ); boolean loadCompletelyRequested = false; - for (ImgLoaderHint hint : hints) - if (hint == ImgLoaderHints.LOAD_COMPLETELY) + for (ImgLoaderHint hint : hints) { + if (hint == ImgLoaderHints.LOAD_COMPLETELY) { loadCompletelyRequested = true; + break; + } + } if (loadCompletelyRequested) { @@ -216,13 +206,14 @@ public RandomAccessibleInterval< T > getImage(int timepointId, int level, ImgLoa numPx *= rai.dimension( d ); final ImgFactory< T > imgFactory; - if (Math.log(numPx) / Math.log( 2 ) < 31) - imgFactory = new ArrayImgFactory(); - else - imgFactory = new CellImgFactory(); + if (Math.log(numPx) / Math.log(2) < 31) { + imgFactory = new ArrayImgFactory<>(getImageType()); + } else { + imgFactory = new CellImgFactory<>(getImageType()); + } - Img< T > loadedImg = imgFactory.create( rai, getImageType() ); - RealTypeConverters.copyFromTo( Views.extendZero( rai ), loadedImg ); + Img loadedImg = imgFactory.create(rai); + RealTypeConverters.copyFromTo(Views.extendZero(rai), loadedImg); rai = loadedImg; } @@ -232,8 +223,8 @@ else if ( cacheResult ) Arrays.fill( cellSize, 1 ); for ( int d = 0; d < rai.numDimensions() - 1; d++ ) cellSize[d] = (int) rai.dimension( d ); - rai = FusionTools.cacheRandomAccessibleInterval( rai, Long.MAX_VALUE, - Views.iterable( rai ).firstElement().createVariable(), cellSize ); + rai = FusionTools.cacheRandomAccessibleInterval( + rai, Long.MAX_VALUE, rai.firstElement().createVariable(), cellSize); } return rai; } @@ -268,9 +259,12 @@ public RandomAccessibleInterval< FloatType > getFloatImage(int timepointId, int RandomAccessibleInterval< FloatType > raiNormalized = new VirtuallyNormalizedRandomAccessibleInterval<>( rai ); boolean loadCompletelyRequested = false; - for (ImgLoaderHint hint : hints) - if (hint == ImgLoaderHints.LOAD_COMPLETELY) + for (ImgLoaderHint hint : hints) { + if (hint == ImgLoaderHints.LOAD_COMPLETELY) { loadCompletelyRequested = true; + break; + } + } if (loadCompletelyRequested) { @@ -279,13 +273,14 @@ public RandomAccessibleInterval< FloatType > getFloatImage(int timepointId, int numPx *= raiNormalized.dimension( d ); final ImgFactory< FloatType > imgFactory; - if (Math.log(numPx) / Math.log( 2 ) < 31) - imgFactory = new ArrayImgFactory(); - else - imgFactory = new CellImgFactory(); + if (Math.log(numPx) / Math.log(2) < 31) { + imgFactory = new ArrayImgFactory<>(new FloatType()); + } else { + imgFactory = new CellImgFactory<>(new FloatType()); + } - Img< FloatType > loadedImg = imgFactory.create( raiNormalized, new FloatType() ); - FileMapImgLoaderLOCI2.copy(Views.extendZero( raiNormalized ), loadedImg); + Img loadedImg = imgFactory.create(raiNormalized); + RealTypeConverters.copyFromTo(Views.extendZero(raiNormalized), loadedImg); raiNormalized = loadedImg; } @@ -295,17 +290,19 @@ else if ( cacheResult ) Arrays.fill( cellSize, 1 ); for ( int d = 0; d < raiNormalized.numDimensions() - 1; d++ ) cellSize[d] = (int) raiNormalized.dimension( d ); - rai = FusionTools.cacheRandomAccessibleInterval( raiNormalized, Long.MAX_VALUE, - Views.iterable( rai ).firstElement().createVariable(), cellSize ); + rai = FusionTools.cacheRandomAccessibleInterval( + raiNormalized, Long.MAX_VALUE, rai.firstElement().createVariable(), cellSize); } rai = raiNormalized; } - else - { + else { boolean loadCompletelyRequested = false; - for (ImgLoaderHint hint : hints) - if (hint == ImgLoaderHints.LOAD_COMPLETELY) + for (ImgLoaderHint hint : hints) { + if (hint == ImgLoaderHints.LOAD_COMPLETELY) { loadCompletelyRequested = true; + break; + } + } if (loadCompletelyRequested) { @@ -315,12 +312,12 @@ else if ( cacheResult ) final ImgFactory< FloatType > imgFactory; if (Math.log(numPx) / Math.log( 2 ) < 31) - imgFactory = new ArrayImgFactory(); + imgFactory = new ArrayImgFactory<>(new FloatType()); else - imgFactory = new CellImgFactory(); + imgFactory = new CellImgFactory<>(new FloatType()); - Img< FloatType > loadedImg = imgFactory.create( rai, new FloatType() ); - FileMapImgLoaderLOCI2.copy(Views.extendZero( rai ), loadedImg); + Img loadedImg = imgFactory.create(rai); + RealTypeConverters.copyFromTo(Views.extendZero(rai), loadedImg); rai = loadedImg; } @@ -330,8 +327,8 @@ else if ( cacheResult ) Arrays.fill( cellSize, 1 ); for ( int d = 0; d < rai.numDimensions() - 1; d++ ) cellSize[d] = (int) rai.dimension( d ); - rai = FusionTools.cacheRandomAccessibleInterval( rai, Long.MAX_VALUE, - Views.iterable( rai ).firstElement().createVariable(), cellSize ); + rai = FusionTools.cacheRandomAccessibleInterval( + rai, Long.MAX_VALUE, rai.firstElement().createVariable(), cellSize); } } @@ -398,84 +395,54 @@ public Dimensions getImageSize(int timepointId, int level) } /** - * downsampling code form {@link WriteSequenceToHdf5}, distilled into one - * method - * - * @param input - * image to downsample - * @param dsFactor - * factors to downsample by - * @param - * the image type - * @return downsampled image + * Downsample an image using the imglib2-algorithm blocks API. + *

+ * If the input is a cell/chunked image, the output cell size is computed + * to align with input chunk boundaries (input chunk size / downsampling factor). + * + * @param input image to downsample + * @param dsFactor factors to downsample by (may have more dimensions than input) + * @param the image type + * @return downsampled image, or input unchanged if no downsampling needed */ - public static & NativeType< T >> RandomAccessibleInterval< T > downsampleHDF5( - RandomAccessibleInterval< T > input, final int[] dsFactor) - { - final long[] blockMin = new long[input.numDimensions()]; - - final long[] outDim = new long[input.numDimensions()]; - for ( int d = 0; d < input.numDimensions(); d++ ) - outDim[d] = Math.max( input.dimension( d ) / dsFactor[d], 1 ); - - final Img< T > downsampled = new ArrayImgFactory< T >().create( new FinalDimensions( outDim ), - Views.iterable( input ).firstElement().createVariable() ); - final RandomAccess< T > randomAccess = Views.extendBorder( input ).randomAccess(); - - final Cursor< T > out = downsampled.cursor(); - - double scale = 1; - for ( int f : dsFactor ) - scale *= f; - scale = 1.0 / scale; - - final int numBlockPixels = (int) ( outDim[0] * outDim[1] * outDim[2] ); - final double[] accumulator = new double[numBlockPixels]; - - randomAccess.setPosition( blockMin ); - - final int ox = (int) outDim[0]; - final int oy = (int) outDim[1]; - final int oz = (int) outDim[2]; - - final int sx = ox * dsFactor[0]; - final int sy = oy * dsFactor[1]; - final int sz = oz * dsFactor[2]; + public static & NativeType> RandomAccessibleInterval downsampleHDF5( + RandomAccessibleInterval input, + final int[] dsFactor + ) { + final int n = input.numDimensions(); + + // Build effective factors matching input dimensions, check if downsampling needed + boolean needsDownsampling = false; + final int[] effectiveFactors = new int[n]; + for (int d = 0; d < n; d++) { + effectiveFactors[d] = (d < dsFactor.length) ? dsFactor[d] : 1; + if (effectiveFactors[d] > 1) + needsDownsampling = true; + } - int i = 0; - for ( int z = 0, bz = 0; z < sz; ++z ) - { - for ( int y = 0, by = 0; y < sy; ++y ) - { - for ( int x = 0, bx = 0; x < sx; ++x ) - { - accumulator[i] += randomAccess.get().getRealDouble(); - randomAccess.fwd( 0 ); - if ( ++bx == dsFactor[0] ) - { - bx = 0; - ++i; - } - } - randomAccess.move( -sx, 0 ); - randomAccess.fwd( 1 ); - if ( ++by == dsFactor[1] ) - by = 0; - else - i -= ox; - } - randomAccess.move( -sy, 1 ); - randomAccess.fwd( 2 ); - if ( ++bz == dsFactor[2] ) - bz = 0; - else - i -= ox * oy; + // Return input unchanged if all factors are 1 + if (!needsDownsampling) + return input; + + final long[] outDim = new long[n]; + for (int d = 0; d < n; d++) + outDim[d] = Math.max(input.dimension(d) / effectiveFactors[d], 1); + + // Determine output cell size - use input chunk size if available + final int[] cellSize = new int[n]; + if (input instanceof AbstractCellImg) { + @SuppressWarnings("rawtypes") + final CellGrid grid = ((AbstractCellImg) input).getCellGrid(); + grid.cellDimensions(cellSize); + } else { + // Default fallback for non-chunked images + Arrays.fill(cellSize, 128); } - for ( int j = 0; j < numBlockPixels; ++j ) - out.next().setReal( accumulator[j] * scale ); + final BlockSupplier blocks = BlockSupplier.of(input) + .andThen(Downsample.downsample(effectiveFactors)); - return downsampled; + return BlockAlgoUtils.cellImg(blocks, outDim, cellSize); } public static void main(String[] args) @@ -488,7 +455,7 @@ public static void main(String[] args) MultiResolutionImgLoader il = (MultiResolutionImgLoader) data.getSequenceDescription().getImgLoader(); MultiResolutionFlatfieldCorrectionWrappedImgLoader ffcil = new MultiResolutionFlatfieldCorrectionWrappedImgLoader( il ); - ffcil.setDarkImage( new ViewId( 0, 0 ), new File( "/Users/david/desktop/ff.tif" ) ); + ffcil.setDarkImage( new ViewId( 0, 0 ), new FlatfieldImageInfo( new File( "/Users/david/desktop/ff.tif" ).toURI(), null ) ); data.getSequenceDescription().setImgLoader( ffcil ); diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestDecoratorChain.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestDecoratorChain.java new file mode 100644 index 000000000..4fe5b8662 --- /dev/null +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestDecoratorChain.java @@ -0,0 +1,217 @@ +/*- + * #%L + * Software for the reconstruction of multi-view microscopic acquisitions + * like Selective Plane Illumination Microscopy (SPIM) Data. + * %% + * Copyright (C) 2012 - 2025 Multiview Reconstruction developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield; + +import java.io.File; +import java.util.HashMap; + +import ij.ImageJ; +import mpicbg.spim.data.SpimDataException; +import mpicbg.spim.data.sequence.MultiResolutionImgLoader; +import mpicbg.spim.data.sequence.SequenceDescription; +import mpicbg.spim.data.sequence.ViewId; +import mpicbg.spim.data.sequence.ViewSetup; +import net.imglib2.FinalInterval; +import net.imglib2.Interval; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.type.numeric.real.FloatType; +import net.preibisch.mvrecon.fiji.spimdata.SpimData2; +import net.preibisch.mvrecon.fiji.spimdata.XmlIoSpimData2; +import net.preibisch.mvrecon.fiji.spimdata.imgloaders.splitting.SplitMultiResolutionImgLoader; + +/** + * Test class demonstrating the full decorator chain: + * + * BaseLoader (N5/OME-Zarr) + * → MultiResolutionFlatfieldCorrectionWrappedImgLoader (applies correction) + * → SplitMultiResolutionImgLoader (splits into regions) + * + * This shows how to compose multiple wrapper/decorator layers while maintaining + * the MultiResolutionImgLoader interface throughout the chain. + */ +public class TestDecoratorChain { + // Mapping from ViewSetup ID to Tile ID (from dataset.xml) + private static final int[][] SETUP_TO_TILE = { + {0, 187}, + {1, 188}, + {2, 189}, + {3, 200}, + {4, 201}, + {5, 202}, + {6, 213}, + {7, 214}, + {8, 215} + }; + + public static void main(String[] args) throws SpimDataException { + // ========== CONFIGURATION ========== + final String basePath = "/Users/innerbergerm/Projects/janelia/multiview-reconstruction/"; + final String xmlPath = basePath + "data/dataset.xml"; + final String correctionPath = basePath + "dark_and_flatfields/"; + + // Which setup to demonstrate (0-8) + final int setupToShow = 0; + final int timepoint = 0; + + // ========== STEP 1: Load dataset and get base loader ========== + System.out.println("=== STEP 1: Loading dataset ==="); + System.out.println("XML path: " + xmlPath); + + final SpimData2 data = new XmlIoSpimData2().load(xmlPath); + final SequenceDescription seqDesc = data.getSequenceDescription(); + + // The base loader - this could be N5ImageLoader, AllenOMEZarrLoader, etc. + final MultiResolutionImgLoader baseLoader = + (MultiResolutionImgLoader) seqDesc.getImgLoader(); + + System.out.println("Base loader type: " + baseLoader.getClass().getSimpleName()); + + // ========== STEP 2: Wrap with flatfield correction ========== + System.out.println("\n=== STEP 2: Wrapping with flatfield correction ==="); + + final MultiResolutionFlatfieldCorrectionWrappedImgLoader correctedLoader = + new MultiResolutionFlatfieldCorrectionWrappedImgLoader(baseLoader, true); + + // Configure correction images for each view setup + for (int[] mapping : SETUP_TO_TILE) { + final int setupId = mapping[0]; + final int tileId = mapping[1]; + + // Find darkfield file + final File darkfield = new File(correctionPath + "setup" + tileId + "-AVG_darkfield-fromdata.tif"); + + // Find flatfield file (try both naming conventions) + File flatfield = new File(correctionPath + "setup" + tileId + "-flatfield.tif"); + if (!flatfield.exists()) + flatfield = new File(correctionPath + "setup" + tileId + "-flatfield (fixed by mirroring).tif"); + + final ViewId viewId = new ViewId(timepoint, setupId); + + if (darkfield.exists()) { + correctedLoader.setDarkImage(viewId, new FlatfieldImageInfo(darkfield.toURI(), null)); + System.out.println(" Setup " + setupId + ": darkfield = " + darkfield.getName()); + } + + if (flatfield.exists()) { + correctedLoader.setBrightImage(viewId, new FlatfieldImageInfo(flatfield.toURI(), null)); + System.out.println(" Setup " + setupId + ": flatfield = " + flatfield.getName()); + } + } + + // ========== STEP 3: Wrap with splitting ========== + System.out.println("\n=== STEP 3: Wrapping with splitting ==="); + + // Get original image dimensions for the setup we're demonstrating + final ViewSetup vs = seqDesc.getViewSetups().get(setupToShow); + final long[] dims = new long[3]; + vs.getSize().dimensions(dims); + System.out.println("Original image size: " + dims[0] + " x " + dims[1] + " x " + dims[2]); + + // Create a simple 2x1 split in X dimension + // Split the 512-wide image into two 256-wide regions + final long splitX = dims[0] / 2; + + // Define the mappings for split regions + // New setup IDs 100, 101 will map to original setup 0, with different X intervals + final HashMap new2oldSetupId = new HashMap<>(); + final HashMap newSetupId2Interval = new HashMap<>(); + + // Split region 0: left half [0, splitX) x [0, dimY) x [0, dimZ) + new2oldSetupId.put(100, setupToShow); + newSetupId2Interval.put(100, new FinalInterval( + new long[] {0, 0, 0}, + new long[] {splitX - 1, dims[1] - 1, dims[2] - 1} + )); + + // Split region 1: right half [splitX, dimX) x [0, dimY) x [0, dimZ) + new2oldSetupId.put(101, setupToShow); + newSetupId2Interval.put(101, new FinalInterval( + new long[] {splitX, 0, 0}, + new long[] {dims[0] - 1, dims[1] - 1, dims[2] - 1} + )); + + System.out.println("Created 2 split regions:"); + System.out.println(" Setup 100: X=[0, " + (splitX-1) + "] (left half)"); + System.out.println(" Setup 101: X=[" + splitX + ", " + (dims[0]-1) + "] (right half)"); + + // Create the split loader wrapping the CORRECTED loader + // This is the key: correction is applied BEFORE splitting + final SplitMultiResolutionImgLoader splitLoader = new SplitMultiResolutionImgLoader( + correctedLoader, // <-- corrected loader, not base loader! + new2oldSetupId, + newSetupId2Interval, + seqDesc + ); + + // ========== STEP 4: Display comparison images ========== + System.out.println("\n=== STEP 4: Displaying images ==="); + new ImageJ(); + + final int tileId = SETUP_TO_TILE[setupToShow][1]; + + // 4a. Show UNCORRECTED original (full image, level 0) + System.out.println("Loading uncorrected image..."); + final RandomAccessibleInterval uncorrected = + baseLoader.getSetupImgLoader(setupToShow).getFloatImage(timepoint, 0, false); + ImageJFunctions.show(uncorrected, "1. Uncorrected - Setup " + setupToShow + " (tile " + tileId + ")"); + + // 4b. Show CORRECTED (full image, level 0) + System.out.println("Loading corrected image..."); + final RandomAccessibleInterval corrected = + correctedLoader.getSetupImgLoader(setupToShow).getFloatImage(timepoint, 0, false); + ImageJFunctions.show(corrected, "2. Corrected - Setup " + setupToShow + " (tile " + tileId + ")"); + + // 4c. Show CORRECTED + SPLIT (left half, level 0) + System.out.println("Loading corrected + split (left half)..."); + final RandomAccessibleInterval splitLeft = + splitLoader.getSetupImgLoader(100).getFloatImage(timepoint, 0, false); + ImageJFunctions.show(splitLeft, "3. Corrected+Split LEFT - Setup 100"); + + // 4d. Show CORRECTED + SPLIT (right half, level 0) + System.out.println("Loading corrected + split (right half)..."); + final RandomAccessibleInterval splitRight = + splitLoader.getSetupImgLoader(101).getFloatImage(timepoint, 0, false); + ImageJFunctions.show(splitRight, "4. Corrected+Split RIGHT - Setup 101"); + + // ========== Summary ========== + System.out.println("\n=== DECORATOR CHAIN SUMMARY ==="); + System.out.println("Layer 1 (innermost): " + baseLoader.getClass().getSimpleName()); + System.out.println("Layer 2 (middle): " + correctedLoader.getClass().getSimpleName()); + System.out.println("Layer 3 (outermost): " + splitLoader.getClass().getSimpleName()); + System.out.println(); + System.out.println("Data flow:"); + System.out.println(" Request for split region 100 or 101"); + System.out.println(" → SplitMultiResolutionImgLoader maps to setup " + setupToShow + " with interval"); + System.out.println(" → MultiResolutionFlatfieldCorrectionWrappedImgLoader applies correction"); + System.out.println(" → Base loader fetches raw pixels from N5"); + System.out.println(" → Corrected pixels flow back up through the chain"); + System.out.println(" → Split interval is extracted and returned"); + System.out.println(); + System.out.println("Compare the images to verify:"); + System.out.println(" - Image 1 vs 2: See flatfield correction effect"); + System.out.println(" - Image 2 vs 3+4: Verify split regions match the corrected full image"); + System.out.println(); + System.out.println("Tip: Use Image > Adjust > Brightness/Contrast (Ctrl+Shift+C)"); + } +} diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestFlatfieldCorrection.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestFlatfieldCorrection.java new file mode 100644 index 000000000..cb996338b --- /dev/null +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestFlatfieldCorrection.java @@ -0,0 +1,116 @@ +/*- + * #%L + * Software for the reconstruction of multi-view microscopic acquisitions + * like Selective Plane Illumination Microscopy (SPIM) Data. + * %% + * Copyright (C) 2012 - 2025 Multiview Reconstruction developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield; + +import ij.ImageJ; +import mpicbg.spim.data.SpimDataException; +import mpicbg.spim.data.sequence.ImgLoader; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.type.numeric.real.FloatType; +import net.preibisch.mvrecon.fiji.spimdata.SpimData2; +import net.preibisch.mvrecon.fiji.spimdata.XmlIoSpimData2; + +/** + * Test class for XML-based on-the-fly flatfield/darkfield correction. + * + * Loads dataset_corrected.xml which has flatfield correction configured + * directly in the ImageLoader section. No manual configuration needed! + * + * The XML wraps the N5 loader with MultiResolutionFlatfieldCorrectionWrappedImgLoader + * and specifies bright/dark images for each view setup. + */ +public class TestFlatfieldCorrection { + + public static void main(String[] args) throws SpimDataException { + // Paths + final String basePath = "/Users/innerbergerm/Projects/janelia/multiview-reconstruction/"; + final String correctedXmlPath = basePath + "data/dataset_corrected.xml"; + final String uncorrectedXmlPath = basePath + "data/dataset.xml"; + + // Which setup to display (0-8) + final int setupToShow = 0; + final int timepoint = 0; + + // ========== Load CORRECTED dataset (from XML with flatfield config) ========== + System.out.println("=== Loading CORRECTED dataset ==="); + System.out.println("XML path: " + correctedXmlPath); + + final SpimData2 correctedData = new XmlIoSpimData2().load(correctedXmlPath); + final ImgLoader correctedImgLoader = correctedData.getSequenceDescription().getImgLoader(); + + System.out.println("ImgLoader type: " + correctedImgLoader.getClass().getSimpleName()); + + // Verify it's a flatfield-corrected loader + if (correctedImgLoader instanceof FlatfieldCorrectionWrappedImgLoader) { + final FlatfieldCorrectionWrappedImgLoader ffcLoader = + (FlatfieldCorrectionWrappedImgLoader) correctedImgLoader; + System.out.println(" Correction active: " + ffcLoader.isActive()); + System.out.println(" Caching enabled: " + ffcLoader.isCached()); + System.out.println(" Wrapped loader: " + ffcLoader.getWrappedImgLoder().getClass().getSimpleName()); + } + + // ========== Load UNCORRECTED dataset (original XML) ========== + System.out.println("\n=== Loading UNCORRECTED dataset ==="); + System.out.println("XML path: " + uncorrectedXmlPath); + + final SpimData2 uncorrectedData = new XmlIoSpimData2().load(uncorrectedXmlPath); + final ImgLoader uncorrectedImgLoader = uncorrectedData.getSequenceDescription().getImgLoader(); + + System.out.println("ImgLoader type: " + uncorrectedImgLoader.getClass().getSimpleName()); + + // ========== Display images for comparison ========== + new ImageJ(); + + // Get tile ID from ViewSetup metadata (no hardcoded mapping needed!) + final int tileId = correctedData.getSequenceDescription() + .getViewSetups().get(setupToShow).getTile().getId(); + + System.out.println("\n=== Displaying setup " + setupToShow + " (tile " + tileId + ") ==="); + System.out.println(" Dimensions: " + correctedData.getSequenceDescription() + .getViewSetups().get(setupToShow).getSize()); + + // Load and display UNCORRECTED image + System.out.println("Loading uncorrected image..."); + final RandomAccessibleInterval uncorrected = + uncorrectedImgLoader.getSetupImgLoader(setupToShow).getFloatImage(timepoint, false); + ImageJFunctions.show(uncorrected, "1. Uncorrected - Setup " + setupToShow + " (tile " + tileId + ")"); + + // Load and display CORRECTED image + System.out.println("Loading corrected image..."); + final RandomAccessibleInterval corrected = + correctedImgLoader.getSetupImgLoader(setupToShow).getFloatImage(timepoint, false); + ImageJFunctions.show(corrected, "2. Corrected - Setup " + setupToShow + " (tile " + tileId + ")"); + + // ========== Summary ========== + System.out.println("\n=== SUMMARY ==="); + System.out.println("Flatfield correction is now configured in the XML!"); + System.out.println("No manual setBrightImage()/setDarkImage() calls needed."); + System.out.println(); + System.out.println("Compare the two images to verify correction:"); + System.out.println(" - Image 1: Raw data from N5"); + System.out.println(" - Image 2: Corrected with flatfield/darkfield"); + System.out.println(); + System.out.println("Tip: Use Image > Adjust > Brightness/Contrast (Ctrl+Shift+C)"); + } +} diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestViewerFlatfieldCorrection.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestViewerFlatfieldCorrection.java new file mode 100644 index 000000000..4910a12be --- /dev/null +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestViewerFlatfieldCorrection.java @@ -0,0 +1,231 @@ +/*- + * #%L + * Software for the reconstruction of multi-view microscopic acquisitions + * like Selective Plane Illumination Microscopy (SPIM) Data. + * %% + * Copyright (C) 2012 - 2025 Multiview Reconstruction developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield; + +import java.util.HashMap; + +import bdv.ViewerImgLoader; +import bdv.ViewerSetupImgLoader; +import ij.ImageJ; +import mpicbg.spim.data.SpimDataException; +import mpicbg.spim.data.sequence.MultiResolutionSetupImgLoader; +import mpicbg.spim.data.sequence.SequenceDescription; +import mpicbg.spim.data.sequence.ViewSetup; +import net.imglib2.FinalInterval; +import net.imglib2.Interval; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.type.numeric.real.FloatType; +import net.preibisch.mvrecon.fiji.spimdata.SpimData2; +import net.preibisch.mvrecon.fiji.spimdata.XmlIoSpimData2; +import net.preibisch.mvrecon.fiji.spimdata.imgloaders.splitting.SplitViewerImgLoader; + +/** + * Test class for XML-based ViewerFlatfieldCorrectionWrappedImgLoader. + * + * Demonstrates the full ViewerImgLoader-compatible decorator chain: + * N5ImageLoader (ViewerImgLoader) + * -> ViewerFlatfieldCorrectionWrappedImgLoader (ViewerImgLoader) + * -> SplitViewerImgLoader (ViewerImgLoader) + * + * Loads dataset_corrected_viewer.xml which has flatfield correction configured + * directly in the ImageLoader section. No manual configuration needed! + * + * This maintains full BDV compatibility throughout the chain, including: + * - Cache control delegation + * - Volatile image support + * - Multi-resolution mipmap levels + */ +public class TestViewerFlatfieldCorrection { + public static void main(String[] args) throws SpimDataException { + // Paths + final String basePath = "/Users/innerbergerm/Projects/janelia/multiview-reconstruction/"; + final String correctedXmlPath = basePath + "data/dataset_corrected_viewer_s3.xml"; + final String uncorrectedXmlPath = basePath + "data/dataset.xml"; + + // Which setup to demonstrate (0-8) + final int setupToShow = 0; + final int timepoint = 0; + + // ========== STEP 1: Load CORRECTED dataset (from XML with flatfield config) ========== + System.out.println("=== STEP 1: Loading CORRECTED dataset ==="); + System.out.println("XML path: " + correctedXmlPath); + + final SpimData2 correctedData = new XmlIoSpimData2().load(correctedXmlPath); + final SequenceDescription correctedSeqDesc = correctedData.getSequenceDescription(); + + // Verify it's a ViewerFlatfieldCorrectionWrappedImgLoader + if (!(correctedSeqDesc.getImgLoader() instanceof ViewerFlatfieldCorrectionWrappedImgLoader)) { + System.err.println("ERROR: Expected ViewerFlatfieldCorrectionWrappedImgLoader!"); + System.err.println("Loader type: " + correctedSeqDesc.getImgLoader().getClass().getName()); + return; + } + + final ViewerFlatfieldCorrectionWrappedImgLoader correctedLoader = + (ViewerFlatfieldCorrectionWrappedImgLoader) correctedSeqDesc.getImgLoader(); + + System.out.println("Corrected loader type: " + correctedLoader.getClass().getSimpleName()); + System.out.println(" Correction active: " + correctedLoader.isActive()); + System.out.println(" Caching enabled: " + correctedLoader.isCached()); + System.out.println(" Wrapped loader: " + correctedLoader.getWrappedImgLoader().getClass().getSimpleName()); + System.out.println(" Implements ViewerImgLoader: " + (correctedLoader instanceof ViewerImgLoader ? "YES" : "NO")); + + // ========== STEP 2: Load UNCORRECTED dataset (original XML) ========== + System.out.println("\n=== STEP 2: Loading UNCORRECTED dataset ==="); + System.out.println("XML path: " + uncorrectedXmlPath); + + final SpimData2 uncorrectedData = new XmlIoSpimData2().load(uncorrectedXmlPath); + final SequenceDescription uncorrectedSeqDesc = uncorrectedData.getSequenceDescription(); + + // Verify the base loader is a ViewerImgLoader + if (!(uncorrectedSeqDesc.getImgLoader() instanceof ViewerImgLoader)) { + System.err.println("ERROR: Base loader is not a ViewerImgLoader!"); + System.err.println("Loader type: " + uncorrectedSeqDesc.getImgLoader().getClass().getName()); + return; + } + + final ViewerImgLoader uncorrectedLoader = (ViewerImgLoader) uncorrectedSeqDesc.getImgLoader(); + System.out.println("Uncorrected loader type: " + uncorrectedLoader.getClass().getSimpleName()); + + // ========== STEP 3: Create SplitViewerImgLoader wrapping the corrected loader ========== + System.out.println("\n=== STEP 3: Creating SplitViewerImgLoader ==="); + + // Get original image dimensions for the setup we're demonstrating + final ViewSetup vs = correctedSeqDesc.getViewSetups().get(setupToShow); + final long[] dims = new long[3]; + vs.getSize().dimensions(dims); + System.out.println("Original image size: " + dims[0] + " x " + dims[1] + " x " + dims[2]); + + // Create a simple 2x1 split in X dimension + final long splitX = dims[0] / 2; + + // Define the mappings for split regions + final HashMap new2oldSetupId = new HashMap<>(); + final HashMap newSetupId2Interval = new HashMap<>(); + + // Split region 0: left half + new2oldSetupId.put(100, setupToShow); + newSetupId2Interval.put(100, new FinalInterval( + new long[] {0, 0, 0}, + new long[] {splitX - 1, dims[1] - 1, dims[2] - 1} + )); + + // Split region 1: right half + new2oldSetupId.put(101, setupToShow); + newSetupId2Interval.put(101, new FinalInterval( + new long[] {splitX, 0, 0}, + new long[] {dims[0] - 1, dims[1] - 1, dims[2] - 1} + )); + + System.out.println("Created 2 split regions:"); + System.out.println(" Setup 100: X=[0, " + (splitX-1) + "] (left half)"); + System.out.println(" Setup 101: X=[" + splitX + ", " + (dims[0]-1) + "] (right half)"); + + // Create the split loader wrapping the CORRECTED ViewerImgLoader + final SplitViewerImgLoader splitLoader = new SplitViewerImgLoader( + correctedLoader, // <-- ViewerImgLoader compatible! + new2oldSetupId, + newSetupId2Interval, + correctedSeqDesc + ); + + System.out.println("Split loader implements ViewerImgLoader: " + + (splitLoader instanceof ViewerImgLoader ? "YES" : "NO")); + + // ========== STEP 4: Test ViewerImgLoader-specific features ========== + System.out.println("\n=== STEP 4: Testing ViewerImgLoader features ==="); + + // Test cache control delegation + System.out.println("Cache control available: " + (splitLoader.getCacheControl() != null)); + + // Test mipmap levels + final ViewerSetupImgLoader setupImgLoader = splitLoader.getSetupImgLoader(100); + System.out.println("Number of mipmap levels: " + setupImgLoader.numMipmapLevels()); + + final double[][] resolutions = setupImgLoader.getMipmapResolutions(); + System.out.println("Mipmap resolutions:"); + for (int level = 0; level < resolutions.length; level++) { + System.out.println(" Level " + level + ": " + + resolutions[level][0] + " x " + resolutions[level][1] + " x " + resolutions[level][2]); + } + + // ========== STEP 5: Display comparison images ========== + System.out.println("\n=== STEP 5: Displaying images ==="); + new ImageJ(); + + // Get tile ID from ViewSetup metadata + final int tileId = correctedData.getSequenceDescription() + .getViewSetups().get(setupToShow).getTile().getId(); + + // 5a. Show UNCORRECTED original at level 0 + System.out.println("Loading uncorrected image (level 0)..."); + @SuppressWarnings("unchecked") + final MultiResolutionSetupImgLoader uncorrectedSetupLoader = + (MultiResolutionSetupImgLoader) uncorrectedLoader.getSetupImgLoader(setupToShow); + final RandomAccessibleInterval uncorrected = + uncorrectedSetupLoader.getFloatImage(timepoint, 0, false); + ImageJFunctions.show(uncorrected, "1. Uncorrected - Setup " + setupToShow + " (tile " + tileId + ")"); + + // 5b. Show CORRECTED at level 0 + System.out.println("Loading corrected image (level 0)..."); + final RandomAccessibleInterval corrected = + correctedLoader.getSetupImgLoader(setupToShow).getFloatImage(timepoint, 0, false); + ImageJFunctions.show(corrected, "2. Corrected - Setup " + setupToShow + " (tile " + tileId + ")"); + + // 5c. Show CORRECTED + SPLIT (left half) at level 0 + System.out.println("Loading corrected + split (left half, level 0)..."); + final RandomAccessibleInterval splitLeft = + splitLoader.getSetupImgLoader(100).getFloatImage(timepoint, 0, false); + ImageJFunctions.show(splitLeft, "3. Corrected+Split LEFT - Setup 100"); + + // 5d. Show at different mipmap level if available + if (setupImgLoader.numMipmapLevels() > 1) { + System.out.println("Loading corrected + split (left half, level 1)..."); + final RandomAccessibleInterval splitLeftLevel1 = + splitLoader.getSetupImgLoader(100).getFloatImage(timepoint, 1, false); + ImageJFunctions.show(splitLeftLevel1, "4. Corrected+Split LEFT (Level 1) - Setup 100"); + } + + // ========== Summary ========== + System.out.println("\n=== VIEWERIMGLOADER CHAIN SUMMARY ==="); + System.out.println("Layer 1 (innermost): " + correctedLoader.getWrappedImgLoader().getClass().getSimpleName() + " [ViewerImgLoader]"); + System.out.println("Layer 2 (middle): " + correctedLoader.getClass().getSimpleName() + " [ViewerImgLoader]"); + System.out.println("Layer 3 (outermost): " + splitLoader.getClass().getSimpleName() + " [ViewerImgLoader]"); + System.out.println(); + System.out.println("Flatfield correction is now configured in the XML!"); + System.out.println("No manual setBrightImage()/setDarkImage() calls needed."); + System.out.println(); + System.out.println("All layers maintain ViewerImgLoader compatibility:"); + System.out.println(" - Cache control: delegated through chain"); + System.out.println(" - Volatile images: supported at all levels"); + System.out.println(" - Multi-resolution: " + setupImgLoader.numMipmapLevels() + " mipmap levels available"); + System.out.println(); + System.out.println("Compare the images to verify:"); + System.out.println(" - Image 1 vs 2: See flatfield correction effect"); + System.out.println(" - Image 2 vs 3: Verify split region matches corrected full image"); + if (setupImgLoader.numMipmapLevels() > 1) + System.out.println(" - Image 3 vs 4: Compare different mipmap levels"); + System.out.println(); + System.out.println("Tip: Use Image > Adjust > Brightness/Contrast (Ctrl+Shift+C)"); + } +} diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/ViewerFlatfieldCorrectionWrappedImgLoader.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/ViewerFlatfieldCorrectionWrappedImgLoader.java new file mode 100644 index 000000000..6d9709213 --- /dev/null +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/ViewerFlatfieldCorrectionWrappedImgLoader.java @@ -0,0 +1,431 @@ +/*- + * #%L + * Software for the reconstruction of multi-view microscopic acquisitions + * like Selective Plane Illumination Microscopy (SPIM) Data. + * %% + * Copyright (C) 2012 - 2025 Multiview Reconstruction developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import bdv.ViewerImgLoader; +import bdv.ViewerSetupImgLoader; +import bdv.cache.CacheControl; +import mpicbg.spim.data.generic.sequence.ImgLoaderHint; +import mpicbg.spim.data.generic.sequence.ImgLoaderHints; +import mpicbg.spim.data.sequence.MultiResolutionImgLoader; +import mpicbg.spim.data.sequence.MultiResolutionSetupImgLoader; +import mpicbg.spim.data.sequence.ViewId; +import mpicbg.spim.data.sequence.VoxelDimensions; +import net.imglib2.Dimensions; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.Volatile; +import net.imglib2.converter.RealTypeConverters; +import net.imglib2.img.Img; +import net.imglib2.img.ImgFactory; +import net.imglib2.img.array.ArrayImgFactory; +import net.imglib2.img.cell.CellImgFactory; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.RealType; +import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.util.Pair; +import net.imglib2.util.ValuePair; +import net.imglib2.view.Views; +import net.preibisch.mvrecon.process.fusion.FusionTools; + +/** + * Flatfield correction wrapper for ViewerImgLoader. + * + * This class wraps a ViewerImgLoader and applies flatfield (bright/dark image) correction + * on-the-fly. It implements both ViewerImgLoader and MultiResolutionImgLoader interfaces, + * making it compatible with BigDataViewer's caching and async loading infrastructure. + * + * The correction formula is: + * corrected = (source - dark) * mean(bright - dark) / (bright - dark) + * + * Usage in decorator chain: + * N5ImageLoader (ViewerImgLoader) + * -> ViewerFlatfieldCorrectionWrappedImgLoader (ViewerImgLoader) + * -> SplitViewerImgLoader (ViewerImgLoader) + */ +public class ViewerFlatfieldCorrectionWrappedImgLoader + implements ViewerImgLoader, MultiResolutionImgLoader { + + private final ViewerImgLoader wrappedImgLoader; + private boolean active; + private boolean cacheResult; + + /** Helper for loading flatfield images */ + private final FlatfieldImageLoader imageLoader; + + /** Downsampled bright/dark images for each mipmap level */ + private final Map>, RandomAccessibleInterval> dsRaiMap; + + public ViewerFlatfieldCorrectionWrappedImgLoader(final ViewerImgLoader wrappedImgLoader) { + this(wrappedImgLoader, true); + } + + public ViewerFlatfieldCorrectionWrappedImgLoader(final ViewerImgLoader wrappedImgLoader, final boolean cacheResult) { + this.wrappedImgLoader = wrappedImgLoader; + this.active = true; + this.cacheResult = cacheResult; + this.imageLoader = new FlatfieldImageLoader(); + this.dsRaiMap = new HashMap<>(); + } + + // ========== Configuration methods ========== + + public ViewerImgLoader getWrappedImgLoader() { + return wrappedImgLoader; + } + + public void setActive(final boolean active) { + this.active = active; + } + + public boolean isActive() { + return active; + } + + public boolean isCached() { + return cacheResult; + } + + public void setCached(final boolean cached) { + this.cacheResult = cached; + } + + public void setBrightImage(final ViewId vId, final FlatfieldImageInfo info) { + imageLoader.setBrightImage(vId, info); + } + + public void setDarkImage(final ViewId vId, final FlatfieldImageInfo info) { + imageLoader.setDarkImage(vId, info); + } + + /** + * Get the info map for bright/dark images per view. + * @return map from ViewId to (brightInfo, darkInfo) pair + */ + public Map> getInfoMap() { + return imageLoader.getInfoMap(); + } + + // ========== ViewerImgLoader interface ========== + + @Override + public ViewerFlatfieldCorrectionWrappedSetupImgLoader getSetupImgLoader(final int setupId) { + return new ViewerFlatfieldCorrectionWrappedSetupImgLoader<>(setupId); + } + + @Override + public CacheControl getCacheControl() { + return wrappedImgLoader.getCacheControl(); + } + + @Override + public void setNumFetcherThreads(final int n) { + wrappedImgLoader.setNumFetcherThreads(n); + } + + // ========== Image loading helpers ========== + + protected RandomAccessibleInterval getBrightImg(final ViewId vId) { + return imageLoader.getBrightImg(vId); + } + + protected RandomAccessibleInterval getDarkImg(final ViewId vId) { + return imageLoader.getDarkImg(vId); + } + + protected RandomAccessibleInterval getOrCreateBrightImgDownsampled( + final ViewId vId, + final int[] downsamplingFactors + ) { + return getOrCreateDownsampledImg(vId, downsamplingFactors, Pair::getA, this::getBrightImg); + } + + protected RandomAccessibleInterval getOrCreateDarkImgDownsampled( + final ViewId vId, + final int[] downsamplingFactors + ) { + return getOrCreateDownsampledImg(vId, downsamplingFactors, Pair::getB, this::getDarkImg); + } + + /** + * Generic method to get a downsampled image or do downsampling on the fly. The bright image + * is stored in the A element of the pair, the dark image in B. + */ + private RandomAccessibleInterval getOrCreateDownsampledImg( + ViewId vId, + int[] downsamplingFactors, + Function, FlatfieldImageInfo> infoSelector, + Function> imgGetter + ) { + // Convert to a list here to have a proper hash code for the map key + List dsFactorList = Arrays.stream(downsamplingFactors).boxed().collect(Collectors.toList()); + final ValuePair> key = new ValuePair<>(infoSelector.apply(imageLoader.getInfoMap().get(vId)), dsFactorList); + + if (!dsRaiMap.containsKey(key)) { + final RandomAccessibleInterval img = imgGetter.apply(vId); + + if (img == null) + return null; + + final RandomAccessibleInterval downsampled = + MultiResolutionFlatfieldCorrectionWrappedImgLoader.downsampleHDF5(img, downsamplingFactors); + dsRaiMap.put(key, downsampled); + } + + return dsRaiMap.get(key); + } + + // ========== Inner class: ViewerSetupImgLoader implementation ========== + + public class ViewerFlatfieldCorrectionWrappedSetupImgLoader & NativeType, V extends Volatile & RealType & NativeType> + implements ViewerSetupImgLoader, MultiResolutionSetupImgLoader { + private final int setupId; + + ViewerFlatfieldCorrectionWrappedSetupImgLoader(final int setupId) { + this.setupId = setupId; + } + + @SuppressWarnings("unchecked") + private ViewerSetupImgLoader getUnderlyingViewerSetupImgLoader() { + return (ViewerSetupImgLoader) wrappedImgLoader.getSetupImgLoader(setupId); + } + + @SuppressWarnings("unchecked") + private MultiResolutionSetupImgLoader getUnderlyingMultiResSetupImgLoader() { + // The wrapped ViewerImgLoader should also be a MultiResolutionImgLoader + return (MultiResolutionSetupImgLoader) ((MultiResolutionImgLoader) wrappedImgLoader).getSetupImgLoader(setupId); + } + + // ========== Regular image access ========== + + @Override + public RandomAccessibleInterval getImage(final int timepointId, final ImgLoaderHint... hints) { + return getImage(timepointId, 0, hints); + } + + @Override + public RandomAccessibleInterval getImage(final int timepointId, final int level, final ImgLoaderHint... hints) { + final ViewerSetupImgLoader viewerSetupIL = getUnderlyingViewerSetupImgLoader(); + final MultiResolutionSetupImgLoader multiResSetupIL = getUnderlyingMultiResSetupImgLoader(); + + if (!active) + return viewerSetupIL.getImage(timepointId, level, hints); + + final int n = multiResSetupIL.getImageSize(timepointId).numDimensions(); + + // Calculate downsampling factors for this mipmap level + final int[] dsFactors = new int[n]; + final double[] dsD = viewerSetupIL.getMipmapResolutions()[level]; + for (int d = 0; d < n; d++) + dsFactors[d] = (int) dsD[d]; + // Don't downsample z for 2D correction images + dsFactors[n - 1] = 1; + + RandomAccessibleInterval rai = FlatFieldCorrectedRandomAccessibleIntervals.create( + viewerSetupIL.getImage(timepointId, level, hints), + getOrCreateBrightImgDownsampled(new ViewId(timepointId, setupId), dsFactors), + getOrCreateDarkImgDownsampled(new ViewId(timepointId, setupId), dsFactors)); + + // Handle LOAD_COMPLETELY hint + boolean loadCompletelyRequested = false; + for (final ImgLoaderHint hint : hints) + if (hint == ImgLoaderHints.LOAD_COMPLETELY) { + loadCompletelyRequested = true; + break; + } + + if (loadCompletelyRequested) { + long numPx = 1; + for (int d = 0; d < rai.numDimensions(); d++) + numPx *= rai.dimension(d); + + final ImgFactory imgFactory; + if (Math.log(numPx) / Math.log(2) < 31) { + imgFactory = new ArrayImgFactory<>(getImageType()); + } else { + imgFactory = new CellImgFactory<>(getImageType()); + } + + final Img loadedImg = imgFactory.create(rai); + RealTypeConverters.copyFromTo(Views.extendZero(rai), loadedImg); + + rai = loadedImg; + } else if (cacheResult) { + final int[] cellSize = new int[rai.numDimensions()]; + Arrays.fill(cellSize, 1); + for (int d = 0; d < rai.numDimensions() - 1; d++) + cellSize[d] = (int) rai.dimension(d); + rai = FusionTools.cacheRandomAccessibleInterval( + rai, Long.MAX_VALUE, rai.firstElement().createVariable(), cellSize); + } + + return rai; + } + + // ========== Volatile image access ========== + + @Override + public RandomAccessibleInterval getVolatileImage(final int timepointId, final int level, final ImgLoaderHint... hints) { + final ViewerSetupImgLoader viewerSetupIL = getUnderlyingViewerSetupImgLoader(); + final MultiResolutionSetupImgLoader multiResSetupIL = getUnderlyingMultiResSetupImgLoader(); + + if (!active) + return viewerSetupIL.getVolatileImage(timepointId, level, hints); + + final int n = multiResSetupIL.getImageSize(timepointId).numDimensions(); + + // Calculate downsampling factors for this mipmap level + final int[] dsFactors = new int[n]; + final double[] dsD = viewerSetupIL.getMipmapResolutions()[level]; + for (int d = 0; d < n; d++) + dsFactors[d] = (int) dsD[d]; + dsFactors[n - 1] = 1; + + // Apply correction to volatile image + // Note: The volatile validity flag propagation may not be perfect, + // but BDV will re-request invalid pixels automatically + return FlatFieldCorrectedRandomAccessibleIntervals.create( + viewerSetupIL.getVolatileImage(timepointId, level, hints), + getOrCreateBrightImgDownsampled(new ViewId(timepointId, setupId), dsFactors), + getOrCreateDarkImgDownsampled(new ViewId(timepointId, setupId), dsFactors), + getVolatileImageType()); + } + + // ========== Float image access ========== + + @Override + public RandomAccessibleInterval getFloatImage(final int timepointId, final boolean normalize, final ImgLoaderHint... hints) { + return getFloatImage(timepointId, 0, normalize, hints); + } + + @Override + public RandomAccessibleInterval getFloatImage(final int timepointId, final int level, final boolean normalize, final ImgLoaderHint... hints) { + final ViewerSetupImgLoader viewerSetupIL = getUnderlyingViewerSetupImgLoader(); + final MultiResolutionSetupImgLoader multiResSetupIL = getUnderlyingMultiResSetupImgLoader(); + + if (!active) + return multiResSetupIL.getFloatImage(timepointId, level, normalize, hints); + + final int n = multiResSetupIL.getImageSize(timepointId).numDimensions(); + + final int[] dsFactors = new int[n]; + final double[] dsD = viewerSetupIL.getMipmapResolutions()[level]; + for (int d = 0; d < n; d++) + dsFactors[d] = (int) dsD[d]; + dsFactors[n - 1] = 1; + + RandomAccessibleInterval rai = FlatFieldCorrectedRandomAccessibleIntervals.create( + viewerSetupIL.getImage(timepointId, level, hints), + getOrCreateBrightImgDownsampled(new ViewId(timepointId, setupId), dsFactors), + getOrCreateDarkImgDownsampled(new ViewId(timepointId, setupId), dsFactors), + new FloatType()); + + if (normalize) { + rai = new VirtuallyNormalizedRandomAccessibleInterval<>(rai); + } + + // Handle caching/loading + boolean loadCompletelyRequested = false; + for (final ImgLoaderHint hint : hints) + if (hint == ImgLoaderHints.LOAD_COMPLETELY) { + loadCompletelyRequested = true; + break; + } + + if (loadCompletelyRequested) { + long numPx = 1; + for (int d = 0; d < rai.numDimensions(); d++) + numPx *= rai.dimension(d); + + final ImgFactory imgFactory; + if (Math.log(numPx) / Math.log(2) < 31) + imgFactory = new ArrayImgFactory<>(new FloatType()); + else + imgFactory = new CellImgFactory<>(new FloatType()); + + final Img loadedImg = imgFactory.create(rai); + RealTypeConverters.copyFromTo(Views.extendZero(rai), loadedImg); + + rai = loadedImg; + } else if (cacheResult) { + final int[] cellSize = new int[rai.numDimensions()]; + Arrays.fill(cellSize, 1); + for (int d = 0; d < rai.numDimensions() - 1; d++) + cellSize[d] = (int) rai.dimension(d); + rai = FusionTools.cacheRandomAccessibleInterval(rai, Long.MAX_VALUE, + new FloatType(), cellSize); + } + + return rai; + } + + // ========== Metadata delegation ========== + + @Override + public T getImageType() { + return getUnderlyingViewerSetupImgLoader().getImageType(); + } + + @Override + public V getVolatileImageType() { + return getUnderlyingViewerSetupImgLoader().getVolatileImageType(); + } + + @Override + public double[][] getMipmapResolutions() { + return getUnderlyingViewerSetupImgLoader().getMipmapResolutions(); + } + + @Override + public AffineTransform3D[] getMipmapTransforms() { + return getUnderlyingViewerSetupImgLoader().getMipmapTransforms(); + } + + @Override + public int numMipmapLevels() { + return getUnderlyingViewerSetupImgLoader().numMipmapLevels(); + } + + @Override + public Dimensions getImageSize(final int timepointId) { + return getUnderlyingMultiResSetupImgLoader().getImageSize(timepointId); + } + + @Override + public Dimensions getImageSize(final int timepointId, final int level) { + return getUnderlyingMultiResSetupImgLoader().getImageSize(timepointId, level); + } + + @Override + public VoxelDimensions getVoxelSize(final int timepointId) { + return getUnderlyingMultiResSetupImgLoader().getVoxelSize(timepointId); + } + } +} diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/XmlIoFlatfieldCorrectedWrappedImgLoader.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/XmlIoFlatfieldCorrectedWrappedImgLoader.java index 70e59c01f..6d7b2edd4 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/XmlIoFlatfieldCorrectedWrappedImgLoader.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/XmlIoFlatfieldCorrectedWrappedImgLoader.java @@ -9,12 +9,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 2 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -26,15 +26,19 @@ import static mpicbg.spim.data.XmlKeys.IMGLOADER_TAG; import static mpicbg.spim.data.XmlKeys.TIMEPOINTS_TIMEPOINT_TAG; import static mpicbg.spim.data.XmlKeys.VIEWSETUP_TAG; +import static net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield.XmlIoViewerFlatfieldCorrectionWrappedImgLoader.FORMAT_ATTR; +import static net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield.XmlIoViewerFlatfieldCorrectionWrappedImgLoader.DATASET_ATTR; +import static net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield.XmlIoViewerFlatfieldCorrectionWrappedImgLoader.parseFlatfieldImageInfo; +import static net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield.XmlIoViewerFlatfieldCorrectionWrappedImgLoader.createFlatfieldImageElement; import java.io.File; +import java.net.URI; import java.util.Map; import org.jdom2.DataConversionException; import org.jdom2.Element; import mpicbg.spim.data.SpimDataInstantiationException; -import mpicbg.spim.data.XmlHelpers; import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription; import mpicbg.spim.data.generic.sequence.BasicImgLoader; import mpicbg.spim.data.generic.sequence.ImgLoaderIo; @@ -44,12 +48,11 @@ import mpicbg.spim.data.sequence.MultiResolutionImgLoader; import mpicbg.spim.data.sequence.ViewId; import net.imglib2.util.Pair; -import net.preibisch.legacy.io.IOFunctions; @ImgLoaderIo(format = "spimreconstruction.wrapped.flatfield.default", type = DefaultFlatfieldCorrectionWrappedImgLoader.class) public class XmlIoFlatfieldCorrectedWrappedImgLoader - implements XmlIoBasicImgLoader< FlatfieldCorrectionWrappedImgLoader< ? extends ImgLoader > > + implements XmlIoBasicImgLoader> { public final static String WRAPPED_IMGLOADER_TAG = "WrappedImgLoader"; public final static String FLATFIELDS_TAG = "FlatFields"; @@ -60,17 +63,24 @@ public class XmlIoFlatfieldCorrectedWrappedImgLoader public final static String CACHED_TAG = "Cached"; @Override - public FlatfieldCorrectionWrappedImgLoader< ? extends ImgLoader > fromXml(Element elem, File basePath, - AbstractSequenceDescription< ?, ?, ? > sequenceDescription) + public FlatfieldCorrectionWrappedImgLoader fromXml(Element elem, File basePath, + AbstractSequenceDescription sequenceDescription) { - Element wrappedImgLoaderEl = elem.getChild( WRAPPED_IMGLOADER_TAG ).getChild( IMGLOADER_TAG ); - XmlIoBasicImgLoader< ? > xmlIoWrapped = null; + return fromXml(elem, basePath == null ? null : basePath.toURI(), sequenceDescription); + } + + @Override + public FlatfieldCorrectionWrappedImgLoader fromXml(Element elem, URI basePathURI, + AbstractSequenceDescription sequenceDescription) + { + Element wrappedImgLoaderEl = elem.getChild(WRAPPED_IMGLOADER_TAG).getChild(IMGLOADER_TAG); + XmlIoBasicImgLoader xmlIoWrapped = null; try { xmlIoWrapped = ImgLoaders - .createXmlIoForFormat( wrappedImgLoaderEl.getAttributeValue( IMGLOADER_FORMAT_ATTRIBUTE_NAME ) ); + .createXmlIoForFormat(wrappedImgLoaderEl.getAttributeValue(IMGLOADER_FORMAT_ATTRIBUTE_NAME)); } - catch ( SpimDataInstantiationException e ) + catch (SpimDataInstantiationException e) { e.printStackTrace(); return null; @@ -80,92 +90,102 @@ public class XmlIoFlatfieldCorrectedWrappedImgLoader boolean active = false; try { - cached = elem.getAttribute( CACHED_TAG ).getBooleanValue(); - active = elem.getAttribute( ACTIVE_TAG ).getBooleanValue(); + cached = elem.getAttribute(CACHED_TAG).getBooleanValue(); + active = elem.getAttribute(ACTIVE_TAG).getBooleanValue(); } - catch ( DataConversionException e ) + catch (DataConversionException e) { e.printStackTrace(); } - BasicImgLoader wrappedImgLoader = xmlIoWrapped.fromXml( wrappedImgLoaderEl, basePath, sequenceDescription ); + BasicImgLoader wrappedImgLoader = xmlIoWrapped.fromXml(wrappedImgLoaderEl, basePathURI, sequenceDescription); - FlatfieldCorrectionWrappedImgLoader< ? extends ImgLoader > res = null; + FlatfieldCorrectionWrappedImgLoader res = null; - if ( MultiResolutionImgLoader.class.isInstance( wrappedImgLoader ) ) - res = new MultiResolutionFlatfieldCorrectionWrappedImgLoader( (MultiResolutionImgLoader) wrappedImgLoader, - cached ); - else if ( ImgLoader.class.isInstance( wrappedImgLoader ) ) - res = new DefaultFlatfieldCorrectionWrappedImgLoader( (ImgLoader) wrappedImgLoader, cached ); + if (MultiResolutionImgLoader.class.isInstance(wrappedImgLoader)) + res = new MultiResolutionFlatfieldCorrectionWrappedImgLoader((MultiResolutionImgLoader) wrappedImgLoader, + cached); + else if (ImgLoader.class.isInstance(wrappedImgLoader)) + res = new DefaultFlatfieldCorrectionWrappedImgLoader((ImgLoader) wrappedImgLoader, cached); else return null; - Element flatfields = elem.getChild( FLATFIELDS_TAG ); - for ( Element flatfield : flatfields.getChildren() ) + Element flatfields = elem.getChild(FLATFIELDS_TAG); + for (Element flatfield : flatfields.getChildren()) { - int tp = Integer.parseInt( flatfield.getAttributeValue( TIMEPOINTS_TIMEPOINT_TAG ) ); - int vs = Integer.parseInt( flatfield.getAttributeValue( VIEWSETUP_TAG ) ); - File brightImg = XmlHelpers.loadPath( flatfield, BRIGHTIMG_TAG, basePath ); - File darkImg = XmlHelpers.loadPath( flatfield, DARKIMG_TAG, basePath ); - res.setBrightImage( new ViewId( tp, vs ), brightImg ); - res.setDarkImage( new ViewId( tp, vs ), darkImg ); + int tp = Integer.parseInt(flatfield.getAttributeValue(TIMEPOINTS_TIMEPOINT_TAG)); + int vs = Integer.parseInt(flatfield.getAttributeValue(VIEWSETUP_TAG)); + ViewId viewId = new ViewId(tp, vs); + + FlatfieldImageInfo brightInfo = parseFlatfieldImageInfo(flatfield, BRIGHTIMG_TAG, basePathURI); + FlatfieldImageInfo darkInfo = parseFlatfieldImageInfo(flatfield, DARKIMG_TAG, basePathURI); + + if (brightInfo != null) + res.setBrightImage(viewId, brightInfo); + if (darkInfo != null) + res.setDarkImage(viewId, darkInfo); } - res.setActive( active ); + res.setActive(active); return res; } @Override - public Element toXml(FlatfieldCorrectionWrappedImgLoader< ? extends ImgLoader > imgLoader, File basePath) + public Element toXml(FlatfieldCorrectionWrappedImgLoader imgLoader, File basePath) { + return toXml(imgLoader, basePath == null ? null : basePath.toURI()); + } - final Map< ViewId, Pair< File, File > > fileMap = ( (LazyLoadingFlatFieldCorrectionMap< ? extends ImgLoader >) imgLoader ).fileMap; + @Override + public Element toXml(FlatfieldCorrectionWrappedImgLoader imgLoader, URI basePathURI) + { + final Map> infoMap = + ((LazyLoadingFlatFieldCorrectionMap) imgLoader).getInfoMap(); - final Element wholeElem = new Element( IMGLOADER_TAG ); - wholeElem.setAttribute( IMGLOADER_FORMAT_ATTRIBUTE_NAME, - this.getClass().getAnnotation( ImgLoaderIo.class ).format() ); - final Element wrappedIL = new Element( WRAPPED_IMGLOADER_TAG ); + final Element wholeElem = new Element(IMGLOADER_TAG); + wholeElem.setAttribute(IMGLOADER_FORMAT_ATTRIBUTE_NAME, + this.getClass().getAnnotation(ImgLoaderIo.class).format()); + final Element wrappedIL = new Element(WRAPPED_IMGLOADER_TAG); - wholeElem.setAttribute( ACTIVE_TAG, Boolean.toString( imgLoader.isActive() ) ); - wholeElem.setAttribute( CACHED_TAG, Boolean.toString( imgLoader.isCached() ) ); + wholeElem.setAttribute(ACTIVE_TAG, Boolean.toString(imgLoader.isActive())); + wholeElem.setAttribute(CACHED_TAG, Boolean.toString(imgLoader.isCached())); try { - XmlIoBasicImgLoader< ImgLoader > loaderIO = (XmlIoBasicImgLoader< ImgLoader >) ImgLoaders - .createXmlIoForImgLoaderClass( imgLoader.getWrappedImgLoder().getClass() ); - Element wrappedInner = loaderIO.toXml( (ImgLoader) imgLoader.getWrappedImgLoder(), basePath ); - wrappedIL.addContent( wrappedInner ); - + @SuppressWarnings("unchecked") + XmlIoBasicImgLoader loaderIO = (XmlIoBasicImgLoader) ImgLoaders + .createXmlIoForImgLoaderClass(imgLoader.getWrappedImgLoder().getClass()); + Element wrappedInner = loaderIO.toXml((ImgLoader) imgLoader.getWrappedImgLoder(), basePathURI); + wrappedIL.addContent(wrappedInner); } - catch ( SpimDataInstantiationException e ) + catch (SpimDataInstantiationException e) { e.printStackTrace(); return null; } - final Element elFlatfields = new Element( FLATFIELDS_TAG ); + final Element elFlatfields = new Element(FLATFIELDS_TAG); - for ( ViewId vid : fileMap.keySet() ) + for (ViewId vid : infoMap.keySet()) { - final Pair< File, File > files = fileMap.get( vid ); - if ( files == null || ( files.getA() == null && files.getB() == null ) ) + final Pair infos = infoMap.get(vid); + if (infos == null || (infos.getA() == null && infos.getB() == null)) continue; - final Element elFlatfield = new Element( FLATFIELD_TAG ); - elFlatfield.setAttribute( TIMEPOINTS_TIMEPOINT_TAG, Integer.toString( vid.getTimePointId() ) ); - elFlatfield.setAttribute( VIEWSETUP_TAG, Integer.toString( vid.getViewSetupId() ) ); + final Element elFlatfield = new Element(FLATFIELD_TAG); + elFlatfield.setAttribute(TIMEPOINTS_TIMEPOINT_TAG, Integer.toString(vid.getTimePointId())); + elFlatfield.setAttribute(VIEWSETUP_TAG, Integer.toString(vid.getViewSetupId())); - if ( files.getA() != null ) - elFlatfield.addContent( XmlHelpers.pathElement( BRIGHTIMG_TAG, files.getA(), basePath ) ); - if ( files.getB() != null ) - elFlatfield.addContent( XmlHelpers.pathElement( DARKIMG_TAG, files.getB(), basePath ) ); + if (infos.getA() != null) + elFlatfield.addContent(createFlatfieldImageElement(BRIGHTIMG_TAG, infos.getA(), basePathURI)); + if (infos.getB() != null) + elFlatfield.addContent(createFlatfieldImageElement(DARKIMG_TAG, infos.getB(), basePathURI)); - elFlatfields.addContent( elFlatfield ); + elFlatfields.addContent(elFlatfield); } - wholeElem.addContent( wrappedIL ); - wholeElem.addContent( elFlatfields ); + wholeElem.addContent(wrappedIL); + wholeElem.addContent(elFlatfields); return wholeElem; } - } diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/XmlIoViewerFlatfieldCorrectionWrappedImgLoader.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/XmlIoViewerFlatfieldCorrectionWrappedImgLoader.java new file mode 100644 index 000000000..fb6e56b4b --- /dev/null +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/XmlIoViewerFlatfieldCorrectionWrappedImgLoader.java @@ -0,0 +1,219 @@ +/*- + * #%L + * Software for the reconstruction of multi-view microscopic acquisitions + * like Selective Plane Illumination Microscopy (SPIM) Data. + * %% + * Copyright (C) 2012 - 2025 Multiview Reconstruction developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield; + +import static mpicbg.spim.data.XmlKeys.IMGLOADER_FORMAT_ATTRIBUTE_NAME; +import static mpicbg.spim.data.XmlKeys.IMGLOADER_TAG; +import static mpicbg.spim.data.XmlKeys.TIMEPOINTS_TIMEPOINT_TAG; +import static mpicbg.spim.data.XmlKeys.VIEWSETUP_TAG; + +import java.io.File; +import java.net.URI; +import java.util.Map; + +import org.janelia.saalfeldlab.n5.universe.StorageFormat; +import org.jdom2.DataConversionException; +import org.jdom2.Element; + +import bdv.ViewerImgLoader; +import mpicbg.spim.data.SpimDataInstantiationException; +import mpicbg.spim.data.XmlHelpers; +import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription; +import mpicbg.spim.data.generic.sequence.BasicImgLoader; +import mpicbg.spim.data.generic.sequence.ImgLoaderIo; +import mpicbg.spim.data.generic.sequence.ImgLoaders; +import mpicbg.spim.data.generic.sequence.XmlIoBasicImgLoader; +import mpicbg.spim.data.sequence.ViewId; +import net.imglib2.util.Pair; + +/** + * XML I/O handler for ViewerFlatfieldCorrectionWrappedImgLoader. + * + * Registers format "spimreconstruction.wrapped.flatfield.viewer" for + * ViewerImgLoader-based flatfield correction wrappers. + */ +@ImgLoaderIo(format = "spimreconstruction.wrapped.flatfield.viewer", type = ViewerFlatfieldCorrectionWrappedImgLoader.class) +public class XmlIoViewerFlatfieldCorrectionWrappedImgLoader + implements XmlIoBasicImgLoader { + public final static String WRAPPED_IMGLOADER_TAG = "WrappedImgLoader"; + public final static String FLATFIELDS_TAG = "FlatFields"; + public final static String FLATFIELD_TAG = "FlatField"; + public final static String BRIGHTIMG_TAG = "BrightImg"; + public final static String DARKIMG_TAG = "DarkImg"; + public final static String ACTIVE_TAG = "Active"; + public final static String CACHED_TAG = "Cached"; + public final static String FORMAT_ATTR = "format"; + public final static String DATASET_ATTR = "dataset"; + + @Override + public ViewerFlatfieldCorrectionWrappedImgLoader fromXml(Element elem, File basePath, + AbstractSequenceDescription sequenceDescription) { + return fromXml(elem, basePath == null ? null : basePath.toURI(), sequenceDescription); + } + + @Override + public ViewerFlatfieldCorrectionWrappedImgLoader fromXml(Element elem, URI basePathURI, + AbstractSequenceDescription sequenceDescription) { + Element wrappedImgLoaderEl = elem.getChild(WRAPPED_IMGLOADER_TAG).getChild(IMGLOADER_TAG); + XmlIoBasicImgLoader xmlIoWrapped; + try { + xmlIoWrapped = ImgLoaders + .createXmlIoForFormat(wrappedImgLoaderEl.getAttributeValue(IMGLOADER_FORMAT_ATTRIBUTE_NAME)); + } catch (SpimDataInstantiationException e) { + e.printStackTrace(); + return null; + } + + boolean cached = false; + boolean active = false; + try { + cached = elem.getAttribute(CACHED_TAG).getBooleanValue(); + active = elem.getAttribute(ACTIVE_TAG).getBooleanValue(); + } catch (DataConversionException e) { + e.printStackTrace(); + } + + BasicImgLoader wrappedImgLoader = xmlIoWrapped.fromXml(wrappedImgLoaderEl, basePathURI, sequenceDescription); + + // Verify wrapped loader is a ViewerImgLoader + if (!(wrappedImgLoader instanceof ViewerImgLoader)) { + System.err.println("ViewerFlatfieldCorrectionWrappedImgLoader requires a ViewerImgLoader, but got: " + + wrappedImgLoader.getClass().getName()); + return null; + } + + ViewerFlatfieldCorrectionWrappedImgLoader res = + new ViewerFlatfieldCorrectionWrappedImgLoader((ViewerImgLoader) wrappedImgLoader, cached); + + Element flatfields = elem.getChild(FLATFIELDS_TAG); + for (Element flatfield : flatfields.getChildren()) { + int tp = Integer.parseInt(flatfield.getAttributeValue(TIMEPOINTS_TIMEPOINT_TAG)); + int vs = Integer.parseInt(flatfield.getAttributeValue(VIEWSETUP_TAG)); + ViewId viewId = new ViewId(tp, vs); + + FlatfieldImageInfo brightInfo = parseFlatfieldImageInfo(flatfield, BRIGHTIMG_TAG, basePathURI); + FlatfieldImageInfo darkInfo = parseFlatfieldImageInfo(flatfield, DARKIMG_TAG, basePathURI); + + if (brightInfo != null) + res.setBrightImage(viewId, brightInfo); + if (darkInfo != null) + res.setDarkImage(viewId, darkInfo); + } + + res.setActive(active); + return res; + } + + @Override + public Element toXml(ViewerFlatfieldCorrectionWrappedImgLoader imgLoader, File basePath) { + return toXml(imgLoader, basePath == null ? null : basePath.toURI()); + } + + @Override + public Element toXml(ViewerFlatfieldCorrectionWrappedImgLoader imgLoader, URI basePathURI) { + final Map> infoMap = imgLoader.getInfoMap(); + + final Element wholeElem = new Element(IMGLOADER_TAG); + wholeElem.setAttribute(IMGLOADER_FORMAT_ATTRIBUTE_NAME, + this.getClass().getAnnotation(ImgLoaderIo.class).format()); + final Element wrappedIL = new Element(WRAPPED_IMGLOADER_TAG); + + wholeElem.setAttribute(ACTIVE_TAG, Boolean.toString(imgLoader.isActive())); + wholeElem.setAttribute(CACHED_TAG, Boolean.toString(imgLoader.isCached())); + + try { + @SuppressWarnings({"rawtypes"}) + XmlIoBasicImgLoader loaderIO = ImgLoaders + .createXmlIoForImgLoaderClass(imgLoader.getWrappedImgLoader().getClass()); + @SuppressWarnings("unchecked") + Element wrappedInner = loaderIO.toXml(imgLoader.getWrappedImgLoader(), basePathURI); + wrappedIL.addContent(wrappedInner); + } catch (SpimDataInstantiationException e) { + e.printStackTrace(); + return null; + } + + final Element elFlatfields = new Element(FLATFIELDS_TAG); + + for (ViewId vid : infoMap.keySet()) { + final Pair infos = infoMap.get(vid); + if (infos == null || (infos.getA() == null && infos.getB() == null)) + continue; + + final Element elFlatfield = new Element(FLATFIELD_TAG); + elFlatfield.setAttribute(TIMEPOINTS_TIMEPOINT_TAG, Integer.toString(vid.getTimePointId())); + elFlatfield.setAttribute(VIEWSETUP_TAG, Integer.toString(vid.getViewSetupId())); + + if (infos.getA() != null) + elFlatfield.addContent(createFlatfieldImageElement(BRIGHTIMG_TAG, infos.getA(), basePathURI)); + if (infos.getB() != null) + elFlatfield.addContent(createFlatfieldImageElement(DARKIMG_TAG, infos.getB(), basePathURI)); + + elFlatfields.addContent(elFlatfield); + } + + wholeElem.addContent(wrappedIL); + wholeElem.addContent(elFlatfields); + return wholeElem; + } + + /** + * Parse a flatfield image element (BrightImg or DarkImg) into a FlatfieldImageInfo. + * + * @param parent parent element containing the image element + * @param tag the tag name (BRIGHTIMG_TAG or DARKIMG_TAG) + * @param basePathURI base path for resolving relative URIs + * @return FlatfieldImageInfo, or null if element doesn't exist + */ + protected static FlatfieldImageInfo parseFlatfieldImageInfo(Element parent, String tag, URI basePathURI) { + Element imgElement = parent.getChild(tag); + if (imgElement == null) + return null; + + URI uri = XmlHelpers.loadPathURI(parent, tag, basePathURI); + if (uri == null) + return null; + + String formatStr = imgElement.getAttributeValue(FORMAT_ATTR); + String dataset = imgElement.getAttributeValue(DATASET_ATTR); + + StorageFormat format = FlatfieldImageLoader.parseFormat(formatStr); + return new FlatfieldImageInfo(uri, format, dataset); + } + + /** + * Create an XML element for a flatfield image with format and dataset attributes. + * + * @param tag the tag name (BRIGHTIMG_TAG or DARKIMG_TAG) + * @param info the flatfield image info + * @param basePathURI base path for creating relative URIs + * @return the XML element + */ + protected static Element createFlatfieldImageElement(String tag, FlatfieldImageInfo info, URI basePathURI) { + Element el = XmlHelpers.pathElementURI(tag, info.getUri(), basePathURI); + el.setAttribute(FORMAT_ATTR, FlatfieldImageLoader.formatToString(info.getFormat())); + if (info.getDataset() != null && !info.getDataset().isEmpty()) + el.setAttribute(DATASET_ATTR, info.getDataset()); + return el; + } +} diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/splitting/SplitMultiResolutionSetupImgLoader.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/splitting/SplitMultiResolutionSetupImgLoader.java index 0d925d273..7a7724e71 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/splitting/SplitMultiResolutionSetupImgLoader.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/splitting/SplitMultiResolutionSetupImgLoader.java @@ -170,7 +170,11 @@ public int numMipmapLevels() public RandomAccessibleInterval< FloatType > getFloatImage( int timepointId, int level, boolean normalize, ImgLoaderHint... hints ) { - throw new RuntimeException( "not supported." ); + final RandomAccessibleInterval< FloatType > full = underlyingSetupImgLoader.getFloatImage( timepointId, level, normalize, hints ); + + updateScaledIntervals( this.scaledIntervals, level, n, full ); + + return Views.zeroMin( Views.interval( full, scaledIntervals[ level ] ) ); } @Override diff --git a/src/test/java/net/preibisch/mvrecon/tests/BenchmarkFlatfieldFusion.java b/src/test/java/net/preibisch/mvrecon/tests/BenchmarkFlatfieldFusion.java new file mode 100644 index 000000000..1627007fe --- /dev/null +++ b/src/test/java/net/preibisch/mvrecon/tests/BenchmarkFlatfieldFusion.java @@ -0,0 +1,291 @@ +/*- + * #%L + * Software for the reconstruction of multi-view microscopic acquisitions + * like Selective Plane Illumination Microscopy (SPIM) Data. + * %% + * Copyright (C) 2012 - 2025 Multiview Reconstruction developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package net.preibisch.mvrecon.tests; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.function.Consumer; + +import mpicbg.spim.data.SpimDataException; +import mpicbg.spim.data.sequence.ViewId; +import net.imglib2.FinalInterval; +import net.imglib2.Interval; +import net.imglib2.algorithm.blocks.BlockSupplier; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.preibisch.mvrecon.fiji.plugin.fusion.FusionGUI.FusionType; +import net.preibisch.mvrecon.fiji.spimdata.SpimData2; +import net.preibisch.mvrecon.fiji.spimdata.XmlIoSpimData2; +import net.preibisch.mvrecon.fiji.spimdata.boundingbox.BoundingBox; +import net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield.FlatfieldCorrectionWrappedImgLoader; +import net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield.ViewerFlatfieldCorrectionWrappedImgLoader; +import net.preibisch.mvrecon.process.boundingbox.BoundingBoxMaximal; +import net.preibisch.mvrecon.process.fusion.FusionTools; +import net.preibisch.mvrecon.process.fusion.blk.BlkAffineFusion; +import net.preibisch.mvrecon.process.fusion.transformed.TransformVirtual; +import util.BlockSupplierUtils; + +/** + * Benchmark to measure the performance overhead of lazy flatfield correction during multi-view fusion. + * + * This benchmark: + * 1. Loads a dataset with flatfield correction configured (data/dataset_corrected_viewer.xml) + * 2. Toggles flatfield correction on/off using setActive() + * 3. Runs fusion with and without correction + * 4. Measures and reports timing comparison + * + * Run with: mvn compile exec:java -Dexec.mainClass="net.preibisch.mvrecon.tests.BenchmarkFlatfieldFusion" + * Or run from IDE as a Java application. + */ +public class BenchmarkFlatfieldFusion { + // Benchmark configuration + private static final double DOWNSAMPLING = 4.0; + private static final double ANISOTROPY_FACTOR = 3.0; // matches dataset's 1x1x3 voxel size + private static final int WARMUP_ITERATIONS = 10; + private static final int BENCHMARK_ITERATIONS = 50; + private static final FusionType FUSION_TYPE = FusionType.AVG_BLEND; + private static final int[] BLOCK_SIZE = new int[] {64, 64, 64}; + + public static void main(String[] args) { + // Path to dataset with flatfield correction + final File xmlFile = new File("data/dataset_corrected_viewer.xml"); + + if (!xmlFile.exists()) { + System.err.println("Dataset not found: " + xmlFile.getAbsolutePath()); + System.err.println("Please ensure data/dataset_corrected_viewer.xml exists."); + System.exit(1); + } + + try { + runBenchmark(xmlFile); + } catch (Exception e) { + System.err.println("Benchmark failed: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } + + private static void runBenchmark(final File xmlFile) throws SpimDataException { + System.out.println("============================================================"); + System.out.println("Flatfield Correction Benchmark During Fusion"); + System.out.println("============================================================"); + System.out.println(); + + // Load the dataset + System.out.println("Loading dataset: " + xmlFile.getAbsolutePath()); + final SpimData2 spimData = new XmlIoSpimData2().load(xmlFile.toURI()); + + // Get the flatfield-wrapped image loader + // Support both FlatfieldCorrectionWrappedImgLoader (interface) and ViewerFlatfieldCorrectionWrappedImgLoader (class) + final FlatfieldCorrectionWrappedImgLoader ffLoader; + final ViewerFlatfieldCorrectionWrappedImgLoader viewerFfLoader; + + if (spimData.getSequenceDescription().getImgLoader() instanceof FlatfieldCorrectionWrappedImgLoader) { + ffLoader = (FlatfieldCorrectionWrappedImgLoader) spimData.getSequenceDescription().getImgLoader(); + viewerFfLoader = null; + } else if (spimData.getSequenceDescription().getImgLoader() instanceof ViewerFlatfieldCorrectionWrappedImgLoader) { + ffLoader = null; + viewerFfLoader = (ViewerFlatfieldCorrectionWrappedImgLoader) spimData.getSequenceDescription().getImgLoader(); + } else { + System.err.println("Dataset does not have a flatfield correction image loader."); + System.err.println("ImgLoader type: " + spimData.getSequenceDescription().getImgLoader().getClass().getName()); + return; + } + + // Create a lambda to toggle flatfield correction for either loader type + final Consumer setActive = (active) -> { + if (ffLoader != null) + ffLoader.setActive(active); + else + viewerFfLoader.setActive(active); + }; + + // Get all views + final List viewIds = new ArrayList<>(); + viewIds.addAll(spimData.getSequenceDescription().getViewDescriptions().values()); + SpimData2.filterMissingViews(spimData, viewIds); + + System.out.println(); + System.out.println("Dataset Information:"); + System.out.println(" Views: " + viewIds.size()); + System.out.println(" Downsampling: " + DOWNSAMPLING + ", Anisotropy: " + ANISOTROPY_FACTOR); + System.out.println(" Fusion type: " + FUSION_TYPE); + System.out.println(" Block size: " + BLOCK_SIZE[0] + "x" + BLOCK_SIZE[1] + "x" + BLOCK_SIZE[2]); + System.out.println(" Warmup iterations: " + WARMUP_ITERATIONS + ", Benchmark iterations: " + BENCHMARK_ITERATIONS); + + // Compute bounding box + final BoundingBox bb = new BoundingBoxMaximal(viewIds, spimData).estimate("benchmark"); + System.out.println(" Bounding box: [" + bb.getMin()[0] + ", " + bb.getMin()[1] + ", " + bb.getMin()[2] + + "] to [" + bb.getMax()[0] + ", " + bb.getMax()[1] + ", " + bb.getMax()[2] + "]"); + + // Apply anisotropy and downsampling to bounding box + Interval boundingBoxFusion = FusionTools.createAnisotropicBoundingBox(bb, ANISOTROPY_FACTOR).getA(); + boundingBoxFusion = FusionTools.createDownsampledBoundingBox(boundingBoxFusion, DOWNSAMPLING).getA(); + + final long[] dims = boundingBoxFusion.dimensionsAsLongArray(); + System.out.println(" Fusion dimensions: " + dims[0] + " x " + dims[1] + " x " + dims[2]); + System.out.println(); + + // Adjust view registrations for anisotropy and downsampling + final HashMap registrations = + TransformVirtual.adjustAllTransforms( + viewIds, + spimData.getViewRegistrations().getViewRegistrations(), + ANISOTROPY_FACTOR, + DOWNSAMPLING); + + // Warmup runs + System.out.println("--- Warmup Phase ---"); + + for (int i = 0; i < WARMUP_ITERATIONS; i++) { + System.out.print(" Warmup WITHOUT FF (" + (i + 1) + "/" + WARMUP_ITERATIONS + ")... "); + setActive.accept(false); + runFusion(spimData, viewIds, registrations, boundingBoxFusion); + System.out.println("done"); + } + + for (int i = 0; i < WARMUP_ITERATIONS; i++) { + System.out.print(" Warmup WITH FF (" + (i + 1) + "/" + WARMUP_ITERATIONS + ")... "); + setActive.accept(true); + runFusion(spimData, viewIds, registrations, boundingBoxFusion); + System.out.println("done"); + } + + System.out.println(); + + // Benchmark WITHOUT flatfield correction + System.out.println("--- Benchmark Phase ---"); + System.out.println("Running WITHOUT flatfield correction..."); + setActive.accept(false); + final long[] timesWithout = new long[BENCHMARK_ITERATIONS]; + + for (int i = 0; i < BENCHMARK_ITERATIONS; i++) { + System.out.print(" Iteration " + (i + 1) + "/" + BENCHMARK_ITERATIONS + ": "); + final long start = System.currentTimeMillis(); + runFusion(spimData, viewIds, registrations, boundingBoxFusion); + timesWithout[i] = System.currentTimeMillis() - start; + System.out.println(timesWithout[i] + " ms"); + } + + System.out.println(); + + // Benchmark WITH flatfield correction + System.out.println("Running WITH flatfield correction..."); + setActive.accept(true); + final long[] timesWith = new long[BENCHMARK_ITERATIONS]; + + for (int i = 0; i < BENCHMARK_ITERATIONS; i++) { + System.out.print(" Iteration " + (i + 1) + "/" + BENCHMARK_ITERATIONS + ": "); + final long start = System.currentTimeMillis(); + runFusion(spimData, viewIds, registrations, boundingBoxFusion); + timesWith[i] = System.currentTimeMillis() - start; + System.out.println(timesWith[i] + " ms"); + } + + System.out.println(); + + // Calculate and report statistics + final Stats statsWithout = calculateStats(timesWithout); + final Stats statsWith = calculateStats(timesWith); + + System.out.println("--- Benchmark Results ---"); + System.out.printf("Without FF: avg %d ms (min %d, max %d, stddev %.1f)%n", + statsWithout.avg, statsWithout.min, statsWithout.max, statsWithout.stddev); + System.out.printf("With FF: avg %d ms (min %d, max %d, stddev %.1f)%n", + statsWith.avg, statsWith.min, statsWith.max, statsWith.stddev); + + final long overhead = statsWith.avg - statsWithout.avg; + final double overheadPercent = 100.0 * overhead / statsWithout.avg; + + System.out.println(); + System.out.printf("Overhead: %+d ms (%+.1f%%)%n", overhead, overheadPercent); + System.out.println("============================================================"); + } + + /** + * Run fusion and force full computation by copying to ArrayImg. + */ + private static void runFusion( + final SpimData2 spimData, + final List viewIds, + final HashMap registrations, + final Interval boundingBoxFusion) { + // Create fusion BlockSupplier + final BlockSupplier blocks = BlkAffineFusion.init( + (i, o) -> o.set(Math.round(i.get())), + spimData.getSequenceDescription().getImgLoader(), + viewIds, + registrations, + spimData.getSequenceDescription().getViewDescriptions(), + FUSION_TYPE, + ANISOTROPY_FACTOR, + 1, // interpolation: linear + null, // no intensity adjustments + boundingBoxFusion, + new UnsignedShortType(), + BLOCK_SIZE); + + // Force full computation by copying to ArrayImg (not lazy CellImg) + // This ensures all flatfield calculations are performed + BlockSupplierUtils.arrayImg(blocks, new FinalInterval(boundingBoxFusion.dimensionsAsLongArray())); + } + + private static Stats calculateStats(final long[] times) { + long sum = 0; + long min = Long.MAX_VALUE; + long max = Long.MIN_VALUE; + + for (final long t : times) { + sum += t; + min = Math.min(min, t); + max = Math.max(max, t); + } + + final long avg = sum / times.length; + + double variance = 0; + for (final long t : times) { + variance += (t - avg) * (t - avg); + } + variance /= times.length; + final double stddev = Math.sqrt(variance); + + return new Stats(avg, min, max, stddev); + } + + private static class Stats { + final long avg; + final long min; + final long max; + final double stddev; + + Stats(long avg, long min, long max, double stddev) { + this.avg = avg; + this.min = min; + this.max = max; + this.stddev = stddev; + } + } +}