diff --git a/README.rst b/README.rst index d72ba06..b19998d 100644 --- a/README.rst +++ b/README.rst @@ -1,215 +1,486 @@ -med2image -========= +med2image 2.6.6 +================== Quick Overview -------------- -- Convert DICOM or NIfTI to jpg or png +- Convert ``DICOM`` or ``NIfTI`` inputs to ``jpg`` or ``png`` outputs. Overview -------- -``med2image`` is a simple Python3 utility that converts medical image -formatted files to more visual friendly ones, such as png and jpg. +``med2image`` is a simple Python3 utility that converts medical image formatted files (such as ``DICOM`` and ``NifTI``) to more web friendly ones, such as ``png`` and ``jpg``. -Currently, NIfTI and DICOM input formats are understood, while any -graphical output type that is supported by matplotlib can be generated. +Currently, ``NIfTI`` and ``DICOM`` input formats are understood, while any graphical output type that is supported by matplotlib can be generated. + +At present ``med2image`` does not convert ``DICOM`` to ``NifTI``, but this is planned for a future release. Dependencies ------------ -Make sure that the following dependencies are installed on your host -system (or even better, a python3 virtual env): +Make sure that the following dependencies are installed on your host system (or even better, a ``python3`` virtual env): + +- ``pfmisc`` : (a general miscellaneous module for color support, etc) +- ``nibabel`` : (to read NIfTI files) +- ``pydicom`` : (to read DICOM files) +- ``matplotlib`` : (to save data in various image formats) +- ``pillow`` : (to save data in ``jpg`` format) -- ``nibabel`` (to read NIfTI files) -- ``pydicom`` (to read DICOM files) -- ``matplotlib`` (to save data in various image formats) -- ``pillow`` (to save data in jpg format) +Assumptions +----------- + +This document assumes UNIX conventions and a ``bash`` shell. The script should work fine under Windows, but we have not actively tested on that platform -- our dev envs are Linux Ubuntu and macOS. Installation ~~~~~~~~~~~~ -The best method of installing this script and all of its dependencies is -by fetching it from PyPI +Python module +~~~~~~~~~~~~~ + +One method of installing this script and all of its dependencies is by fetching it from `PyPI `_. .. code:: bash pip3 install med2image -Should you get an error about `python3-tk` not installed, simply do (for example on Ubuntu): +Should you get an error about ``python3-tk`` not installed, simply do (for example on Ubuntu): .. code:: bash sudo apt-get update sudo apt-get install -y python3-tk +Docker container +~~~~~~~~~~~~~~~~ -Command line arguments ----------------------- +We also offer a docker container of ``med2image`` as a ChRIS-conformant platform plugin here https://github.com/FNNDSC/pl-med2img (see also the closely related https://github.com/FNNDSC/pl-dcm2img that performs conversions down a directory tree recursively) -- please that reference for information on running the dockerized container. The containerized version exposes a similar CLI and functionality as this module. -:: +How to Use +---------- - -i|--inputFile - Input file to convert. Typically a DICOM file or a nifti volume. +``med2image`` needs at a minimum (some of) the following required command line arguments: - [-d|--outputDir ] - The directory to contain the converted output image files. +- ``-i | --inputFile `` : Input file to convert. Typically a ``DICOM`` file or a ``NifTI`` volume. - -o|--outputFileStem - The output file stem to store conversion. If this is specified - with an extension, this extension will be used to specify the - output file type. +- ``--inputFileSubStr `` : A short hand trick to specify the ``inputFile``. By only specifying a sub string that identifies the file, the first file in the ``inputDir`` that contains the sub string is tagged as the ``inputFile``. This saves a user from needing to specify long and cumbersome file names, esp in the case of many DICOM filenames. - SPECIAL CASES: - For DICOM data, the can be set to the value of - an internal DICOM tag. The tag is specified by preceding the tag - name with a percent character '%', so +- ``-d | --outputDir :`` The directory to contain the converted output image files. - -o %ProtocolName +**Example:** - will use the DICOM 'ProtocolName' to name the output file. Note - that special characters (like spaces) in the DICOM value are - replaced by underscores '_'. +.. code:: bash - Multiple tags can be specified, for example + # Convert a NifTI file 'vol.nii' to JPEG and store + # the results in a dirctory called 'out'. + # The 'out' dir will contain a set of JPEG + # images of form 'output-sliceXXX.jpg'. - -o %PatientName%PatientID%ProtocolName + med2image -i vol.nii -d out - and the output filename will have each DICOM tag string as - specified in order, connected with dashes. +.. code:: bash - A special %inputFile is available to specify the input file that - was read (without extension). + # Convert a DICOM file 'file.dcm' to JPEG and store + # the results in a dirctory called 'out'. + # The 'out' dir will contain a set of JPEG + # images of form 'output-sliceXXX.jpg'. - [-t|--outputFileType ] - The output file type. If different to extension, - will override extension in favour of . + # NOTE! If the directory containing 'file.dcm' contains + # multiple DICOM files, *ALL* of these will be converted + # to JPEG. See later for only converting a *single* + # DICOM file. - [-s|--sliceToConvert ] - In the case of volume files, the slice (z) index to convert. Ignored - for 2D input data. If a '-1' is sent, then convert *all* the slices. - If an 'm' is specified, only convert the middle slice in an input - volume. - - [-f|--frameToConvert ] - In the case of 4D volume files, the volume (V) containing the - slice (z) index to convert. Ignored for 3D input data. If a '-1' is - sent, then convert *all* the frames. If an 'm' is specified, only - convert the middle frame in the 4D input stack. + med2image -i file.dcm -d out - [--showSlices] - If specified, render/show image slices as they are created. +``NIfTI`` details +----------------- - [--reslice] - For 3D data only. Assuming [i,j,k] coordinates, the default is to save - along the 'k' direction. By passing a --reslice image data in the 'i' and - 'j' directions are also saved. Furthermore, the is subdivided into - 'slice' (k), 'row' (i), and 'col' (j) subdirectories. +**NOTE:** ``NifTI`` is typically a *volume* format. One ``NIfTI`` (``.nii``) volume contains multiple *slices*. Converting a ``NifTI`` volume results in multiple ``.jpg`` or ``.png`` results. - [-x|--man] - Show full help. +- ``NIfTI`` input data can be in 2 forms: - [-y|--synopsis] - Show brief help. + - 3D : The ``.nii`` volume contains multiple 2D slices + + - 4D : The ``.nii`` file contains multiple 3D volumes that each contain multiple 2D slices + +- ``med2image`` understands both types of inputs. + +Pull ``NIfTI`` +~~~~~~~~~~~~~~ + +The input should be a ``NIfTI`` volume with extension ``.nii``. We provide a sample volume here https://github.com/FNNDSC/SAG-anon-nii.git + +- Clone this repository (``SAG-anon-nii``) to your local computer. + +.. code:: bash -NIfTI conversion ----------------- + git clone https://github.com/FNNDSC/SAG-anon-nii.git -Both 3D and 4D NIfTI input data are understood. In the case of 4D NIfTI, -a specific frame can be specified in conjunction with a specific slice -index. In most cases, only a slice is required since most NIfTI data is -3D. Furthermore, all slices can be converted, or just the middle one. +Convert ``NIfTI`` +~~~~~~~~~~~~~~~~~ -Examples -~~~~~~~~ +**NOTE:** -All slices in a volume -~~~~~~~~~~~~~~~~~~~~~~ +- If ``--outputDir | -d`` is not provided, outputs are created in the *current* directory. -To convert all slices in an input NIfTI volume called vol.nii, to save -the results in a directory called out and to use as output the file stem -name image, do +- if ``--sliceToConvert`` is not provided, *all* the slices of the ``.nii`` volume are converted. -``med2image -i vol.nii -d out -o image.jpg -s -1`` +Both 3D and 4D ``NIfTI`` input data are understood. In the case of 4D ``NIfTI``, a specific frame (``--frameToConvert``) can be additionally provided in conjunction with a specific slice index. Conversion options include: + +- *all* slices (default) +- *middle* slice only, with the CLI ``--sliceToConvert m`` +- *someSpecificSlice*, with the CLI ``--sliceToConvert `` + +CASE 1: All slices in a volume +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Now, let's convert all slices in the input ``NIfTI`` volume ``SAG-anon.nii``, and save the results to a nested subdir ``nifti-results/all-slices``. We'll use as output file name stem ``sample`` and convert to ``jpg``. + +Assuming you have cloned the ``SAG-anon-nii`` repo and assuming that you have ``med2image`` on your UNIX shell path, + +.. code:: bash + + med2image -i SAG-anon-nii/SAG-anon.nii \ + -d nifti-results/all-slices \ + -o sample.jpg -s -1 or equivalently and more verbosely, -:: +.. code:: bash - med2image --inputFile vol.nii --outputDir out \ - --outputFileStem image --outputFileType jpg \ + med2image --inputFile SAG-anon-nii/SAG-anon.nii \ + --outputDir nifti-results/all-slices \ + --outputFileStem sample --outputFileType jpg \ --sliceToConvert -1 -This will create the following files in out +resulting in :: - image-slice000.jpg - image-slice001.jpg - image-slice002.jpg - image-slice003.jpg - image-slice004.jpg - image-slice005.jpg - image-slice006.jpg - image-slice007.jpg + nifti-results/all-slices/sample-slice000.jpg + nifti-results/all-slices/sample-slice001.jpg + nifti-results/all-slices/sample-slice002.jpg + nifti-results/all-slices/sample-slice003.jpg ... - image-slice049.jpg - image-slice050.jpg - image-slice051.jpg - image-slice052.jpg - image-slice053.jpg + nifti-results/all-slices/sample-slice188.jpg + nifti-results/all-slices/sample-slice189.jpg + nifti-results/all-slices/sample-slice190.jpg + nifti-results/all-slices/sample-slice191.jpg + +Note that even if the nested output directory structure does not exist, ``med2image`` will create it for you. -Convert only a single slice -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Case 2: Convert only a single slice +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Often times, you might only want to convert the "middle" slice in a volume (for example to generate a representative thumbnail of the volume). To do this, simply specify an ``m`` to ``--sliceToConvert`` (or ``-s m``): + +.. code:: bash -Mostly, you'll probably only want to convert the "middle" slice in a -volume (for example to generate a representative thumbnail of the -volume). To do this, simply specify a m to --sliceToConvert + med2image -i SAG-anon-nii/SAG-anon.nii \ + -d nifti-results/middle-slice \ + -o sample --outputFileType jpg \ + --sliceToConvert m -``med2image -i input.nii -o input.jpg -s m`` +resulting in -or, again, slightly more verbosely and with an outputDirectory specifier +:: -``med2image -i input.nii -d out -o vol --outputFileType jpg --sliceToConvert m`` + nifti-results/middle-slice/sample-slice096.jpg Alternatively a specific slice index can be converted. Use -``med2image -i input.nii -d out -o vol --outputFileType jpg --sliceToConvert 20`` +.. code:: bash + + med2image -i SAG-anon-nii/SAG-anon.nii \ + -d nifti-results/specific-slice \ + -o sample \ + --outputFileType jpg \ + --sliceToConvert 20 to convert only the 20th slice of the volume. -DICOM conversion ----------------- +resulting in + +:: + + nifti-results/specific-slice/sample-slice020.jpg + +``DICOM`` +--------- + +**NOTE:** One ``DICOM`` (``.dcm``) file typically corresponds to one ``.png`` or ``.jpg`` file (slice). + +Pull DICOM +~~~~~~~~~~ + +The input should be a ``DICOM`` file usually with extension ``.dcm`` + +We provide a sample directory of ``.dcm`` images here ``FNNDSC/SAG-anon``. (https://github.com/FNNDSC/SAG-anon.git) + +- Clone this repository (``SAG-anon``) to your local computer. + +.. code:: bash + + git clone https://github.com/FNNDSC/SAG-anon.git -Convert a single DICOM file -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Convert ``DICOM`` +~~~~~~~~~~~~~~~~~ -To convert a single DICOM file called slice.dcm to slice.jpg, do: +**NOTE:** -``med2image -i slice.dcm -o slice.jpg`` +- If ``--outputDir | -d`` is not provided, any output(s) are created in the current directory. +- if ``--sliceToConvert`` argument is not specified and if mutiple ``dcm`` files are contained in the input directory with the ``DICOM`` input, then all the ``.dcm`` files are converted. +- alternatively, specifying a ``--convertOnlySingleDICOM`` will only convert the DICOM file specified with the ``--inputFile`` flag. -which will create a single file, slice.jpg in the current directory. Convert all DICOMS in a directory/series -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To convert all the ``DICOM`` files in a directory, simply specify either ``--sliceToConvert -1`` (or just leave out the argument/value pair completely): + +.. code:: bash + + med2image -i SAG-anon/0001-1.3.12.2.1107.5.2.19.45152.2013030808110258929186035.dcm \ + -d dicom-results/all-slices \ + -o sample \ + --outputFileType jpg \ + --sliceToConvert -1 + + # OR equivalently + + med2image -i SAG-anon/0001-1.3.12.2.1107.5.2.19.45152.2013030808110258929186035.dcm \ + -d dicom-results/all-slices \ + -o sample \ + --outputFileType jpg + + +resulting in + +:: + + dicom-results/all-slices/sample-slice000.jpg + dicom-results/all-slices/sample-slice001.jpg + dicom-results/all-slices/sample-slice002.jpg + dicom-results/all-slices/sample-slice003.jpg + ... + dicom-results/all-slices/sample-slice188.jpg + dicom-results/all-slices/sample-slice189.jpg + dicom-results/all-slices/sample-slice190.jpg + dicom-results/all-slices/sample-slice191.jpg + +Convert a single ``DICOM`` file +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Mostly, you'll probably only want to convert the "middle" slice in a DICOM directory (for example to generate a representative thumbnail of the directory). To do this, simply specify a `m` to --sliceToConvert (or `-s m`) + +.. code:: bash + + med2image -i SAG-anon/0001-1.3.12.2.1107.5.2.19.45152.2013030808110258929186035.dcm \ + -d dicom-results/middle-slice \ + -o sample --outputFileType jpg \ + --sliceToConvert m + +resulting in + +:: + + dicom-results/middle-slice/sample.jpg + +Note that even though the first slice in the ``SAG-anon`` directory was supplied to the script, ``med2image`` nonetheless found and converted the middle slice in the directory. + +Alternatively a specific slice index can be converted. Use + +.. code:: bash + + med2image -i SAG-anon/0001-1.3.12.2.1107.5.2.19.45152.2013030808110258929186035.dcm \ + -d dicom-results/specific-slice \ + -o sample --outputFileType jpg \ + --sliceToConvert 20 + +resulting in + +:: + + dicom-results/specific-slice/sample.jpg + +Again, even though the first slice was supplied to the script, ``med2image`` selected and converted the 20th slice in the directory. + +Special Cases +^^^^^^^^^^^^^ + +For ``DICOM`` data, the ```` can optionally be set to the value of an internal ``DICOM`` tag. The tag is specified by preceding the tag name with a percent character ``%``, so + +- ``-o %PatientID`` + +will use the ``DICOM`` ``PatientID`` to name the output file. Note that special characters (like spaces) in the ``DICOM`` value are replaced by underscores '_'. + +.. code:: bash + + med2image -i SAG-anon/0001-1.3.12.2.1107.5.2.19.45152.2013030808110258929186035.dcm \ + -d dicom-results/tags \ + -o %PatientID.jpg -s m + +This will create the following file in the ``tags`` sub-directory within ``dicom-results`` directory. + +.. code:: bash + + dicom-results/tags/1449c1d.jpg + +Multiple tags can be specified, for example + +- ``-o %PatientName%PatientID%ProtocolName`` + +and the output filename will have each ``DICOM`` tag string as specified in order, connected with dashes. + +.. code:: bash + + med2image -i SAG-anon/0001-1.3.12.2.1107.5.2.19.45152.2013030808110258929186035.dcm \ + -d dicom-results/tags \ + -o %PatientName%PatientID%ProtocolName.jpg \ + -s m -To convert all the DICOMS in a directory, simply specifiy a '-1' to the -sliceIndex: +This will create the following file in the ``tags`` sub-directory within ``dicom-results`` directory. -``med2image -i inputDir/slice.dcm -d outputDir -o slice.jpg -s -1`` +.. code:: bash + + dicom-results/tags/anonymized-1449c1d-SAG_MPRAGE_220_FOV.jpg -Note that this assumes all the DICOM files in the directory inputDir -belong to the same series. Multiple Direction Reslicing ---------------------------- -By default, only the slice (or slices) in the acquisition direction are -converted. However, by passing a -r to the script, all dimensions are -converted. Since the script does not know the anatomical orientation of -the image, the directions are simply labeled x, y, and z. +By default, only the slice (or slices) in the acquisition direction are converted. However, by passing a `--reslice` to the script, all dimensions are converted. Since the script does not know the anatomical orientation of the image, the directions are simply labeled ``x``, ``y``, and ``z``. + +The ``z`` direction is the original acquistion (slice) direction, while ``x`` and ``y`` correspond to planes normal to the row and column directions. Converted images are stored in subdirectories labeled ``x``, ``y``, and ``z``. + +No interpolation in the ``x`` and ``y`` directions is performed. This often results in ugly images! + +**NOTE:** In case of ``DICOM`` images, the `--reslice` option will work only if all slices in the directory are converted, i.e. converting with ``--sliceToConvert -1`` + +Special Operations +------------------ + +``med2image`` also supports some very basic image processing through a ``--func `` CLI, which applies some canned transformation on the image. Currently supported is + +:: + + --func invertIntensities + +which simply inverts the contrast intensity of the source image. Additional functions are planned for future releases. + +Command Line Arguments +---------------------- + +:: + + [-i|--inputFile ] + Input file to convert. Typically a DICOM file or a nifti volume. + + [--inputFileSubStr ] + As a convenience, the input file can be determined via a substring + search of all the files in the using this flag. The first + filename hit that contains the will be assigned the + . + + This flag is useful is input names are long and cumbersome, but + a short substring search would identify the file. For example, an + input file of + + 0043-1.3.12.2.1107.5.2.19.45152.2013030808110149471485951.dcm + + can be specified using ``--inputFileSubStr 0043-`` + + [-I|--inputDir ] + If specified, a directory containing the . In this case + should be specified as relative to . + + [-d|--outputDir ] + The directory to contain the converted output image files. + + -o|--outputFileStem + The output file stem to store conversion. If this is specified + with an extension, this extension will be used to specify the + output file type. + + SPECIAL CASES: + For DICOM data, the can be set to the value of + an internal DICOM tag. The tag is specified by preceding the tag + name with a percent character '%', so + + -o %ProtocolName + + will use the DICOM 'ProtocolName' to name the output file. Note + that special characters (like spaces) in the DICOM value are + replaced by underscores '_'. + + Multiple tags can be specified, for example + + -o %PatientName%PatientID%ProtocolName + + and the output filename will have each DICOM tag string as + specified in order, connected with dashes. + + [--convertOnlySingleDICOM] + If specified, will only convert the single DICOM specified by the + '--inputFile' flag. This is useful for the case when an input + directory has many DICOMS but you specifially only want to convert + the named file. By default the script assumes that multiple DICOMS + should be converted en mass otherwise. + + [--preserveDICOMinputName] + If specified, use the input DICOM name as the stem of the output + filename, with the specified type ('jpg' or 'png') as the extension. + In the case where [--reslice] is additionally specified, only the + slice or 'z' direction will preserve original DICOM names. + + [-t|--outputFileType ] + The output file type. If different to extension, + will override extension in favour of . + + [-s|--sliceToConvert ] + In the case of volume files, the slice (z) index to convert. Ignored + for 2D input data. If a '-1' is sent, then convert *all* the slices. + If an 'm' is specified, only convert the middle slice in an input + volume. + + [-f|--frameToConvert ] + In the case of 4D volume files, the volume (V) containing the + slice (z) index to convert. Ignored for 3D input data. If a '-1' is + sent, then convert *all* the frames. If an 'm' is specified, only + convert the middle frame in the 4D input stack. + + [--showSlices] + If specified, render/show image slices as they are created. + + [--rot <3DbinVector>] + A per dimension binary rotation vector. Useful to rotate individual + dimensions by an angle specified with [--rotAngle ]. Default + is '110', i.e. rotate 'x' and 'y' but not 'z'. Note that for a + non-reslice selection, only the 'z' (or third) element of the vector + is used. + + [--rotAngle ] + Default 90 -- the rotation angle to apply to a given dimension of the + <3DbinVector>. -The z direction is the original acquistion (slice) direction, while x -and y correspond to planes normal to the row and column directions. + [--func ] + Apply the specified transformation function before saving. Currently + support functions: + + * invertIntensities + Inverts the contrast intensity of the source image. + + [--reslice] + For 3D data only. Assuming [x,y,z] coordinates, the default is to save + along the 'z' direction. By passing a --reslice image data in the 'x' + and 'y' directions are also saved. Furthermore, the is + subdivided into 'slice' (z), 'row' (x), and 'col' (y) subdirectories. + + [-x|--man] + Show full help. + + [-y|--synopsis] + Show brief help. -Converted images are stored in subdirectories labeled x, y, and z. + [--verbosity ] + Control how chatty med2image is. Set to '0' for blissful silence, '1' + for sane progress and '3' for full information. diff --git a/bin/med2image b/bin/med2image index 0711cc2..7a50504 100755 --- a/bin/med2image +++ b/bin/med2image @@ -21,19 +21,17 @@ from pfmisc._colors import Colors import pudb -str_version = "2.0.1" -#import pdb; pdb.set_trace() - +str_version = "2.6.6" str_desc = Colors.CYAN + """ - _ _____ _ - | |/ __ \(_) - _ __ ___ ___ __| |`' / /' _ _ __ ___ __ _ __ _ ___ + _ _____ _ + | |/ __ \(_) + _ __ ___ ___ __| |`' / /' _ _ __ ___ __ _ __ _ ___ | '_ ` _ \ / _ \/ _` | / / | | '_ ` _ \ / _` |/ _` |/ _ \\ | | | | | | __/ (_| |./ /___| | | | | | | (_| | (_| | __/ |_| |_| |_|\___|\__,_|\_____/|_|_| |_| |_|\__,_|\__, |\___| - __/ | - |___/ + __/ | + |___/ med(ical image)2image @@ -55,26 +53,33 @@ def synopsis(ab_shortOnly=False): shortSynopsis = ''' NAME - med2image.py - convert medical images to jpg/png/etc. + med2image.py - convert medical images to jpg/png/etc. SYNOPSIS %s \\ - -i|--input \\ + [-i|--input ] \\ + [--inputFileSubStr ] \\ + [-I|--inputDir ] \\ [-d|--outputDir ] \\ -o|--output \\ [-t|--outputFileType ] \\ [-s|--sliceToConvert ] \\ + [--convertOnlySingleDICOM] \\ + [--preserveDICOMinputName] \\ [-f|--frameToConvert ] \\ [--showSlices] \\ [--func ] \\ [--reslice] \\ + [--rotAngle ] \\ + [--rot <3vec>] \\ [-x|--man] \\ - [-y|--synopsis] + [-y|--synopsis] \\ + [--verbosity ] BRIEF EXAMPLE - med2image.py -i slice.dcm -o slice.jpg + med2image.py -i slice.dcm -o slice.jpg ''' % scriptName @@ -88,9 +93,27 @@ def synopsis(ab_shortOnly=False): ARGS - -i|--inputFile + [-i|--inputFile ] Input file to convert. Typically a DICOM file or a nifti volume. + [--inputFileSubStr ] + As a convenience, the input file can be determined via a substring + search of all the files in the using this flag. The first + filename hit that contains the will be assigned the + . + + This flag is useful if input names are long and cumbersome, but + a short substring search would identify the file. For example, an + input file of + + 0043-1.3.12.2.1107.5.2.19.45152.2013030808110149471485951.dcm + + can be specified using ``--inputFileSubStr 0043-`` + + [-I|--inputDir ] + If specified, a directory containing the . In this case + should be specified as relative to . + [-d|--outputDir ] The directory to contain the converted output image files. @@ -102,23 +125,33 @@ def synopsis(ab_shortOnly=False): SPECIAL CASES: For DICOM data, the can be set to the value of an internal DICOM tag. The tag is specified by preceding the tag - name with a percent character '%%', so + name with a percent character '%%', so -o %%ProtocolName will use the DICOM 'ProtocolName' to name the output file. Note - that special characters (like spaces) in the DICOM value are + that special characters (like spaces) in the DICOM value are replaced by underscores '_'. Multiple tags can be specified, for example -o %%PatientName%%PatientID%%ProtocolName - and the output filename will have each DICOM tag string as + and the output filename will have each DICOM tag string as specified in order, connected with dashes. - A special %%inputFile is available to specify the input file that - was read (without extension). + [--convertOnlySingleDICOM] + If specified, will only convert the single DICOM specified by the + '--inputFile' flag. This is useful for the case when an input + directory has many DICOMS but you specifially only want to convert + the named file. By default the script assumes that multiple DICOMS + should be converted en mass otherwise. + + [--preserveDICOMinputName] + If specified, use the input DICOM name as the stem of the output + filename, with the specified type ('jpg' or 'png') as the extension. + In the case where [--reslice] is additionally specified, only the + slice or 'z' direction will preserve original DICOM names. [-t|--outputFileType ] The output file type. If different to extension, @@ -138,19 +171,30 @@ def synopsis(ab_shortOnly=False): [--showSlices] If specified, render/show image slices as they are created. - + + [--rot <3DbinVector>] + A per dimension binary rotation vector. Useful to rotate individual + dimensions by an angle specified with [--rotAngle ]. Default + is '110', i.e. rotate 'x' and 'y' but not 'z'. Note that for a + non-reslice selection, only the 'z' (or third) element of the vector + is used. + + [--rotAngle ] + Default 90 -- the rotation angle to apply to a given dimension of the + <3DbinVector>. + [--func ] - Apply the specified transformation function before saving. Currently + Apply the specified transformation function before saving. Currently support functions: * invertIntensities Inverts the contrast intensity of the source image. [--reslice] - For 3D data only. Assuming [i,j,k] coordinates, the default is to save - along the 'k' direction. By passing a --reslice image data in the 'i' and - 'j' directions are also saved. Furthermore, the is subdivided into - 'slice' (k), 'row' (i), and 'col' (j) subdirectories. + For 3D data only. Assuming [x,y,z] coordinates, the default is to save + along the 'z' direction. By passing a --reslice image data in the 'x' + and 'y' directions are also saved. Furthermore, the is + subdivided into 'slice' (z), 'row' (x), and 'col' (y) subdirectories. [-x|--man] Show full help. @@ -158,6 +202,10 @@ def synopsis(ab_shortOnly=False): [-y|--synopsis] Show brief help. + [--verbosity ] + Control how chatty med2image is. Set to '0' for blissful silence, '1' + for sane progress and '3' for full information. + EXAMPLES NIfTI @@ -165,22 +213,22 @@ def synopsis(ab_shortOnly=False): o Convert each slice in a NIfTI volume 'vol.nii' to a jpg called 'image-sliceXXX.jpg' and store results in a directory called 'out': - med2image -i vol.nii -d out -o image.jpg -s -1 + med2image -i vol.nii -d out -o image.jpg -s -1 o Convert only the middle slice in an input volume and store in current directory: - med2image -i vol.nii -o image.jpg -s m + med2image -i vol.nii -o image.jpg -s m o Convert a specific slice, i.e. slice 20 - med2image -i vol.nii -o image.jpg -s 20 + med2image -i vol.nii -o image.jpg -s 20 DICOM o Simply convert a DICOM file called 'slice.dcm' to a jpg called 'slice.jpg': - med2image -i slice.dcm -o slice.jpg + med2image -i slice.dcm -o slice.jpg o Convert all DICOMs in a directory. Note that is assumes all DICOM files in the directory containing the passed file belong to the same series. @@ -206,7 +254,12 @@ parser = ArgumentParser(description = str_desc, formatter_class = RawTextHelpFo parser.add_argument("-i", "--inputFile", help = "input file", - dest = 'inputFile') + dest = 'inputFile', + default = '') +parser.add_argument("--inputFileSubStr", + help = "input file substring", + dest = 'inputFileSubStr', + default = '') parser.add_argument("-I", "--inputDir", help = "input directory", dest = 'inputDir', @@ -223,6 +276,16 @@ parser.add_argument("-t", "--outputFileType", help = "output image type", dest = 'outputFileType', default = '') +parser.add_argument("--convertOnlySingleDICOM", + help = "if specified, only convert the specific input DICOM", + dest = 'convertOnlySingleDICOM', + action = 'store_true', + default = False) +parser.add_argument("--preserveDICOMinputName", + help = "if specified, save output files with the basename of their input DICOM", + dest = 'preserveDICOMinputName', + action = 'store_true', + default = False) parser.add_argument("-s", "--sliceToConvert", help="slice to convert (for 3D data)", dest='sliceToConvert', @@ -250,6 +313,18 @@ parser.add_argument('--func', help = "apply transformation function before saving", dest = 'func', default = "") +parser.add_argument('--verbosity', + help = "verbosity level for app", + dest = 'verbosity', + default = "1") +parser.add_argument('--rot', + help = "3D slice/dimenstion rotation vector", + dest = 'rot', + default = "110") +parser.add_argument('--rotAngle', + help = "3D slice/dimenstion rotation angle", + dest = 'rotAngle', + default = "90") parser.add_argument("-x", "--man", help = "man", dest = 'man', @@ -270,6 +345,7 @@ parser.add_argument('-v', '--version', # parse passed arguments args = parser.parse_args() +# Do some minor CLI checks if args.b_version: print("Version: %s" % str_version) sys.exit(1) @@ -283,15 +359,16 @@ if args.man or args.synopsis: print(str_help) sys.exit(1) +# Create the object imgConverter = med2image.object_factoryCreate(args).C_convert -if args.func: - imgConverter.func = args.func - -# pudb.set_trace() -# And now run it! -imgConverter.tic() -imgConverter.run() -if args.printElapsedTime: print("Elapsed time = %f seconds" % imgConverter.toc()) -sys.exit(0) +# and if it's valid... +if imgConverter: + # run it! + imgConverter.tic() + imgConverter.run() + if args.printElapsedTime: print("Elapsed time = %f seconds" % imgConverter.toc()) + sys.exit(0) +else: + sys.exit(1) diff --git a/med2image/med2image.py b/med2image/med2image.py index 4b1b02b..9c3474d 100755 --- a/med2image/med2image.py +++ b/med2image/med2image.py @@ -2,27 +2,25 @@ # System imports import os +from os import listdir +from os import walk +from os.path import isfile, join +from pathlib import Path + import sys import glob import numpy as np import re import time -# import pudb - +import pudb +from scipy import ndimage # System dependency imports -import nibabel as nib -import pydicom as dicom -import pylab -import matplotlib.cm as cm - -# # Project specific imports -# from . import error -# from . import message as msg -# from . import systemMisc as misc +import nibabel as nib +import pydicom as dicom +import pylab +import matplotlib.cm as cm import pfmisc - -# pfurl local dependencies from pfmisc._colors import Colors from pfmisc.message import Message @@ -34,8 +32,10 @@ def report( callingClass, ): ''' Error handling. + Based on the , error information is extracted from _dictErr and sent to log object. + If is False, error is considered non-fatal and processing can continue, otherwise processing terminates. ''' @@ -96,41 +96,45 @@ class med2image(object): _dictErr = { 'inputFileFail' : { - 'action' : 'trying to read input file, ', - 'error' : 'could not access/read file -- does it exist? Do you have permission?', - 'exitCode' : 10}, + 'action' : 'trying to read input file, ', + 'error' : 'could not access/read file -- does it exist? Do you have permission?', + 'exitCode' : 10}, 'emailFail' : { - 'action' : 'attempting to send notification email, ', - 'error' : 'sending failed. Perhaps host is not email configured?', - 'exitCode' : 20}, + 'action' : 'attempting to send notification email, ', + 'error' : 'sending failed. Perhaps host is not email configured?', + 'exitCode' : 20}, 'dcmInsertionFail': { - 'action' : 'attempting insert DICOM into volume structure, ', - 'error' : 'a dimension mismatch occurred. This DICOM file is of different image size to the rest.', - 'exitCode' : 20}, + 'action' : 'attempting insert DICOM into volume structure, ', + 'error' : 'a dimension mismatch occurred. This DICOM file is of different image size to the rest.', + 'exitCode' : 30}, 'ProtocolNameTag': { - 'action' : 'attempting to parse DICOM header, ', - 'error' : 'the DICOM file does not seem to contain a ProtocolName tag.', - 'exitCode' : 30}, - 'PatientNameTag': { - 'action': 'attempting to parse DICOM header, ', - 'error': 'the DICOM file does not seem to contain a PatientName tag.', - 'exitCode': 30}, + 'action' : 'attempting to parse DICOM header, ', + 'error' : 'the DICOM file does not seem to contain a ProtocolName tag.', + 'exitCode' : 40}, + 'PatientNameTag': { + 'action': 'attempting to parse DICOM header, ', + 'error': 'the DICOM file does not seem to contain a PatientName tag.', + 'exitCode': 41}, 'PatientAgeTag': { - 'action': 'attempting to parse DICOM header, ', - 'error': 'the DICOM file does not seem to contain a PatientAge tag.', - 'exitCode': 30}, + 'action': ' attempting to parse DICOM header, ', + 'error': 'the DICOM file does not seem to contain a PatientAge tag.', + 'exitCode': 42}, 'PatientNameSex': { - 'action': 'attempting to parse DICOM header, ', - 'error': 'the DICOM file does not seem to contain a PatientSex tag.', - 'exitCode': 30}, + 'action': 'attempting to parse DICOM header, ', + 'error': 'the DICOM file does not seem to contain a PatientSex tag.', + 'exitCode': 43}, 'PatientIDTag': { - 'action': 'attempting to parse DICOM header, ', - 'error': 'the DICOM file does not seem to contain a PatientID tag.', - 'exitCode': 30}, + 'action': 'attempting to parse DICOM header, ', + 'error': 'the DICOM file does not seem to contain a PatientID tag.', + 'exitCode': 44}, 'SeriesDescriptionTag': { - 'action': 'attempting to parse DICOM header, ', - 'error': 'the DICOM file does not seem to contain a SeriesDescription tag.', - 'exitCode': 30} + 'action': 'attempting to parse DICOM header, ', + 'error': 'the DICOM file does not seem to contain a SeriesDescription tag.', + 'exitCode': 45}, + 'PatientSexTag': { + 'action': 'attempting to parse DICOM header, ', + 'error': 'the DICOM file does not seem to contain a PatientSex tag.', + 'exitCode': 46} } @staticmethod @@ -147,11 +151,12 @@ def mkdir(newdir, mode=0x775): raise OSError("a file with the same name as the desired " \ "dir, '%s', already exists." % newdir) else: - head, tail = os.path.split(newdir) - if head and not os.path.isdir(head): - os.mkdir(head) - if tail: - os.mkdir(newdir) + Path(newdir).mkdir(parents = True, exist_ok = True) + # head, tail = os.path.split(newdir) + # if head and not os.path.isdir(head): + # os.mkdirs(head, exist_ok=True) + # if tail: + # os.mkdirs(newdir, exist_ok=True) def log(self, *args): ''' @@ -186,6 +191,7 @@ def description(self, *args): @staticmethod def urlify(astr, astr_join = '_'): # Remove all non-word characters (everything except numbers and letters) + # pudb.set_trace() astr = re.sub(r"[^\w\s]", '', astr) # Replace all runs of whitespace with an underscore @@ -198,27 +204,29 @@ def __init__(self, **kwargs): # # Object desc block # - self.str_desc = '' - # self._log = msg.Message() - # self._log._b_syslog = True - self.__name__ = "med2image" + self.str_desc = '' + # self._log = msg.Message() + # self._log._b_syslog = True + self.__name__ = "med2image" # Directory and filenames - self.str_workingDir = '' - self.str_inputFile = '' - self.str_outputFileStem = '' - self.str_outputFileType = '' - self.str_outputDir = '' - self.str_inputDir = '' + self.str_workingDir = '' + self.str_inputFile = '' + self.lstr_inputFile = [] + self.str_inputFileSubStr = '' + self.str_outputFileStem = '' + self.str_outputFileType = '' + self.str_outputDir = '' + self.str_inputDir = '' self._b_convertAllSlices = False - self.str_sliceToConvert = '' - self.str_frameToConvert = '' + self.str_sliceToConvert = '' + self.str_frameToConvert = '' self._sliceToConvert = -1 self._frameToConvert = -1 - self.str_stdout = "" - self.str_stderr = "" + self.str_stdout = "" + self.str_stderr = "" self._exitCode = 0 # The actual data volume and slice @@ -226,6 +234,8 @@ def __init__(self, **kwargs): self._b_4D = False self._b_3D = False self._b_DICOM = False + self.convertOnlySingleDICOM = False + self.preserveDICOMinputName = False self._Vnp_4DVol = None self._Vnp_3DVol = None self._Mnp_2Dslice = None @@ -234,34 +244,39 @@ def __init__(self, **kwargs): self.verbosity = 1 - # A logger - # self._log = msg.Message() - # self._log.syslog(True) - - self.dp = pfmisc.debug( - verbosity = self.verbosity, - within = self.__name__ - ) - - # Flags self._b_showSlices = False self._b_convertMiddleSlice = False self._b_convertMiddleFrame = False self._b_reslice = False - self.func = None #transformation function + self.func = None # transformation function + self.rot = '110' + self.rotAngle = 90 for key, value in kwargs.items(): - if key == "inputFile": self.str_inputFile = value - if key == "inputDir": self.str_inputDir = value - if key == "outputDir": self.str_outputDir = value - if key == "outputFileStem": self.str_outputFileStem = value - if key == "outputFileType": self.str_outputFileType = value - if key == "sliceToConvert": self.str_sliceToConvert = value - if key == "frameToConvert": self.str_frameToConvert = value - if key == "showSlices": self._b_showSlices = value - if key == 'reslice': self._b_reslice = value - if key == "func": self.func = value + if key == "inputFile": self.str_inputFile = value + if key == "inputFileSubStr": self.str_inputFileSubStr = value + if key == "inputDir": self.str_inputDir = value + if key == "outputDir": self.str_outputDir = value + if key == "outputFileStem": self.str_outputFileStem = value + if key == "outputFileType": self.str_outputFileType = value + if key == "sliceToConvert": self.str_sliceToConvert = value + if key == "frameToConvert": self.str_frameToConvert = value + if key == "convertOnlySingleDICOM": self.convertOnlySingleDICOM = value + if key == "preserveDICOMinputName": self.preserveDICOMinputName = value + if key == "showSlices": self._b_showSlices = value + if key == 'reslice': self._b_reslice = value + if key == "func": self.func = value + if key == "verbosity": self.verbosity = int(value) + if key == "rot": self.rot = value + if key == "rotAngle": self.rotAngle = int(value) + + # A logger + self.dp = pfmisc.debug( + verbosity = self.verbosity, + within = self.__name__ + ) + self.LOG = self.dp.qprint if self.str_frameToConvert.lower() == 'm': self._b_convertMiddleFrame = True @@ -286,7 +301,7 @@ def __init__(self, **kwargs): self.str_outputFileType = str_fileExtension if not len(self.str_outputFileType) and not len(str_fileExtension): - self.str_outputFileType = '.png' + self.str_outputFileType = 'png' def tic(self): """ @@ -368,17 +383,19 @@ def get_output_file_name(self, **kwargs): frame, index, self.str_outputFileType) else: - str_outputFile = '%s/%s/%s-slice%03d.%s' % ( + if self.preserveDICOMinputName and (str_subDir == 'z' or str_subDir == ''): + str_filePart = os.path.splitext(self.lstr_inputFile[index])[0] + else: + str_filePart = '%s-slice%03d' % (self.str_outputFileStem, index) + str_outputFile = '%s/%s/%s.%s' % ( self.str_outputDir, str_subDir, - self.str_outputFileStem, - index, + str_filePart, self.str_outputFileType) return str_outputFile def dim_save(self, **kwargs): dims = self._Vnp_3DVol.shape - self.dp.qprint('Image volume logical (i, j, k) size: %s' % str(dims)) str_dim = 'z' b_makeSubDir = False b_rot90 = False @@ -401,7 +418,7 @@ def dim_save(self, **kwargs): dim_ix = {'x':0, 'y':1, 'z':2} if indexStart == 0 and indexStop == -1: indexStop = dims[dim_ix[str_dim]] - + self.LOG('Saving along "%s" dimension with %i degree rotation...' % (str_dim, self.rotAngle*b_rot90)) for i in range(indexStart, indexStop): if str_dim == 'x': self._Mnp_2Dslice = self._Vnp_3DVol[i, :, :] @@ -414,26 +431,32 @@ def dim_save(self, **kwargs): if str_outputFile.endswith('dcm'): self._dcm = self._dcmList[i] self.slice_save(str_outputFile) + self.LOG('%d images saved along "%s" dimension' % ((i+1), str_dim), + end = '') + if self.func: + self.LOG(" with '%s' function applied." % self.func, + syslog = False) + else: + self.LOG(".", syslog = False) - def process_slice(self, b_rot90=None): + def process_slice(self, b_rot90 = False): ''' Processes a single slice. ''' if b_rot90: - self._Mnp_2Dslice = np.rot90(self._Mnp_2Dslice) + self._Mnp_2Dslice = ndimage.rotate(self._Mnp_2Dslice, self.rotAngle) if self.func == 'invertIntensities': self.invert_slice_intensities() def slice_save(self, astr_outputFile): ''' - Saves a single slice. - ARGS o astr_output - The output filename to save the slice to. + The output filename. ''' - self.dp.qprint('Outputfile = %s' % astr_outputFile) + self.LOG('Input file = %s' % self.str_inputFile, level = 3) + self.LOG('Outputfile = %s' % astr_outputFile, level = 3) fformat = astr_outputFile.split('.')[-1] if fformat == 'dcm': if self._dcm: @@ -468,30 +491,32 @@ def __init__(self, **kwargs): self.l_dcmFileNames = sorted(glob.glob('%s/*.dcm' % self.str_inputDir)) self.slices = len(self.l_dcmFileNames) - - if self._b_convertMiddleSlice: - self._sliceToConvert = int(self.slices/2) - self._dcm = dicom.read_file(self.l_dcmFileNames[self._sliceToConvert],force=True) - self.str_inputFile = self.l_dcmFileNames[self._sliceToConvert] - - # if not self.str_outputFileStem.startswith('%'): - # self.str_outputFileStem, ext = os.path.splitext(self.l_dcmFileNames[self._sliceToConvert]) - if not self._b_convertMiddleSlice and self._sliceToConvert != -1: - self._dcm = dicom.read_file(self.l_dcmFileNames[self._sliceToConvert],force=True) - self.str_inputFile = self.l_dcmFileNames[self._sliceToConvert] + if self._sliceToConvert != -1 or self.convertOnlySingleDICOM: + if self._b_convertMiddleSlice: + self._sliceToConvert = int(self.slices/2) + self._dcm = dicom.read_file(self.l_dcmFileNames[self._sliceToConvert],force=True) + self.str_inputFile = self.l_dcmFileNames[self._sliceToConvert] + if not self._b_convertMiddleSlice and self._sliceToConvert != -1: + self._dcm = dicom.read_file(self.l_dcmFileNames[self._sliceToConvert],force=True) + self.str_inputFile = self.l_dcmFileNames[self._sliceToConvert] + else: + self._dcm = dicom.read_file(self.str_inputFile,force=True) + if self.convertOnlySingleDICOM: + self._sliceToConvert = 1 + self._dcm = dicom.read_file(self.str_inputFile,force=True) + self.lstr_inputFile.append(os.path.basename(self.str_inputFile)) else: - self._dcm = dicom.read_file(self.str_inputFile,force=True) - if self._sliceToConvert == -1: - self._b_3D = True - self._dcm = dicom.read_file(self.str_inputFile,force=True) - image = self._dcm.pixel_array - shape2D = image.shape + self._b_3D = True + self._dcm = dicom.read_file(self.str_inputFile,force=True) + image = self._dcm.pixel_array + shape2D = image.shape #print(shape2D) - self._Vnp_3DVol = np.empty( (shape2D[0], shape2D[1], self.slices) ) - i = 0 + self._Vnp_3DVol = np.empty( (shape2D[0], shape2D[1], self.slices) ) + i = 0 for img in self.l_dcmFileNames: - self._dcm = dicom.read_file(img,force=True) - image = self._dcm.pixel_array + self._dcm = dicom.read_file(img,force=True) + self.lstr_inputFile.append(os.path.basename(img)) + image = self._dcm.pixel_array self._dcmList.append(self._dcm) #print('%s: %s' % (img, image.shape)) try: @@ -499,20 +524,21 @@ def __init__(self, **kwargs): except Exception as e: self.warn( 'dcmInsertionFail', - '\nFor input DICOM file %s%s' % (img, str(e)), + '\nFor input DICOM file %s, %s' % (img, str(e)), True) i += 1 if self.str_outputFileStem.startswith('%'): - str_spec = self.str_outputFileStem + str_spec = self.str_outputFileStem self.str_outputFileStem = '' for key in str_spec.split('%')[1:]: - str_fileComponent = '' + str_fileComponent = '' if key == 'inputFile': - str_fileName, str_ext = os.path.splitext(self.str_inputFile) - str_fileComponent = str_fileName + str_fileName, str_ext = os.path.splitext(self.str_inputFile) + str_fileComponent = str_fileName else: - str_fileComponent = eval('self._dcm.%s' % key) - str_fileComponent = med2image.urlify(str_fileComponent) + # pudb.set_trace() + str_fileComponent = eval('str(self._dcm.%s)' % key) + str_fileComponent = med2image.urlify(str_fileComponent) if not len(self.str_outputFileStem): self.str_outputFileStem = str_fileComponent else: @@ -543,17 +569,17 @@ def warn(self, str_tag, str_extraMsg = '', b_exit = False): str_action = med2image._dictErr[str_tag]['action'] str_error = med2image._dictErr[str_tag]['error'] exitCode = med2image._dictErr[str_tag]['exitCode'] - self.dp.qprint( + self.LOG( 'Some error seems to have occured!', comms = 'error' ) - self.dp.qprint( + self.LOG( 'While %s' % str_action, comms = 'error' ) - self.dp.qprint( + self.LOG( '%s' % str_error, comms = 'error' ) if len(str_extraMsg): - self.dp.qprint(str_extraMsg, comms = 'error') + self.LOG(str_extraMsg, comms = 'error') if b_exit: sys.exit(exitCode) @@ -561,57 +587,75 @@ def run(self): ''' Runs the DICOM conversion based on internal state. ''' - self.dp.qprint('Converting DICOM image.') + self.LOG('DICOM conversion (ref: %s).' % self.lstr_inputFile[0]) + if self._b_convertMiddleSlice: + self.LOG('Converting middle slice in DICOM series') try: - self.dp.qprint('PatientName: %s' % self._dcm.PatientName) + self.LOG('\tPatientName: %s' % self._dcm.PatientName) except AttributeError: - self.dp.qprint('PatientName: %s' % 'PatientName not found in DCM header.') + self.LOG('\tPatientName: %s' % 'PatientName not found in DCM header.') self.warn( 'PatientNameTag') try: - self.dp.qprint('PatientAge: %s' % self._dcm.PatientAge) + self.LOG('\tPatientAge: %s' % self._dcm.PatientAge) except AttributeError: - self.dp.qprint('PatientAge: %s' % 'PatientAge not found in DCM header.') + self.LOG('\tPatientAge: %s' % 'PatientAge not found in DCM header.') self.warn( 'PatientAgeTag') try: - self.dp.qprint('PatientSex: %s' % self._dcm.PatientSex) + self.LOG('\tPatientSex: %s' % self._dcm.PatientSex) except AttributeError: - self.dp.qprint('PatientSex: %s' % 'PatientSex not found in DCM header.') + self.LOG('\tPatientSex: %s' % 'PatientSex not found in DCM header.') self.warn( 'PatientSexTag') try: - self.dp.qprint('PatientID: %s' % self._dcm.PatientID) + self.LOG('\tPatientID: %s' % self._dcm.PatientID) except AttributeError: - self.dp.qprint('PatientID: %s' % 'PatientID not found in DCM header.') + self.LOG('\tPatientID: %s' % 'PatientID not found in DCM header.') self.warn( 'PatientIDTag') try: - self.dp.qprint('SeriesDescription: %s' % self._dcm.SeriesDescription) + self.LOG('\tSeriesDescription: %s' % self._dcm.SeriesDescription) except AttributeError: - self.dp.qprint('SeriesDescription: %s' % 'SeriesDescription not found in DCM header.') + self.LOG('\tSeriesDescription: %s' % 'SeriesDescription not found in DCM header.') self.warn( 'SeriesDescriptionTag') try: - self.dp.qprint('ProtocolName: %s' % self._dcm.ProtocolName) + self.LOG('\tProtocolName: %s' % self._dcm.ProtocolName) except AttributeError: - self.dp.qprint('ProtocolName: %s' % 'ProtocolName not found in DCM header.') + self.LOG('\tProtocolName: %s' % 'ProtocolName not found in DCM header.') self.warn( 'ProtocolNameTag') - if self._b_convertMiddleSlice: - self.dp.qprint('Converting middle slice in DICOM series: %d' % self._sliceToConvert) - - l_rot90 = [ True, True, False ] + l_rot90 = [ bool(int(self.rot[0])), bool(int(self.rot[1])), bool(int(self.rot[2])) ] med2image.mkdir(self.str_outputDir) if not self._b_3D: - str_outputFile = '%s/%s.%s' % (self.str_outputDir, + if self.preserveDICOMinputName: + str_outputFile = '%s/%s.%s' % (self.str_outputDir, + os.path.splitext(self.lstr_inputFile[0])[0], + self.str_outputFileType) + else: + str_outputFile = '%s/%s.%s' % (self.str_outputDir, self.str_outputFileStem, self.str_outputFileType) self.process_slice() self.slice_save(str_outputFile) if self._b_3D: + dims = self._Vnp_3DVol.shape + self.LOG('Image volume logical (i, j, k) size: %s' % str(dims)) rotCount = 0 if self._b_reslice: for dim in ['x', 'y', 'z']: - self.dim_save(dimension = dim, makeSubDir = True, rot90 = l_rot90[rotCount], indexStart = 0, indexStop = -1) + self.dim_save( + dimension = dim, + makeSubDir = True, + rot90 = l_rot90[rotCount], + indexStart = 0, + indexStop = -1 + ) rotCount += 1 else: - self.dim_save(dimension = 'z', makeSubDir = False, rot90 = False, indexStart = 0, indexStop = -1) + self.dim_save( + dimension = 'z', + makeSubDir = False, + rot90 = l_rot90[2], + indexStart = 0, + indexStop = -1 + ) class med2image_nii(med2image): @@ -641,7 +685,7 @@ def run(self): Runs the NIfTI conversion based on internal state. ''' - self.dp.qprint('About to perform NifTI to %s conversion...\n' % + self.LOG('About to perform NifTI to %s conversion...\n' % self.str_outputFileType) frames = 1 @@ -652,10 +696,10 @@ def run(self): sliceEnd = 0 if self._b_4D: - self.dp.qprint('4D volume detected.\n') + self.LOG('4D volume detected.\n') frames = self._Vnp_4DVol.shape[3] if self._b_3D: - self.dp.qprint('3D volume detected.\n') + self.LOG('3D volume detected.\n') if self._b_convertMiddleFrame: self._frameToConvert = int(frames/2) @@ -689,13 +733,49 @@ def run(self): class object_factoryCreate: """ A class that examines input file string for extension information and - returns the relevant convert object. + creates a relevant convert object (or None). + + Returns true or false denoting converter object creation. """ def __init__(self, args): """ Parse relevant CLI args. """ + + def inputFile_defineFromSubStr(astr_dir): + """ + This nested function simply determines the first + file in the that has a substring in the + filename that conforms to the . + + That file becomes the . The + is a useful shortcut for quickly identifying and using + a file without needing to provide its full name. + """ + l_filesInDir : list = [] + l_fileHit : list = [] + # First, get a list of all the files in the directory + try: + (_, _, l_filesInDir) = next(os.walk(astr_dir)) + except: + return '' + l_fileHit = [ + s for s in l_filesInDir if args.inputFileSubStr in s + ] + if len(l_fileHit): + return l_fileHit[0] + else: + return '' + + if len(args.inputFileSubStr): + args.inputFile = inputFile_defineFromSubStr(args.inputDir) + if not len(args.inputFile): + print( 'Input dir "%s" has no files with substring "%s"' % + (args.inputDir, args.inputFileSubStr)) + # print('Exiting to system with code 1.') + # sys.exit(1) + str_outputFileStem, str_outputFileExtension = os.path.splitext(args.outputFileStem) if len(str_outputFileExtension): str_outputFileExtension = str_outputFileExtension.split('.')[1] @@ -713,28 +793,38 @@ def __init__(self, args): b_niftiExt = (str_inputFileExtension == '.nii' or str_inputFileExtension == '.gz') b_dicomExt = str_inputFileExtension == '.dcm' + + self.C_convert = None if b_niftiExt: self.C_convert = med2image_nii( - inputFile = args.inputFile, - inputDir = args.inputDir, - outputDir = args.outputDir, - outputFileStem = args.outputFileStem, - outputFileType = args.outputFileType, - sliceToConvert = args.sliceToConvert, - frameToConvert = args.frameToConvert, - showSlices = args.showSlices, - reslice = args.reslice + inputFile = args.inputFile, + inputDir = args.inputDir, + outputDir = args.outputDir, + outputFileStem = args.outputFileStem, + outputFileType = args.outputFileType, + sliceToConvert = args.sliceToConvert, + frameToConvert = args.frameToConvert, + showSlices = args.showSlices, + func = args.func, + reslice = args.reslice, + verbosity = args.verbosity ) print('sliceToConvert:', args.sliceToConvert) if b_dicomExt: self.C_convert = med2image_dcm( - inputFile = args.inputFile, - inputDir = args.inputDir, - outputDir = args.outputDir, - outputFileStem = args.outputFileStem, - outputFileType = args.outputFileType, - sliceToConvert = args.sliceToConvert, - reslice = args.reslice + inputFile = args.inputFile, + inputDir = args.inputDir, + outputDir = args.outputDir, + outputFileStem = args.outputFileStem, + outputFileType = args.outputFileType, + sliceToConvert = args.sliceToConvert, + convertOnlySingleDICOM = args.convertOnlySingleDICOM, + preserveDICOMinputName = args.preserveDICOMinputName, + reslice = args.reslice, + rot = args.rot, + rotAngle = args.rotAngle, + func = args.func, + verbosity = args.verbosity ) diff --git a/setup.py b/setup.py index 61c6221..b2e6f34 100644 --- a/setup.py +++ b/setup.py @@ -12,14 +12,14 @@ def readme(): setup( name = 'med2image', - version = '2.0.1', + version = '2.6.6', description = '(Python) utility to convert medical images to jpg and png', long_description = readme(), author = 'FNNDSC', author_email = 'dev@babymri.org', url = 'https://github.com/FNNDSC/med2image', packages = ['med2image'], - install_requires = ['nibabel', 'dicom', 'pydicom', 'numpy', 'matplotlib', 'pillow'], + install_requires = ['pfmisc', 'nibabel', 'pydicom', 'numpy', 'matplotlib', 'pillow'], #test_suite = 'nose.collector', #tests_require = ['nose'], scripts = ['bin/med2image'],