--- a
+++ b/rocaseg/components/formats.py
@@ -0,0 +1,140 @@
+from glob import glob
+import logging
+
+import numpy as np
+import cv2
+import nibabel as nib
+
+
+logging.basicConfig()
+logger = logging.getLogger('formats')
+logger.setLevel(logging.DEBUG)
+
+
+def png_to_numpy(pattern_fname_in, reverse=False):
+    """
+
+    Args:
+        pattern_fname_in: str
+            String or regexp compatible with `glob`.
+        reverse: bool
+            Whether to use reverse slice order.
+
+    Returns:
+        stack: [R, C, P] ndarray
+    """
+    fnames_in = sorted(glob(pattern_fname_in))
+
+    stack = [cv2.imread(fn, cv2.IMREAD_GRAYSCALE) for fn in fnames_in]
+    stack = np.stack(stack, axis=2)
+    if reverse:
+        stack = stack[..., ::-1]
+    return stack
+
+
+def png_to_nifti(pattern_fname_in, fname_out, spacings=None, reverse=False,
+                 rcp_to_ras=False):
+    """
+
+    Args:
+        pattern_fname_in: str
+            String or regexp compatible with `glob`.
+        fname_out: str
+            Full path to the output file.
+        spacings: 3-tuple of float
+            (pixel spacing in r, pixel spacing in c, slice thickness).
+        reverse: bool
+            Whether to use reverse slice order.
+        rcp_to_ras: bool
+            Whether to convert from row-column-plane to RAS+ coordinates.
+
+    """
+    fnames_in = sorted(glob(pattern_fname_in))
+
+    stack = [cv2.imread(fn, cv2.IMREAD_GRAYSCALE) for fn in fnames_in]
+    stack = np.stack(stack, axis=2)
+    if reverse:
+        stack = stack[..., ::-1]
+
+    numpy_to_nifti(stack=stack, fname_out=fname_out, spacings=spacings,
+                   rcp_to_ras=rcp_to_ras)
+
+
+def nifti_to_png(fname_in, pattern_fname_out, reverse=False, ras_to_rcp=False):
+    """
+
+    Args:
+        fname_in: str
+            Full path to the input file.
+        pattern_fname_out: str
+            Must include `{i}`, which is to be substituted with the running index.
+        reverse: bool
+            Whether to use reverse slice order.
+        ras_to_rcp: bool
+            Whether to convert from RAS+ to row-column-plane coordinates.
+    """
+    stack, spacings = nifti_to_numpy(fname_in=fname_in, ras_to_rcp=ras_to_rcp)
+
+    if reverse:
+        stack = stack[..., ::-1]
+
+    for i in range(stack.shape[-1]):
+        fn = pattern_fname_out.format(i=i)
+        cv2.imwrite(fn, stack[..., i])
+
+
+def nifti_to_numpy(fname_in, ras_to_rcp=False):
+    """
+
+    Args:
+        fname_in: str
+            Full path to the input file.
+        ras_to_rcp: bool
+            Whether to convert from RAS+ to row-column-plane coordinates.
+
+    Returns:
+        stack: [R, C, P] ndarray
+        spacings: 3-tuple of float
+            (pixel spacing in r, pixel spacing in c, slice thickness).
+
+    """
+    scan = nib.load(fname_in)
+    stack = scan.get_fdata()
+    spacings = [scan.affine[i, i] for i in range(3)]
+
+    if ras_to_rcp:
+        stack = np.moveaxis(stack, [2, 1, 0], [0, 1, 2])
+        spacings = [-s for s in spacings[::-1]]
+
+    return stack, spacings
+
+
+def numpy_to_nifti(stack, fname_out, spacings=None, rcp_to_ras=False):
+    """
+
+    Args:
+        stack: (r, c, p) ndarray
+            Data array.
+        fname_out:
+            Full path to the output file.
+        spacings: 3-tuple of float
+            (pixel spacing in r, pixel spacing in c, slice thickness).
+        rcp_to_ras: bool
+            Whether to convert from row-column-plane to RAS+ coordinates.
+    """
+    if not rcp_to_ras:
+        affine = np.eye(4, dtype=np.float)
+        if spacings is not None:
+            affine[0, 0] = spacings[0]
+            affine[1, 1] = spacings[1]
+            affine[2, 2] = spacings[2]
+    else:
+        stack = np.moveaxis(stack, [0, 1, 2], [2, 1, 0])
+        affine = np.diag([-1., -1., -1., 1.]).astype(np.float)
+        if spacings is not None:
+            affine[0, 0] = -spacings[2]
+            affine[1, 1] = -spacings[1]
+            affine[2, 2] = -spacings[0]
+
+    scan = nib.Nifti1Image(stack, affine=affine)
+    nib.save(scan, fname_out)