-
Notifications
You must be signed in to change notification settings - Fork 262
Add an 'axis' parameter to concat_images, plus two tests. #298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
out_data[i] = img.get_data().copy() | ||
if axis is not None: | ||
out_data = np.concatenate(out_data, axis=axis) | ||
elif np.all([d.shape[-1] == 1 for d in out_data]): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer not to have a special case for ones on the last axis - the user can always do (when fixed) axis=-1 for that case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough. :)
@matthew-brett Please take a look. I know |
@@ -115,15 +117,19 @@ def concat_images(images, check_affines=True): | |||
affine = img0.affine | |||
header = img0.header | |||
out_shape = (n_imgs, ) + i0shape |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This loses the memory efficiency of the original. How about:
if axis is None: # collect images in output array for efficiency
out_shape = (n_imgs, ) + i0shape
out_data = np.empty(out_shape)
else: # collect images in list for use with np.concatenate
out_data = [None] * n_imgs
Then you can use out_data[i] = img.get_data()
in the loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this less memory efficient? It doesn't copy anywhere. I assume that, under the covers, numpy preallocates a large array and does a memcopy. So I believe this should be very efficient.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry - I should have explained - it's memory efficient because Python can delete the memory for the individual images as it goes through the loop. Having said that, in order for that to happen, we need more logic:
if is_filename:
del img
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it. I have an idea how to do all the cases efficiently; I'll try it out sometime today. I'll also look into improving the test coverage and checks.
Anything I can do to help? |
Sorry, had a conference this weekend and have been doing some research. I can get get back to this on Monday and try to wrap things up! |
No problem - and thanks for your work on this. |
Hope to get to this today or tomorrow. |
Great - thanks.
|
@matthew-brett I've got the basic code working, but had a question while testing. How flexible do you want Would you prefer simply to let that error, or to do the check to make sure those work together? From the testing standpoint, it's no problem to test that case as I've written things now. |
Also, mixed image/filename is not allowed. Easy to extend for that as well. |
My instinct is to go for simpler code at the expense of not allowing obscurities like mixing 3D and 4D, or image objects and filenames. We can always add those later if it looks like a significant use-case. |
…es possible. Test extensively.
…fine on first image.
@matthew-brett It made the code simpler to allow mixed than to disallow it. However, the 3D/4D possibilities did make the code more complex. The code could lose about 5-6 lines of complexity if all images must be 3D or 4D. The testing code is going to be complex no matter what, due to the testing of all axes, None, and no argument specified. Let me know if you'd like to trim some of the cases, but things are working as-is. |
header = img0.header | ||
out_shape = (n_imgs, ) + i0shape | ||
out_data = np.empty(out_shape) | ||
if n_imgs == 0: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Drop this check? I guess if they pass in an empty list they can expect an empty list back?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the past, and currently, this throws an error. I added the check because the error did not indicate the issue clearly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough.
@matthew-brett I cut out the 3D/4D special cases and added testing for 2D and 5D. I did have to add some complexity. I found a lot of edge cases where numpy's broadcasting, or details of The upside is that the error message will be extremely clear. The downside is that the code to check the shape properly in both axis cases was ~7 lines. Let me know what you think! I think we're pretty close now. |
check_affines : {True, False}, optional | ||
If True, then check that all the affines for `images` are nearly | ||
the same, raising a ``ValueError`` otherwise. Default is True | ||
|
||
axis : None or int, optional | ||
If None, concatenates on a new dimension. This rrequires all images |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo rrequires
Try doing the checks on the first image etc outside the body of the loop.
@matthew-brett made the requested changes, sans the question about the error check. |
OK - thanks very much for all your work on this. The last remaining thing is that we are deleting the image even if it is passed in as an image object, in the list, and that could be bad. We either want to delete the image only if we created it by loading the filename, or (probably better) we could use Also you kindly removed the expect_error from the I put in a pull request to your branch just now with fixes for these, and some optional refactoring. If you prefer my version, please go ahead and merge, otherwise - fix these last things and we'll merge asap. Thanks again... |
@matthew-brett I pushed a change for As for the Thanks for the pull request / refactor. I prefer my version with the initialization within the loop, as it potentially avoids loading the first image's data twice. Since we worked hard to improve efficiency in this version, I think that small optimization is worthwhile. But as with you, my preference isn't strong; happy to defer if you feel any obstacle. Thanks also for your work, sorry I didn't get this done more smoothly! Let me know if, after reading these comments, you think there are still some changes to make, and I'll be glad to take care of them ASAP. |
For the del - yes, thinking about it, the loop iteration will make new reference to the object that may have its only other reference in a passed list. On the other hand, this function will fill the image array cache of the passed image objects, which may be desirable or not desirable. I think probably not. For the load data twice if doing initialization outside the loop - could you explain? I think there's only one 'get_data' call in the code. I prefer my code because - well who doesn't prefer their own code? - but also I have an allergy to doing i == 1 initialization in the loop that is instinctive, but this is your work, so this is your call. |
I believe I understand. I also believe this issue existed in the old version of the code. If so, can we open a new issue to address it?
I probably simply don't know the nibabel code well enough. I assumed the data was loaded when |
MRG: suggested refactoring of concat checks
Just merged your code; given what was discussed above, I think it solves the remaining issues. Let's be done with this! :) |
Thank you for being so patient with my nit-picks and slowness, and thanks very much for taking the time to do this. I think the test fails are spurious and it's time to merge... |
MRG: Add an 'axis' parameter to concat_images, plus two tests Add ability to concatenate images over given axis, with tests. Closes #207
MRG: Add an 'axis' parameter to concat_images, plus two tests Add ability to concatenate images over given axis, with tests. Closes nipy#207
Trying to solve issue #207. There are now three cases:
axis is not None
: concat on the specified dimensionaxis is None
and all images have a final dimension of 1: concat on the final dimension (don't add a new one)axis is None
and any image has a final dimension != 1: concat on a new dimension.Existing tests covered the 3rd case. New tests were added for the first two cases.