From adc8795fbb6d0070864bf36d6fc72ccd6a17ea31 Mon Sep 17 00:00:00 2001
From: Tamino Huxohl <thuxohl@techfak.uni-bielefeld.de>
Date: Thu, 17 Nov 2022 13:47:38 +0100
Subject: [PATCH] update projection script to allow projections without writing
 a file

---
 mu_map/recon/project.py | 172 +++++++++++++++++++++-------------------
 1 file changed, 91 insertions(+), 81 deletions(-)

diff --git a/mu_map/recon/project.py b/mu_map/recon/project.py
index dc92b54..fbf7686 100644
--- a/mu_map/recon/project.py
+++ b/mu_map/recon/project.py
@@ -1,25 +1,22 @@
-import argparse
 import os
 import tempfile
-from typing import Optional, Tuple
+from typing import Dict, Optional, Tuple
 
-import cv2 as cv
 import numpy as np
 from pydicom.dataset import FileDataset as DCMImage
 import stir
-import stirextra
 
-# from vis import to_gray
 from mu_map.file.interfile import (
     load_interfile,
-    KEY_DIM_1,
-    KEY_DIM_2,
-    KEY_DIM_3,
+    write_interfile,
     KEY_SPACING_1,
-    KEY_SPACING_2,
+    KEY_SPACING_3,
 )
 
 
