Trouble with nibabel generated nii file

As part of generating a subdivided parcellation, I’ve generated a volumetric parcellation nii.gz from a NumPy array using Nibabel, with code such as

>>> parc.dtype, parc.shape
(dtype('int32'), (256, 256, 256))
>>> parc_img = nibabel.Nifti1Image(parc, np.eye(4))
>>> parc_img.to_filename('parc.nii.gz')

and while I can view it OK with Freeview, mrview does not seem to see the data in the file.

The uploader here won’t allow it, so here is a link to an example file:

Is there anything I can do to fix the volume so mrview (and labelconfig, etc) will successfully read the data?

Hi @maedoc, I was successfully able to mrinfo your .nii.gz file; but indeed mrviewing it from the command line gave me the error

mrview: [ERROR] cannot access file "par.nii.gz": No such file or directory
mrview: [ERROR] error opening image "par.nii.gz"

I was able to successfully load it into mrview directly from the GUI though; this is what it seems to look like:

(click to enlarge)

It does seem like there’s something odd about it though, unless you expected those labels to be in the far end/corner of the image space?

@ThijsDhollander: that’s weird, I had no such error message when opening the same image on my side…

However, the issue here is a failure of the automatic intensity scaling. When the image is first loaded, the slice displayed (through the centre of the FoV) happen to contain nothing but zeros. This causes the intensity range to be set to (-1, 0), and this is the range used subsequently when looking at all the other slices. So there is nothing shown after that even when the slice does contain data. But to see it, you’d need to set the intensity scale appropriately - the simplest way is using the view tool, and setting the min/max to (0,1).

What MRView should probably do is not set the range in cases where there is no range as such (i.e. min == max), and update it properly when it does encounter a slice with an actual range of values. Maybe we should open an issue for that…?

Thijs’ error is just a mis-spell of the filename.

An alternative solution is that if there’s no contrast in the slice when the intensity scaling is reset, set according to the min/max of the entire image (performing that calculation if necessary). Otherwise you might encounter a neighbouring slice for which the intensity range is not reflective of the rest of the image, and only that slice will trigger the re-windowing. That makes sense if the user has actually requested a windowing reset while looking at that slice, but not if they’re commencing scrolling through the whole volume.

Ah yep, just double checked; it’s definitely just that. :thumbsup:

Sure, that’s an option, but I don’t think it would be a good idea, since that would entail a full load-to-RAM of the data set, which may take a while depending on the storage medium and the size of the image. With the slice-wise windowing, the min/max values get computed on slice load anyway, so there’d be no lag. And the min/max would be appropriate for that slice.

Well, in truth that argument applies even when this issue doesn’t occur. There’s no guarantees that the intensity range of the mid-slice that is initially displayed is appropriate for the rest of the image, so by that rationale we should set the intensity scaling based on the entire volume regardless. This would limit how responsive the viewer can be, something we’ve already discussed a fair bit on GitHub issue 405. This is why I think the simplest is to set the min/max based on the first slice encountered that actually has anything to display.

Thanks for confirming; setting the scale manually works. I was mainly concerned that labelconfig and tck2connectome would not be able to use the volumes I generate.

Re: responsiveness, you could keep current behavior, but have a background thread walk through the full volume to eventually update the intensity scaling.

That is an option - but I’m not sure how useful that would be in practice. You still need to display the slice to the user as soon as possible, so that would set the initial scaling. If you then obtained updated scaling parameters a few seconds later, what do you do? Update the UI accordingly? That would probably become annoying pretty quickly… What if the user has already interacted with the scaling with the mouse? The other issue is what happens over e.g. a network share, if you’re going through images to quickly inspect them (a use case where MRView currently works really well): if you start a thread each time to read each image in full, that would very quickly saturate the bandwidth, most likely for no gain since the user would already have moved on to the next image before the updated scaling parameters become available.

I think we’re better off sticking with the current simple approach, in spite of its minor quirks…

