korbinian90 / ROMEO

Executables for ROMEO unwrapping for Linux, Windows and Mac OSX
39 stars 0 forks source link

Phase Contrast Imaging #9

Open drswgw opened 2 years ago

drswgw commented 2 years ago

Hi Korbinian, Moving the dialog here....the clear SWI portion of things is all resolved. I've moved the newer ROMEO & Laplacian unwrapping stuff here, so you can close the SWI part at your leisure. Right now your answer to the question below is still in the SWI part.

drswgw commented 2 years ago

Hi Korbinian, OK CLEARSWI is working, using MRItools 3.3.1 and being careful to use single quotes 'a' not "a". ClearSWI turns out to be no good for my application: asynchronous phase based encoding of motions/velocities etc. Currently SWI doesn't output the unwrapped phase data, just a magnitude image and the MIP, which for me the MIP is giving a different number of slices than the input data and the magnitude output just isn't my cup of tea. Some of the input slices come in blank, so that may be why the MIP is missing some slices, but it messes up the masking etc. to change the number of slices. So on to Julia to try and implement the Laplacian unwrapping.This is my version of Julia already installed, thought I would upgrade to version 1.7.2 to match the current version of the manual from Julia. Which version of Julia does ROMEO and clearSWI use? image

drswgw commented 2 years ago

Hi Korbinian, Baby steps. Julia up and running, the import for "MriResearchTools" and "FFTW" went swimmingly. What about masking? Probably need to set a mask and call it from Julia, how to do that? image

mre_intrnsep2d_mre_v07_10phs_PRS_150Hz_Pat3_33_FCOFF_A1

drswgw commented 2 years ago