+"""
+A template for a header of a projection in INTERFILE format.
+"""
 TEMPLATE_HEADER_PROJ = """
 !INTERFILE  :=
 !imaging modality := nucmed
@@ -56,70 +53,121 @@ radius := {RADIUS}
 
 
 def forward_project(
-    recon: np.ndarray,
-    spacing_slices: Optional[float],
-    spacing_bins: Optional[float],
+    recon_header: Dict[str, str],
+    recon_img: np.ndarray,
     n_slices: Optional[int] = None,
-    n_bin: Optional[int] = None,
+    n_bins: Optional[int] = None,
     n_projections: int = 120,
     rotation: int = 360,
     start_angle: int = 180,
     radius: int = 150,
-    verbose: bool = False,
-):
-    n_slices = recon.shape[0] if n_slices is None else n_slices
-    n_bins = recon.shape[1] if n_bins is None else n_bins
-
-    # dir_tmp = tempfile.TemporaryDirectory()
-    header_proj = HEADER_TEMPLATE_PROJ.replace("{SLICES}", str(n_slices))
+    **kwargs,
+) -> Tuple[Dict[str, str], np.ndarray]:
+    """
+    Forward project a reconstruction in INTERFILE format.
+
+    :param recon_header: the header of the reconstruction
+    :param recon_img: the reconstruction image
+    :param n_slices: the number of slices of the projection - defaults to the number of slices of the reconstruction
+    :param n_bins: the number of bins of the projection - defaults to the number of rows in the reconstruction
+    :param n_projections: the number of views used for the projection
+    :param rotation: the total rotation in degrees
+    :param start_angle: the start angle of the rotation
+    :param radius: the radius of the detector circle in mm
+    :return: an interfile image as a tuple of a header and a numpy array
+    """
+    n_slices = recon_img.shape[0] if n_slices is None else n_slices
+    n_bins = recon_img.shape[1] if n_bins is None else n_bins
+    # work around a bug in STIR with start_angle = 0
+    start_angle = 360 if start_angle == 0 else start_angle
+
+    # prepare writing temporary files to configure the projection with STIR
+    filename_recon = "recon.hv"
+    filename_proj = "projection"
+    filename_proj_data = f"{filename_proj}.s"
+    filename_proj_header = f"{filename_proj}.hs"
+
+    header_proj = TEMPLATE_HEADER_PROJ.replace("{DATA_FILE}", filename_proj_data)
+    header_proj = header_proj.replace("{SLICES}", str(n_slices))
     header_proj = header_proj.replace("{BINS}", str(n_bins))
-    header_proj = header_proj.replace("{SPACING_SLICES}", f"{spacing_slices:.4f}")
-    header_proj = header_proj.replace("{SPACING_BINS}", f"{spacing_bins:.4f}")
+    header_proj = header_proj.replace("{SPACING_SLICES}", recon_header[KEY_SPACING_3])
+    header_proj = header_proj.replace("{SPACING_BINS}", recon_header[KEY_SPACING_1])
     header_proj = header_proj.replace("{N_PROJECTIONS}", str(n_projections))
     header_proj = header_proj.replace("{ROTATION}", str(rotation))
     header_proj = header_proj.replace("{START_ANGLE}", str(start_angle))
     header_proj = header_proj.replace("{RADIUS}", str(radius))
     header_proj = header_proj.strip()
-    print("Project header...")
-    print(header_proj)
 
+    # write temporary files to configure the projection with STIR
+    dir_tmp = tempfile.TemporaryDirectory()
 
-def forward_project_dcm(dcm: DCMImage, recon: np.ndarray, **kwargs):
-    spacing_slices = float(dcm.SliceThickness)
-    spacing_bins = float(dcm.PixelSpacing[0])
-    return forward_project(recon, spacing_slices=spacing_slices, spacing_bins=spacing_bins, **kwargs)
+    filename_proj_header = os.path.join(dir_tmp.name, filename_proj_header)
+    filename_recon = os.path.join(dir_tmp.name, filename_recon)
 
+    with open(filename_proj_header, mode="w") as f:
+        f.write(header_proj)
+    with open(os.path.join(dir_tmp.name, filename_proj_data), mode="wb") as f:
+        f.write(b"")
+    write_interfile(filename_recon, recon_header, recon_img)
 
+    projdata = stir.ProjData.read_from_file(filename_proj_header)
+    recon = stir.FloatVoxelsOnCartesianGrid.read_from_file(filename_recon)
 
-if __name__ == "__main__":
-    from mu_map.file.dicom import load_dcm
-    dcm, recon = load_dcm("tmp/0001-stress-recon_nac_nsc.dcm")
-    forward_project_dcm(dcm, recon)
+    projmatrix = stir.ProjMatrixByBinUsingRayTracing()
+    projmatrix.set_up(projdata.get_proj_data_info(), recon)
 
+    forwardprojector = stir.ForwardProjectorByBinUsingProjMatrixByBin(projmatrix)
 
-    exit(0)
+    projdataout = stir.ProjDataInMemory(
+        projdata.get_exam_info(), projdata.get_proj_data_info()
+    )
+
+    forwardprojector.set_up(projdataout.get_proj_data_info(), recon)
+    forwardprojector.forward_project(projdataout, recon)
+
+    filename_out = os.path.join(dir_tmp.name, "out.hs")
+    projdataout.write_to_file(filename_out)
+    return load_interfile(filename_out)
+
+
+if __name__ == "__main__":
+    import argparse
+
+    from mu_map.file.dicom import load_dcm
+    from mu_map.file.dicom_to_interfile import to_interfile
 
     parser = argparse.ArgumentParser(
-        description="Project a reconstruction with STIR",
+        description="Forward project a reconstruction with STIR",
         formatter_class=argparse.ArgumentDefaultsHelpFormatter,
     )
     parser.add_argument(
         "--recon",
         type=str,
         required=True,
-        help="the reconstruction to project in INTERFILE format",
+        help="the reconstruction to project in INTERFILE or DICOM format",
     )
     parser.add_argument(
         "--out", type=str, required=True, help="the projection output by this script"
     )
     parser.add_argument(
-        "--n_projections", type=int, default=120, help="the number of performed projections"
+        "--n_slices",
+        type=int,
+        help="configure a different number of slices than in the reconstruction",
+    )
+    parser.add_argument(
+        "--n_projections",
+        type=int,
+        default=120,
+        help="the number of performed projections",
     )
     parser.add_argument(
         "--rotation", type=int, default=360, help="the amount of rotation in degrees"
     )
     parser.add_argument(
-        "--start_angle", type=int, default=360, help="the start angle for the projections"
+        "--start_angle",
+        type=int,
+        default=180,
+        help="the start angle for the projections",
     )
     parser.add_argument(
         "--radius", type=int, default=150, help="the radius of the rotation"
@@ -127,53 +175,15 @@ if __name__ == "__main__":
     parser.add_argument(
         "-v", "--verbosity", type=int, default=0, help="configure the verbosity of STIR"
     )
-    parser.add_argument("--header", type=str, help="configure the verbosity of STIR")
     args = parser.parse_args()
-    args.out = os.path.splitext(args.out)[0]
-    args.start_angle = 360 if args.start_angle == 0 else args.start_angle
 
     stir.Verbosity_set(args.verbosity)
 
-    tmp_dir = tempfile.TemporaryDirectory()
-    if args.header is None:
+    _, ext = os.path.splitext(args.recon)
+    if ext == ".dcm":
+        header, image = to_interfile(*load_dcm(args.recon))
+    else:
         header, image = load_interfile(args.recon)
-        print(header[KEY_DIM_3])
-        # header_proj = HEADER_TEMPLATE_PROJ.replace("{ROWS}", header[KEY_DIM_1])
-        header_proj = HEADER_TEMPLATE_PROJ.replace("{ROWS}", str(33))
-        header_proj = header_proj.replace("{COLUMNS}", header[KEY_DIM_2])
-        header_proj = header_proj.replace("{SPACING_X}", header[KEY_SPACING_1])
-        header_proj = header_proj.replace("{SPACING_Y}", header[KEY_SPACING_2])
-        header_proj = header_proj.replace("{N_PROJECTIONS}", str(args.n_projections))
-        header_proj = header_proj.replace("{ROTATION}", str(args.rotation))
-        header_proj = header_proj.replace("{START_ANGLE}", str(args.start_angle))
-        header_proj = header_proj.replace("{RADIUS}", str(args.radius))
-        header_proj = header_proj.strip()
-        print(f"Header:\n{header_proj}")
-
-        filename = "proj"
-        filename_header = f"{filename}.hs"
-        filename_data = f"{filename}.s"
-        header_proj = header_proj.replace("{DATA_FILE}", filename_data)
-        print(header_proj)
-
-        with open(os.path.join(tmp_dir.name, filename_header), mode="w") as f:
-            f.write(header_proj)
-        with open(os.path.join(tmp_dir.name, filename_data), mode="wb") as f:
-            f.write(b"")
-        args.header = os.path.join(tmp_dir.name, filename_header)
-
-    projdata = stir.ProjData.read_from_file(args.header)
-    recon = stir.FloatVoxelsOnCartesianGrid.read_from_file(args.recon)
-
-    projmatrix = stir.ProjMatrixByBinUsingRayTracing()
-    projmatrix.set_up(projdata.get_proj_data_info(), recon)
 
-    forwardprojector = stir.ForwardProjectorByBinUsingProjMatrixByBin(projmatrix)
-
-    projdataout = stir.ProjDataInMemory(
-        projdata.get_exam_info(), projdata.get_proj_data_info()
-    )
-
-    forwardprojector.set_up(projdataout.get_proj_data_info(), recon)
-    forwardprojector.forward_project(projdataout, recon)
-    projdataout.write_to_file(args.out)
+    header, image = forward_project(header, image, **vars(args))
+    write_interfile(args.out, header, image)
-- 
GitLab