from __future__ import annotations
from typing import Sequence
import numpy as np
import os
import re
import torch
from pathlib import Path
[docs]relation_dict = {'unsignedinteger': 'int',
'shortfloat': 'float',
'int': 'int'}
[docs]def get_projections_from_single_file(headerfile: str) -> torch.Tensor:
"""Gets projection data from a SIMIND header file.
Args:
headerfile (str): Path to the header file
distance (str, optional): The units of measurements in the SIMIND file (this is required as input, since SIMIND uses mm/cm but doesn't specify). Defaults to 'cm'.
Returns:
(torch.Tensor[1, Ltheta, Lr, Lz]): Simulated SPECT projection data.
"""
with open(headerfile) as f:
headerdata = f.readlines()
headerdata = np.array(headerdata)
num_proj = get_header_value(headerdata, 'total number of images', int)
if num_proj>1:
raise ValueError('Only one projection is supported for PSF fitting')
proj_dim1 = get_header_value(headerdata, 'matrix size [1]', int)
proj_dim2 = get_header_value(headerdata, 'matrix size [2]', int)
number_format = get_header_value(headerdata, 'number format', str)
number_format= relation_dict[number_format]
num_bytes_per_pixel = get_header_value(headerdata, 'number of bytes per pixel', int)
imagefile = get_header_value(headerdata, 'name of data file', str)
dtype = eval(f'np.{number_format}{num_bytes_per_pixel*8}')
projections = np.fromfile(os.path.join(str(Path(headerfile).parent), imagefile), dtype=dtype)
projections = np.transpose(projections.reshape((num_proj,proj_dim2,proj_dim1))[:,::-1], (0,2,1))[0]
projections = torch.tensor(projections.copy())
return projections
[docs]def get_projections(headerfiles: str | Sequence[str]) -> torch.Tensor:
"""Obtains projection PSF data from a list of headerfiles and concatenates them together
Args:
headerfiles (str | Sequence[str]): List of length Ld corresponding to all projections at different source-detector distances.
Returns:
torch.Tensor[Ld,Lx,Ly]: Output tensor of PSF data at each source-detector distance
"""
projectionss = []
for headerfile in headerfiles:
projections = get_projections_from_single_file(headerfile)
projectionss.append(projections)
return torch.stack(projectionss)
[docs]def get_source_detector_distances(resfiles: str) -> torch.Tensor:
"""Obtains the source-detector distance from a list of resfiles
Args:
resfiles (str): List of .res files (of length Ld) corresponding to each simulated PSF projection
Returns:
torch.Tensor[Ld]: List of source-detector distances
"""
radii = []
for resfile in resfiles:
with open(resfile) as f:
resdata = f.readlines()
resdata = np.array(resdata)
radius = float(get_header_value(resdata, 'UpperEneWindowTresh:', str).split(':')[-1])
radii.append(radius)
return torch.tensor(radii)
[docs]def get_meshgrid(resfiles: str, device = 'cpu') -> tuple[torch.Tensor, torch.Tensor]:
"""Obtains a meshgrid of the x and y coordinates correpsonding to the PSF data simulated
Args:
resfiles (str): List of .res files (of length Ld) corresponding to each simulated PSF projection
device (str, optional): Device to place the output projection data on. Defaults to 'cpu'.
Returns:
tuple[torch.Tensor, torch.Tensor]: Meshgrid of x and y coordinates
"""
with open(resfiles[0]) as f:
resdata = f.readlines()
resdata = np.array(resdata)
dx = float(get_header_value(resdata, 'PixelSize I', str).split(':')[1].split('S')[0])
dy = float(get_header_value(resdata, 'PixelSize J', str).split(':')[1].split('S')[0])
Nx = int(get_header_value(resdata, 'MatrixSize I', str).split(':')[1].split('I')[0])
Ny = int(get_header_value(resdata, 'MatrixSize J', str).split(':')[1].split('A')[0])
x = torch.arange(-(Nx-1)/2, (Nx+1)/2, 1).to(device).to(torch.float32) * dx
y = torch.arange(-(Ny-1)/2, (Ny+1)/2, 1).to(device).to(torch.float32) * dy
xv, yv = torch.meshgrid(x, y, indexing='xy')
return xv, yv