This movie (it's asynchronous, but there are beat frequencies) gives a reasonably good idea of the data, the phase values encode the motion(s). Need unwrapping that preserves the original phase values, just adds/subtracts 2 pi, 4 pi etc. as needed. Once that part is done can move on to the timing, gradients etc.

korbinian90 commented 2 years ago

Unwrapping

As laplacian unwrapping produces arbitrary additions/subtractions and not +-2pi, best to use romeo. In the julia REPL, you can write ?romeo to get function hints on how to use it. For your data, the best option should be to use it on the 4D dataset (timepoints in the 4th dimension) like this:

using MriResearchTools
phase4D = readphase("path-to-phase/phase.nii")
unwrapped = romeo(phase4D; TEs=ones(size(phase,4)))
savenii(unwrapped, "unwrapped.nii"; header=header(phase4D))

romeo needs the TEs to know if the phase images are from multi-echo with different echo times or from epi with identical echo times for each echo.

Masking

romeo does not need a mask, however, if you want to create one:

Julia

You can type all these commands directly in the REPL or write them in a function and call with include like you did. If you get serious with julia, I would recommend to use VS code with the julia extension. There you can interactively work on writing scripts similar to matlab.

korbinian90 commented 2 years ago

And your case of two disconnected regions might require some more tuning of romeo. You can have a look at ?romeo and try out the different options, otherwise I'm happy to help!

drswgw commented 2 years ago

Hi Korbinian, The Julia version is quickly reaching a critical mass, so that's nice. Slight correction, I do have magnitude images to draw from, they are mostly all the same, so typically we average those across the time domain and use those for mask construction e.g. a logical mask in matlab and cast those to double. Sometimes we will rearrange the matrix to take out the empty space, but that's kind of a lot of work for routine stuff. We do pig brains, human brains, phantoms, bioprints etc. so masking is an important part of things for us and one size most definitely does not fit all. I think I can supply the mask (as a .nii file) and put it in my julia_files folder along with the script(s). It's all about motion encoding, so the question is what does ROMEO do with the TE's. Our case is nominally close to the EPI case, all the echoes are acquired at a fixed interval, but the data in the phase domain is responding to an entirely different set of timings.....more like FMRI I suppose...with asynchronous tasks, in this case it's for heart driven motions...two clocks...one for acquisition...one for the heart....there are a variety of gating approaches...we are trying to do image processing INSTEAD of gating.....e.g. difference between breath hold (gated in a very draconian way) and free breathing (largely ungated). Since stopping the heart is not an option...here we are. One thing that kills us is offsets....the average phase for the original portions of the image that are not wrapped is in fact data for us...so moving that original average phase around is not ideal. We would rather preserve that then offset the wrapped portions either UP by 2 pi, 4 pi, 6 pi or DOWN by 2 pi, 4 pi, 6 pi. That's perfect for us. If we can do THAT, then we can move on to decoding what time it is in the acquisition frame and in the heart frame, which ultimately gives us something a bit like CINE, except with NUMBERS....as to just what the phases are, at a given time, in a QUANTITATIVE manner...it's a phase movie, with numerical values...that's the goal state. Will work on implementing the Julia side, and maybe look into VS. Another option is to pass to Julia from Matlab. For now, keeping it old school with gedit.

drswgw commented 2 years ago

Hi Korbinian, Here is the acquisition clock.....but this is NOT the heart clock....although the slice timing is directly related to the heart clock...kind of like a modulo thing e.g. moduloheart(slice timing Selection_035 )

drswgw commented 2 years ago

Anyway, Not sure I understand what ROMEO is doing with TE = [1,1,1,1....] or TE = [1,2,3,4,....] the latter is what I have been using up till now. In terms of the right timing from the signal side...it probably looks something like (scale of 0-1 of the heart cycle.....[0.3, 0.45, 0.17, 0.22, 0.96, 0.11, 0.44, 0.34, 0.71.......]....not like a tango at all. On a certain level, it might make more sense to feed all the slices independently as one offs.....IF ROMEO is trying to match phases across 'times' e.g. phase at t1 matches phase at t2...that's a disaster....all the 'cuts' are jump cuts in time right now...so there is NO obvious relationship in time between adjacent acquisitions...i.e. asynchronous acquisition.

drswgw commented 2 years ago

Hi Korbinian, ROMEO does not need a mask? So what happens when it tries to unwrap the regions of the image that are random phases?

korbinian90 commented 2 years ago

Not sure I understand what ROMEO is doing with TE

Two things:

  1. To improve the weights that guide unwrapping with the first two volumes. It looks for phase consistency across time. If the second volume has a longer echo time, this needs to be considered. It usually greatly helps the unwrapping
  2. To use temporal unwrapping. By default, only the second volume in spatially unwrapped and starting from that all other volumes are unwrapped. As long there is significantly less than π phase evolution, this works fine.

-> the output phase is still only changed by multiples of 2π. No weird scaling is happening in ROMEO

ROMEO does not need a mask? So what happens when it tries to unwrap the regions of the image that are random phases

No mask is required ROMEO starts with easy-to-unwrap regions with well behaved phase and only later goes into these random regions. It will mess up greatly there, but this doesn't influence any of the "good" areas. If the image consists of multiple parts seperated by noise, you probably have to set the seeds parameter to a higher value. Otherwise the unwrapping has to cross noise.

korbinian90 commented 2 years ago

I didn't fully understand what your effective TE is. So what romeo assumes is that phase evolves linearly with time. For longer echo time, the phase changes are bigger. If the "fMRI" phase changes are small compared to π, it is probably fine using TEs=ones(...). If these changes are quite large, it would probably help if you add proportionality factors as TEs.

I think best is to start with the default romeo configuration of "4D with temporal unwrapping"

If that doesn't give satisfying results, you can try "4D without temporal unwrapping (unwrap-individual flag)"

And if that fails you could try separate 3D unwrapping, but to avoid ending up with 2pi jumps between volumes, it is probably best to use the correctglobal flag

drswgw commented 2 years ago

Perfect, a lot to chew on, but enough to move forward...cool! Onwards from "hellojulia.jl" to "unwrap.jl". I think I will have to try a couple of things and compare those to make sure the results are consistent.

korbinian90 commented 2 years ago

btw, I added documentation to my packages ROMEO and MriResearchTools, accesible via the little badge docs|dev (or here: https://korbinian90.github.io/MriResearchTools.jl/dev/)

drswgw commented 2 years ago

Hi Korbinian, First go round - only so-so. Need to force several things to move forward...first....apply the mask, and have romeo not unwrap outside the mask, and display the data MASKED. As near as I can tell without a mask...romeo is folding the phases the WRONG way....the flow is down then up....or up then down...relative to the gradient, so one side should be darker than the other throughout. Where the flow is concentrated, things should speed up i.e. get whiter (or blacker) depending on direction. seems like all the values near pi or -pi are being wrapped towards zero, or the base level is being shifted. Consider a mountain.....the data starts...partway up the mountain....a correct unwrapping should yield a topo map, everything BELOW the starting point should be LESS, and everything ABOVE the starting point should be MORE. This current unwrapping isn't doing that...or at least I don't think it is, instead it all seems to be getting compressed towards the starting point/average. Selection_039 Selection_038 Selection_037 Selection_036

drswgw commented 2 years ago

Do I need to scale the initial spectra to -pi to pi, that is an acceptable, or acceptable enough, version of how the data really is.....the last frame above on the lower left looks right, darker on one side, lighter on the other, that's pre-unwrapping. And this one below looks OK too. If I was going to guess, I'd say some regions are unwrapping WELL, and other regions, not so well Selection_040 .

drswgw commented 2 years ago

Definitely promising, but not optimized yet, at least I don't think so.

drswgw commented 2 years ago

This looks better, well it doesn't 'look' better, but it seems better, more consistent, more robust

Selection_043

Selection_041

Took a little while to get the syntax, and still don't know how to include a mask to guide the unwrapping...and not sure if the mag image is being used or not......but...tentatively....better and/or moving forward! The numbers look wrong though...I think it is expecting the input in radians, so there is something funky going on with the scaling...should start [-pi,pi] and probably end something like [-5 pi, 5 pi]

drswgw commented 2 years ago

Hi Korbinian, Thought about it a little bit and what is needed is FIXED scaling. The data starts as 12 bit ([0,4095]) and typically is rescaled slightly to yield ([-4095,4095])...I think that's what the DICOM to NIFTI conversion is doing, and that's all copasetic. From there probably needs to be FIXED SCALED down to [-pi, pi] before unwrapping. Variable scaling ([min, max]) => [-pi,pi] won't work because some data is wrapped, while other data is not, the data is intrinsically scaled (by the gradients) so the original [0,4095] => [-4095,4095] => [-pi, pi] is all fine as long as it's a FIXED scaling. I can do that outside Julia, but it would be nice to be able to do fixed scaling inside Julia..... The cast to 16 bit and later on to double etc. doesn't change the original [0,4095] data density.

P.S. All the help is moving this along very nicely and rapidly. Liking the Julia interface a lot, and it runs FAST!

drswgw commented 2 years ago

MUCH BETTER! BAM! Selection_044 Selection_045

Still need to get better control of the masking (importing a NIFTI mask so far is not working, the default type for the mask in ROMEO is apparently Bool, while for NIFTI it is float/double...or more precisely from Matlab to NIFTI). And better control over the scaling - fixed not variable. Great progress!

drswgw commented 2 years ago

Looks like it has shifted the original data away from the starting values...that's NOT ideal....but there are more gradients possible - this is a simple all plus gradient - but +/- are possible which allows for a subtraction later....still even in that case, the DC term is not meaningless but carries data, so would prefer not to mess with the DC component......the challenge is how to turn things OFF in ROMEO....turn it ALL OFF...then bring back ON states one at a time...that would be more useful. i.e. the initial data average was around pi/2 on one side and around pi on the other...the true mean is around 3 pi/4.....the unwrapped data has a mean near zero.....the 3pi/4 true mean has been REMOVED....the raw unwrapped data should look something like pi/2 - 5pi/4.....of course I can sacrifice the DC term if I must....but I'll be sad to see it go....back to the topo map...the top of Everest after shifting has zero elevation...so climbing it is...zero (not strictly true, because the bottom would be negative) but it's meant to be a metaphor about the importance of DC terms. Still, this is increasingly workable...so can move on towards 'fmri' timing...yay! Getting happy!

drswgw commented 2 years ago

Can't import or export masks...... the BitArray{4} julia type is not apparently compatible with NIFTI, or at least not obviously. Can't turn off pre-scaling and replace it with my own absolute scaling scal*Phase4D Can't turn off zero shift Selection_046

drswgw commented 2 years ago

Purely cosmetic touches, but moving towards something easier to run/play with... the JSON module is to pull slice timings from the NIFTI prequel/header via MRIcroGL (which is in JSON). Selection_047 Selection_048 Selection_051

.

korbinian90 commented 2 years ago

A lot to read and understand, I don't have a lot of time, so I will answer some parts Glad that it is moving along!

Masking

I think masking or not masking the data doesn't really influence the values inside your object after unwrapping, only in the background noise. I think this problem could be solved by adapting the scaling inside the viewer. Another option would be to mask after unwrapping with something like: unwrapped = unwrapped .* mask or unwrapped[mask] .= 0 The datatype of the mask shouldn't matter for romeo. But you can just create a binary mask with: binary_mask = mask .== 0 (The . is like in matlab, but is generalized. It works nearly everywhere to make stuff run voxelwise on an array)

Scaling

The readphase() function should perform quite good scaling. You can compare it by just saving the image again. The output of phase = readphase(...) is a struct with header and image. You can access the unscaled values via phase.raw and scale them yourself with scaled_phase = phase.raw .* (pi / 2048). And best to save it back to disk and check if the range is 2pi afterwards.

Gradient

ROMEO shouldn't be able to change a gradient in the phase as it adds/subtracts only 2pi from individual voxels. If there is an unwrapping error in the object, it should be clearly visible as a discrete phase jump of some voxels.

korbinian90 commented 2 years ago

As your data is not wrapped in most part, the unwrapped output should be identical to the input phase in large areas. You could confirm that ROMEO does what you want with:

phase = readphase(...)
unwrapped = romeo(phase, ...)
difference = phase .- unwrapped
# difference .*= mask # optional masking of difference image to avoid confusing background
savenii(difference, "difference.nii")

Viewing the difference.nii should show probably 0 in most of your object and 2pi / -2pi at wrapped parts. In noise the values get prabably large and random.

drswgw commented 2 years ago

Cool, all helpful...will play on!

drswgw commented 2 years ago

Hi Korbinian, Took a while to get Julia to plot anything, documentation is pretty bad/conflicting/confusing. For Ubuntu 16, and the python versions it uses + what I am running in this environment, matplotlib tops out at about vsn 3.0, so gave up on using the pyplot backend, the GR backend is completely hopeless (can only launch a single plot window), and plotlyjs is mediocre (can't resize plots after they are drawn, can probably pre-size...just a detail right now)....plotting is only an aid along the way...therefore plotlyjs is good enough. Working on slice timing, ultimately need to use the slice timing to reorder the unwrapped data in time...and thus check if the unwrapping has affected the temporal response. Since the unwrapping is using constant timing...it may be that the unwrapping will be better if I reorder first...we shall see.....trying to do that reordering in Julia, and it's going OK for now....I'm a hacker not a coder...so elegant...not....but getting pretty happy so far.

Selection_053 Selection_054 Selection_055

drswgw commented 2 years ago

Hi Korbinian, Made some real progress here. The julia code is pretty messy still, so I probably won't post it here - our application is pretty niche. If there's interest I'm not against posting it. Anyway...here is the update.... DATA AFTER TEMPORAL UNWRAPPING (note stripes left image coronal and only image sagittal - from asynchronous acquisition/slice timings, 2 sine waves per slice, alternating UP/DOWN phases). Data is acquired axial, but the slice timings manifest as phase offsets from slice to slice...this is a pretty common problem! unwrp

DATA AFTER RESORTING/RETIMING FOR OUR APPLICATION (note stripes are gone now, and only 1 sine wave per slice, continuous phase direction from slice to slice...SUCCESS!) srt_ret_unwrp

Residual issues are mostly to do with optmizing the ROMEO unwrapping. For instance comparing spatial unwrapping with temporal unwrapping, sort AFTER unwrapping, sort BEFORE unwrapping, play with other parameters etc. All very happy, Julia was the key....cutting out the middle man made a big difference here. Could probably port it all back to Matlab calls...but why...as a Julia "app" very happy...NIFTI IN => unwrapped/sorted OUT....BAM! issue closed

P.S. If you don't mind leaving your comments up, still working through/along those suggestions, very relevant to what I am doing. So long and thanks for all the fish!

drswgw commented 2 years ago

SrtRetUnwrp

drswgw commented 2 years ago

WOW! BEAUTIFUL! (still some tweaking to do...but looks really good!)

korbinian90 commented 2 years ago

Wow, looks very nice! Really cool video Great that you came back to report on the progress, I was wondering if you were getting good results.

In the video, there are some individual voxels at the edge of the top part of the object, which jump between black and white. Temporal unwrapping is supposed to keep those voxels from jumping around by always unwrapping into the same direction. I'm not sure if it is a problem for you, though.

P.S. I will just keep this open so all comments stay there ;)

drswgw commented 2 years ago

OK....can't rest on our laurels here.

All the previous stuff is single gradient asynchronous acquisition...so that's a good start...but now onto something more real world...this data is dual gradient asynchronous. Looks promising to start => stripes (asynchronous encoding)...check => dual gradients (alternating up/down phase offsets)...check

Temporal unwrapping pretty much bombed for dual gradient - froze everything solid in the time domain...strange because it seemed to be working OK for single gradient. Probably need to go back and look at spatial unwrapping for single gradient...just to be sure.

This is spatial unwrapping with global correct. There's a bug (or some inconsistency in my code) so I lost the masking. I can include the julia error or pose it as a question ...does the ordering of the parameter calls to romeo matter e.g. mask then spatial vs spatial then mask??....masking was working for temporal unwrapping, now not working for spatial, anyway...pretty sure that the two points that went awol from global correct are off by 2pi (maybe 4 pi), looks like they slipped out of the corral. Can I just re-correct them afterwards e.g. find the means and offset everything? Suspect that in this case masking may matter, think it's offsetting the ansatz versus the actual data.

new question: what is linear unwrapping??

There's a lot of spaghetti, but heres the meatball romeo

ORIGINAL RAW PHASE DATA dual_gradient_raw

AFTER SPATIAL UNWRAPPING WITH GLOBAL CORRECT dual_gradient

korbinian90 commented 2 years ago

The global correction is this: wrapped .-= (2π * median(round.(filter(isfinite, wrapped[mask]) ./ 2π))) Without a proper mask, it takes the whole image with noise, which is probably why it fails on your data. You can perform it after ROMEO (like you asked) for each 3D volume with a proper mask.

Weird that temporal unwrapping failed. As long as there is less than π phase change between subsequent timepoints it should work. One reason might be that the phase in bipolar data has a mostly linear phase change in readout direction. Is dual gradient what you mentioned similar to a bipolar acquisition for multi-echo scans?

Linear unwrapping is just an experimental feature that might help if there are many closely spaced wraps. If wraps are so close that neighboring voxels have more than π phase difference, standard unwrapping fails. Linear unwrapping uses the slope of phase change and might succeed in such a case. However, this makes unwrapping less robust in noisy areas. So, I think it's nothing that would help your data.

drswgw commented 2 years ago

Hi Korbinian, I think I need to learn how to call romeo in more of a piecemeal manner... right now....romeo do A,B,C,D,E,F....some of which are fighting each other I think....

How do I do...romeo do A, B... then I do some stuff.... then...romeo do C.... then I do some stuff then romeo do D,E,F...

is that possible...i.e. can I use romeo to do a global correct afterwards??? That's the most useful...calling romeo as a julia subroutine, with romeo doing sub_subroutines ideally not all at once...much easier to assess and debug that way

drswgw commented 2 years ago

Lets see, dual gradient is indeed a kind of bipolar gradient, we are just calling + then - gradients as separate acquisitions. The goal is to then subtract those after unwrapping...with the added complication that the asynchronous acquisition has to be dealt with before it is really possible to subtract.....I am intrigued by the linear unwrapping...because under the right circumstances we would fill the bandwidth with signal i.e. a signal that is say - 6 pi to 6 pi, which means, that with the asynchronous acquisition, slice to slice changes could easily be > pi, or greater than 2pi for that matter. Right now the signal is fairly small (working on that), which may be what's going on with the single gradient sneaking in under the pi 'fence', while possibly the dual gradient is just over...not sure yet....am beginning to suspect that the TE's matter, we are diverging away from TE =1,1,1,1....but have no clue as to how romeo would interpret TE = 0.1, 0.7, 0.2, 0.9 which is more how things would be posed in a more standard 2nd echo sense. from that perspective we have something like 10 echoes, all out of sequence e.g. 9th echo, 1st echo, 5th echo etc.

korbinian90 commented 2 years ago

Just a quick idea. You could try doing the even and odds separately, like: uw[1:2:end] = romeo(phase[1:2:end]) uw[2:2:end] = romeo(phase[2:2:end])

korbinian90 commented 2 years ago

If you want to separate ROMEO, these are the parts:

3D unwrapping (first 16 lines of https://github.com/korbinian90/ROMEO.jl/blob/master/src/unwrapping.jl) 1) calculateweights 2) grow_region_unwrap! (this includes region merging, but there was no problem in your case) 3) optional correct_global

Temporal unwrapping for 4D datasets (line 67-107 of https://github.com/korbinian90/ROMEO.jl/blob/master/src/unwrapping.jl) 1) calculateweights (4D) 2) 3D unwrapping 3) temporal unwrapping (w .= unwrapvoxel.(w, refvalue) 4) optional experimental temporal_uncertain_unwrapping (only tries to improve individual noisy voxels)

Unwrap individual of 4D datasets (line 148-157 of https://github.com/korbinian90/ROMEO.jl/blob/master/src/unwrapping.jl) 1) define a phase2 for better unwrapping-weights estimation 2) 3D unwrapping of each volume (with phase2 of neighboring volume)

After writing this, I think there is not much gain for you in separating the parts. I think most promising would be to 1) improve correct_global with using a mask 2) avoid gradient polarity problems (either correct them or separate unwrapping of even and odd)

drswgw commented 2 years ago

Hi Korbinian, Like the divide and conquer approach, so going after 4 chunks (even/odd & pls/minus gradients). Remember, I'm a hacker not a coder, but this approach looks promising so far....

magevnpls = mag4D[:,:,2:2:szmag4D[3],1:2:szmag4D[4]-1] magevnmns = mag4D[:,:,2:2:szmag4D[3],2:2:szmag4D[4]] magoddpls = mag4D[:,:,1:2:szmag4D[3]-1,1:2:szmag4D[4]-1] magoddmns = mag4D[:,:,1:2:szmag4D[3]-1,2:2:szmag4D[4]]

phsevnpls = phs4D[:,:,2:2:szmag4D[3],1:2:szmag4D[4]-1] phsevnmns = phs4D[:,:,2:2:szmag4D[3],2:2:szmag4D[4]] phsoddpls = phs4D[:,:,1:2:szmag4D[3]-1,1:2:szmag4D[4]-1] phsoddmns = phs4D[:,:,1:2:szmag4D[3]-1,2:2:szmag4D[4]]

mskevnpls = msk4D[:,:,2:2:szmag4D[3],1:2:szmag4D[4]-1] mskevnmns = msk4D[:,:,2:2:szmag4D[3],2:2:szmag4D[4]] mskoddpls = msk4D[:,:,1:2:szmag4D[3]-1,1:2:szmag4D[4]-1] mskoddmns = msk4D[:,:,1:2:szmag4D[3]-1,2:2:szmag4D[4]]

uwevnpls = romeo(phsevnpls;TEs=ones(size(phsevnpls,4)),mag=magevnpls,maxseeds=2,mask=mskevnpls)

uwevnmns = romeo(phsevnmns;TEs=ones(size(phsevnmns,4)),mag=magevnmns,maxseeds=2,mask=mskevnmns)

uwoddpls = romeo(phsoddpls;TEs=ones(size(phsoddpls,4)),mag=magoddpls,maxseeds=2,mask=mskoddpls)

uwoddmns = romeo(phsoddmns;TEs=ones(size(phsoddmns,4)),mag=magoddmns,maxseeds=2,mask=mskoddmns)

so those are all smaller submatrices, which I can play with individually, then finally reassemble the whole thing after all the processing is done. That's the concept anyway. Good idea to divide into chunks....not obvious probably, but dividing spatially is unifying temporally.

drswgw commented 2 years ago

Hi Korbinian, Reasonably sure this is not working... savenii(uwoddpls, ggoddpls; header=header(phsoddpls))

tried ?savenii but that doesn't yield any result....

before it was using header = header(phs4D), where that header (I think) was being parsed in from an actual .nii file using readphase().

So not understanding the header aspects, and thus cannot write OUT the submatrices (to check if those are getting unwrapped properly or not).

drswgw commented 2 years ago

fixed that by writing out without a header, seems to be working OK. Working on masking and retiming now.

drswgw commented 2 years ago

So far looks like temporal unwrapping may be working again...not sure yet without the retiming....may circle back to the spatial unwrapping to compare and contrast

drswgw commented 2 years ago

Can't get this to work....romeo is interpreting (I think?) the individual case as an nxmxw matrix which is incompatible in it's mind with an nxmxwxq mask......will try again with a reduced dimensionality mask.....

full raw data, spatial unwrapping instead of temporal

uw = romeo(phs4D; TEs=ones(size(phs4D,4)), mag=mag4D, maxseeds=2, mask=msk4D, correctglobal=true, individual=true)

ERROR: LoadError: BoundsError: attempt to access 80×80×48 view(::Array{Float32, 4}, :, :, :, 1) with eltype Float32 at index [80×80×48×20 BitArray{4}]

drswgw commented 2 years ago

OK, got the spatial unwrapping to work with a mask (of reduced dim)

full raw data, spatial unwrapping instead of temporal

uw = romeo(phs4D; TEs=ones(size(phs4D,4)), mag=mag4D, maxseeds=2, mask=msk4D[:,:,:,1], 
                    correctglobal=true, individual=true)

and that fixes the ansatz problem...so that approach is looking the best so far. Easier than dividing into chunks, although that is still interesting too....

spatial

drswgw commented 2 years ago

No 5D capability....(just in general, not unique to Romeo)....so splitting out the gradients into clusters of 4D data makes the most sense (see divide and conquer did come around, just not the way either of us thought). That's worth circling back in the code for (splitting out the gradients)...but here is the first go round....gradients split out, spatial unwrapping, reduced dim mask, global correct...

PLUS GRADIENT (4D) pls4D

MINUS GRADIENT (4D) SRUmns

I think it's looking pretty good...a little noisy but for THIS data set, I think that's right....

Looking forwards (to 6 gradients) it would be nice to have 5D capability, BUT.....this approach of splitting out the gradients is workable...certainly for now. Romeo...for the most part...is performing very well!

Not quite ready to declare victory....but feeling pretty good about it so far....still a ways to go....

drswgw commented 2 years ago

Not a huge amount of signal...but I think the unwrapping and resyncing are working reasonably well....

Grdt Plus, from pls/min asynchronous acquisition. The global corrections are applied everywhere apparently (inside and outside mask). At a guess, the spatial unwrapping is a bit more ragged.

150Hz_Splsmin_FCOFF_SRUpls

drswgw commented 2 years ago

Wahh, wahh, wahhh. On to stronger gradients. At first looks good...but then...mask is breaking down and the spatial unwrapping misbehaving again. This is using robustmask based on magnitude images. Think I will have to keep working on the mask subroutine, so far it does 0 - no mask, 1 - romeo robust mask....but I think I need to add 2 - external mask.....at a guess, the stronger gradients are a LOT more challenging for the unwrapping.

THIS LOOKS GOOD!!! (considering what it is supposed to be) Apt3_150Hz

Wahhhhhh! Waaahhhh! Waaahh! (thought I had this working)

150Hz_Apt3_other

drswgw commented 2 years ago

robust mask has no external threshold, so the internal threshold is apparently designed around quite different data (admittedly, this mag data is not trivial to mask). MAG DATA mag

korbinian90 commented 2 years ago

This robustmask is obviously not so robust (bad name choice). It uses only the magnitude

You could try the mask from ROMEO weights. romeovoxelquality has the same call syntax as ROMEO

using MriResearchTools
qmap = romeovoxelquality(phase; TEs=ones(size(phase, 4)))
mask = mask_from_voxelquality(qmap, 0.1) # threshold between 0 and 1

Or an external mask of course

drswgw commented 2 years ago

Quality map from the phase data...with adjustable quality threshold...definitely worth trying....otherwise will have to start building perimeter maps...which are a hassle for sure. Robustmask does have a nice ring to it though!

drswgw commented 2 years ago

OK, it's not the masking per se. Romeo is just guessing wrong as to what the 'global state' should be. This may be saying that the data MUST be grouped BEFORE unwrapping, otherwise the deviations are just too abrupt (after all there is a LARGE asynchronous phase lag between slices, AND a LARGE gradient differential on top of that). Back to what you were saying about temporal unwrapping being more robust.....if I can get the spatial to work though...that's the most 'robust' for this application. What about the seed...can I drive that really large so it will 'unify' left and right more so. This particular data set doesn't really 'connect' so it is quite un-brainlike in that respect, but important to yield 'ground truth' results. Consider it to be e.g. two distinct blood vessels.

P.S. the phase masking was a little better because it was more tunable, but overall...not sensational.

This is a combination of threshold and imerode/rolling ball...OKish/better. The majority result (darker) is probably correct.

BETTER EXTERNAL MASK

better_mask

korbinian90 commented 2 years ago

Ah, I see. Yes you can do just that, use a larger number of seeds. The additional seeds are only applied if needed, so you can just try with eg. 50 seeds.

You can get ROMEO to output the detected regions with giving it an empty array to store them in:

regions=zeros(UInt8, size(phase)[1:3]);
uw = romeo(phase; ...., regions, merge_regions=true, correct_regions=true)
savenii(Float32.(regions), ....)

If you look at the regions, you see a different integer for each detected region. You then need to activate the flags region_merging and correct_regions. It performs first merging of regions that touch and then a global correction of regions not touching.