Agree would be annoying. Another minor suggestion: having written a simple volume viewer in MATLAB a while ago, I found it useful to have a CDF of values, even if calculated on 1 out of 10 elements in the image (so, fast enough). The shape of the CDF tells you where the contrast lies. Zooming on the histogram view would reset intensity scaling on the main view. This was particularly useful for CT scans (obviously not mrtrix target, but anyway).

I think we’re thinking along similar lines - I was thinking a histogram-based approach to the automatic windowing would be a more robust approach, as discussed in this post. But I was going to keep it a fair bit simpler than what you seem to be suggesting… :grin:

Well, in truth that argument applies even when this issue doesn’t occur.

The difference is that if the initial slice loaded is not representative of the full-brain contrast, you at least see a reasonable static image of that first slice; if you then scroll and the contrast is no longer appropriate, the user has a reasonable chance of figuring out why.

The problem with doing it this way:

What MRView should probably do is not set the range in cases where there is no range as such (i.e. min == max), and update it properly when it does encounter a slice with an actual range of values.

, is the following scenario. The initial slice is empty, so the scaling is set to [-1,0]. The user then starts quickly scrolling through their volume trying to find something. The first non-empty volume triggers an intensity windowing calculation; but that windowing may not be appropriate for the rest of the volume, so the user probably then sees either all black or all white.

The difference between the two scenarios is that in the first one, the user was at least presented with one reasonable-looking slice before things went awry; in the second, the user is likely still confused as to why they can’t see an image.

With my suggestion a whole-volume intensity windowing would only be triggered if the initial displayed slice were empty; which is very rare, and the time spent scanning the image volume would have been less than that required to post a question on the forum :stuck_out_tongue:

Sorry, I still don’t get this. Why would they see either all black or all white when the scaling has just been set specifically for that slice? I’m assuming you’re talking about what happens once they scroll away from that slice? But why this would be any different between the two scenarios? Whether the scaling is triggered when the data is initially displayed (i.e that slice did happen to have data) or later when scrolling through is totally arbitrary (although the first case is clearly much more likely to happen). But regardless of when the scaling is triggered, the slice used for that is equally likely to be unrepresentative of the rest of the volume. So in both cases, they would see an image as soon as there is one to display - what happens after that may not be appropriate for the rest of the volume, once they scroll away from that slice, but that is the same in both cases (?).

In my experience, the vast majority of cases where this happens is when loading ROIs (or segmentations, as is the case here), which can have a large FoV with only a small portion of the space filled. In this case, the display would be black if the middle slice was empty, and immediately switch to the appropriate scaling as soon as any part of the ROI makes it into the image. Crucially, that scaling ought be adequate for the rest of the ROI from that point on.

And also, these images would be much more appropriately displayed in the overlay or ROI tool, which does a full load of the data (by necessity) and hence does set the scaling to the genuine min/max of the whole volume. But that’s another issue…

If the user doesn’t see any contents in the first slice loaded, they are more likely to scroll by a large number of slices, looking for ‘something’ in their image. What I’m considering is the case where the user flicks the scroll wheel, or uses the keyboard modifier and mouse, to move a lot of slices very quickly, because they panic thinking that their image is empty.

By your approach, the first non-empty slice encountered would trigger an intensity windowing reset - the range of which may not be appropriate for the rest of the volume. Therefore, the user is initially met with a black screen when mrview is opened (because the initial slice is empty); then after having scrolled a bit, they are met with a black screen / massively saturated image (because the intensity windowing was triggered by the first non-empty slice, which wasn’t representative). Sure, that one slice that triggered the windowing would have looked nice; but the user might not have seen it as they started madly scrolling looking for ‘something’.

By my solution, in those rare cases where the first slice is empty, you swallow the speed penalty of a whole image load in order to set the intensity windowing according to the whole volume. That way, regardless of where the user scrolls, they are more likely to see the image features that they are looking for for reference.

Yes, I thought that’s what you might have meant… Oh well, I guess we’ll have to agree to disagree then.

How about we file a GitHub issue and carry on the discussion over there…?