Conversation
nilgoyette
left a comment
There was a problem hiding this comment.
I can't review anything thoroughly now, but here are some questions and comments.
Btw, good job. This is a lot of work. Thank you.
| /// Magic code for full NIFTI-1 files (extention ".nii[.gz]"). | ||
| pub const MAGIC_CODE_NIP2: &[u8; 8] = b"n+2\0\r\n\x1A\n"; | ||
|
|
||
| /// Abstraction over NIFTI version 1 and 2 headers. This is the high-level type |
There was a problem hiding this comment.
May I ask you why you use double space after all sentences? I never seen that before.
There was a problem hiding this comment.
I heard rumors that typewriters had issues which were resolved with the double spacing, and with that some people grew into the aesthetics of separating sentences like this. 🤔
Nowadays though, I practice a form of semantic linefeeds or semantic line breaks, which recommends introducing a new line at the end of each sentence.
In any case, this isn't a critical enough concern to be a blocker.
| /// Header size, must be 348 for a NIFTI-1 header and 540 for NIFTI-2. | ||
| /// There is no corresponding `set_sizeof_hdr()` because you should never | ||
| /// need to set this field manually. | ||
| pub fn get_sizeof_hdr(&self) -> u32 { |
There was a problem hiding this comment.
I think @Enet4 once told me to not use get_ in Rust. I don't have a clear opinion on this matter, but it seems it's not idiomatic. Maybe he can confirm.
There was a problem hiding this comment.
Yup, it's the C-GETTER guideline. Getters in Rust usually usually aren't prefixed with get_ by convention.
| pub fn get_sizeof_hdr(&self) -> u32 { | |
| pub fn sizeof_hdr(&self) -> u32 { |
|
|
||
| // All done, return header with populated fields. | ||
| Ok(h) | ||
| } No newline at end of file |
There was a problem hiding this comment.
Do you use an automatic formatter? (Don't search what's not formatted here ^^)
| let len = header.vox_offset as usize; | ||
| let len = if len < 352 { 0 } else { len - 352 }; | ||
| let len: usize = header.get_vox_offset()?.try_into()?; | ||
| let len = if len == 0 { 0 } else { len - TryInto::<usize>::try_into(header.get_sizeof_hdr())? }; // TODO! |
There was a problem hiding this comment.
Can you explain more what's left TODO? We need to know if we ever need to do it.
| } | ||
|
|
||
| #[test] | ||
| fn write_descrip_panic() { |
There was a problem hiding this comment.
Is this not tested anymore?
There was a problem hiding this comment.
It is no longer possible because descrip was turned back into an 80-byte array. It was kind of what I originally had in mind, but large arrays were not as nice to work with in Rust. That changed in recent versions, so unless we witness a significant performance regression, we can save the description in a [u8; 80] here instead of Vec<u8>.
There was a problem hiding this comment.
Enet4
left a comment
There was a problem hiding this comment.
Thank you for this major contribution, @benkay86! I went through the code and made a quick review. What I feel are the two major concerns are:
- getter naming convention (as already mentioned, they should not use
get_) - linear transformations for volume conversion will use f64 instead of f32 in many cases, which I suspect could affect cases where the original data is all in f32. If all voxel values have to go through f64, this could translate to a bigger memory footprint and maybe less performance. It might help to try a stress test with f32 volumes and see if performance and memory usage changed significantly.
| /// Invalid header size. Header size must be 540 for NIfTI-2 or 348 for | ||
| /// NIfTI-1. | ||
| InvalidHeaderSize(sizeof_hdr: i32) { | ||
| display("Invalid header size {} must eb 540 for NIfTI-2 or 348 for NIfTI-1.", sizeof_hdr) |
There was a problem hiding this comment.
| display("Invalid header size {} must eb 540 for NIfTI-2 or 348 for NIfTI-1.", sizeof_hdr) | |
| display("Invalid header size {}: must be 540 for NIfTI-2 or 348 for NIfTI-1.", sizeof_hdr) |
| /// Header size, must be 348 for a NIFTI-1 header and 540 for NIFTI-2. | ||
| /// There is no corresponding `set_sizeof_hdr()` because you should never | ||
| /// need to set this field manually. | ||
| pub fn get_sizeof_hdr(&self) -> u32 { |
There was a problem hiding this comment.
Yup, it's the C-GETTER guideline. Getters in Rust usually usually aren't prefixed with get_ by convention.
| pub fn get_sizeof_hdr(&self) -> u32 { | |
| pub fn sizeof_hdr(&self) -> u32 { |
| (self.pixdim[0].abs() - 1.).abs() < 1e-11 | ||
| impl Nifti2Header { | ||
| /// Place this `Nifti2Header` into a version-agnostic [`NiftiHeader`] enum. | ||
| pub fn into_nifti(self) -> NiftiHeader { |
There was a problem hiding this comment.
It would be idiomatic to also have implementations of From<Nifti1Header> and From<Nifti2Header> for NiftiHeader. This way, a conversion would be as simple as header.into().
| where | ||
| T: RealField, | ||
| f32: SubsetOf<T>, | ||
| f64: SubsetOf<T>, |
There was a problem hiding this comment.
This seems to suggest that all affine transformations will from now on use f64 for calculations, regardless of the nature of the original data. Volume data value transformations also seem to have changed accordingly. Not sure whether this breaks any expectations to existing consumers (@nilgoyette?).
| /// small, low precision types such as `u8` and `i16`. | ||
| #[derive(Debug)] | ||
| pub struct LinearTransformViaF32; | ||
| pub struct LinearTransformViaF32; // TODO remove this for NIFTI-2? |
There was a problem hiding this comment.
Were these changes merely a consequence of the wider nifti header? Perhaps by letting the API consumer tap into nifti-1, they could keep linear transformations in f32.
| } | ||
|
|
||
| #[test] | ||
| fn write_descrip_panic() { |
There was a problem hiding this comment.
It is no longer possible because descrip was turned back into an 80-byte array. It was kind of what I originally had in mind, but large arrays were not as nice to work with in Rust. That changed in recent versions, so unless we witness a significant performance regression, we can save the description in a [u8; 80] here instead of Vec<u8>.
|
Hi, NIFTI2 support would be very helpful! Can we add additional intent codes from the HCP pipeline? https://github.com/Washington-University/workbench/blob/master/src/FilesBase/nifti2.h#L33-L49 const int32_t NIFTI_INTENT_CONNECTIVITY_UNKNOWN=3000;
const int32_t NIFTI_INTENT_CONNECTIVITY_DENSE=3001;
const int32_t NIFTI_INTENT_CONNECTIVITY_DENSE_TIME=3002;//CIFTI-1 name
const int32_t NIFTI_INTENT_CONNECTIVITY_DENSE_SERIES=3002;//CIFTI-2 name
const int32_t NIFTI_INTENT_CONNECTIVITY_PARCELLATED=3003;
const int32_t NIFTI_INTENT_CONNECTIVITY_PARCELLATED_TIME=3004;//ditto
const int32_t NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SERIES=3004;
const int32_t NIFTI_INTENT_CONNECTIVITY_CONNECTIVITY_TRAJECTORY=3005;//ditto
const int32_t NIFTI_INTENT_CONNECTIVITY_DENSE_TRAJECTORY=3005;
const int32_t NIFTI_INTENT_CONNECTIVITY_DENSE_SCALARS=3006;
const int32_t NIFTI_INTENT_CONNECTIVITY_DENSE_LABELS=3007;
const int32_t NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SCALAR=3008;
const int32_t NIFTI_INTENT_CONNECTIVITY_PARCELLATED_DENSE=3009;
const int32_t NIFTI_INTENT_CONNECTIVITY_DENSE_PARCELLATED=3010;
const int32_t NIFTI_INTENT_CONNECTIVITY_PARCELLATED_PARCELLATED_SERIES=3011;
const int32_t NIFTI_INTENT_CONNECTIVITY_PARCELLATED_PARCELLATED_SCALAR=3012;However, several variables map to the same number which is not possible for rust enum. |
|
Apologies, I had a baby and kind of lost track of this! It looks like there is still broad interest in NIFTI-2 support, and aside from some formatting issues and the need for more test cases, the biggest obstacle is what to name getter methods. @nilgoyette is right that Rust eschews getter methods in general, preferring the simplicity and explicitness of accessing a struct's field directly. The current PR allows this by resolving the In such cases where getter/setter methods are needed, the Rust convention is to name the setter for the field Possibilities:
Thoughts? |
|
I also tried to implement nifti-2 support myself. I took a more aggressive approach and cast nifti-1 and nifti-2 header to a common type derived from nifti-2. The common type also exposes Enum types directly instead of through a getter. I believe connectome workbench took a similar approach. However, this will complete nuke the current interface. I also took a few test cases from nibabel, for nifti-2 unit tests. You can take a look my experimental branch here if you are interested. https://github.com/liningpan/nifti-rs/tree/nifti2 |
I don't take issue in introducing a breaking change to the API, especially for introducing such a big feature to the crate. Aside from this, I would also like to ensure that 1) it stays possible to work with a specific version explicitly, and that 2) calculations on NIfTI-1 can still use |
Adds support for the NIFTI-2 header format. The PR is just enough to compile and run tests. If approved, documentation and additional test cases will be needed in a subsequent PR. These changes are not backwards compatible.
Types and methods outside the header have been changed to default to the larger NIFTI-2 types. For example,
nifti::volume::shape::Dimis now based offu64instead ofu16.NiftiHeaderis changed to anenumthat contains either aNifti1HeaderorNifti2Headerstructure.NiftiHeaderimplements getter/setter methods to modify the underlying header without having to worry about which version it is. The underlying structure can also be accessed directly.Migration should otherwise be relatively painless.
NiftiHeader::from_file()andfrom_reader()automatically detect the header version in the given file or input stream.WriterOptions::write_nifti()automatically writes out whichever header version is referenced by theWriterOptions.NiftiHeader::into_nifti1()andinto_nifti2()conveniently convert between header versions.