diff --git a/conversion/__init__.py b/conversion/__init__.py deleted file mode 100644 index d2a937e..0000000 --- a/conversion/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .conversion import Conversion diff --git a/conversion/conversion.py b/conversion/conversion.py deleted file mode 100644 index c207a3e..0000000 --- a/conversion/conversion.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import Tuple -import numpy as np -from PySide2.QtGui import QPixmap, QImage, QColor - - -class Conversion: - def __init__(self, shape: Tuple[int, int] = None): - if shape is not None: - self.qimage = QImage(shape[1], shape[0], QImage.Format_RGB16) - self.height, self.width = shape[:2] - self.array_greyscale = np.ndarray(shape) - self.array_rgb = np.ndarray((*shape, 3)) - self.is_init = True - else: - self.is_init = False - - def array2qpixmap(self, img: np.ndarray) -> QPixmap: - img8 = img.astype(np.uint8) - if self.is_init: - height, width = self.height, self.width - img = self.qimage - else: - height, width = img8.shape[:2] - img = QImage(width, height, QImage.Format_RGB16) - - color = QColor() - - if len(img8.shape) == 2: - for x in range(width): - for y in range(height): - color.setHsv(0, 0, img8[y][x]) - img.setPixel(x, y, color.rgb()) - else: - for x in range(width): - for y in range(height): - color.setRgb(*img8[y][x]) - img.setPixel(x, y, color.rgb()) - - return QPixmap(img) - - def rgb2greyscale(self, img: np.ndarray) -> np.ndarray: - if len(img.shape) != 3: - return img - - if self.is_init: - array = self.array_greyscale - else: - array = np.ndarray(shape=(img.shape[0], img.shape[1])) - - for i in range(img.shape[0]): - for j in range(img.shape[1]): - array[i, j] = img[i, j, 0] - - return array - - def greyscale2rgb(self, img: np.ndarray) -> np.ndarray: - if self.is_init: - array = self.array_rgb - else: - array = np.ndarray(shape=(img.shape[0], img.shape[1], 3)) - - for i in range(img.shape[0]): - for j in range(img.shape[1]): - array[i, j, :3] = img[i, j] - - return array diff --git a/ct/CT.py b/ct/CT.py deleted file mode 100644 index d6cbdda..0000000 --- a/ct/CT.py +++ /dev/null @@ -1,74 +0,0 @@ -import numpy as np -from typing import Tuple -from abc import abstractmethod -from ct.radon import radonTransform -from ct.iradon import iradonTransform -import logging - - -class CT: - def __init__(self, img: np.ndarray, rotate_angle: int, start_angle: int, detectors_number: int, - farthest_detectors_distance: int): - """ - img: Image to simulate radon transform - rotate_angle: Emiters and detectors_pos angle for next iteration in degrees - start_angle: Initial degree in degrees - detectors_amount: Amount of detectors_pos - farthest_detectors_distance: Distance in pixels between farthest detectors_pos - """ - if rotate_angle <= 0 or rotate_angle >= 180: - raise ArithmeticError("Rotate angle have to be in range (0, 180).") - else: - self.print = False - self.img = img - self.rotate_angle = rotate_angle - self.theta = start_angle - self.detectors_number = detectors_number - self.far_detectors_distance = farthest_detectors_distance - self.logger = logging.getLogger(__name__) - self.logger.addHandler(logging.StreamHandler()) - - def setProcessInfo(self, value: bool) -> None: - self.print = value - - def run(self) -> Tuple[np.ndarray, np.ndarray]: - if self.print: - self.logger.info('Radon transform starting.') - - sinogram = radonTransform(self.img, self.rotate_angle, self.theta, - self.detectors_number, self.far_detectors_distance, self.saveRadonFrame) - - self.resetIter() - - if self.print: - self.logger.info('Radon transform ended, inverse radon transform starting.') - - img = iradonTransform(self.img.shape, sinogram, self.rotate_angle, self.theta, self.far_detectors_distance, - self.saveIradonFrame) - - if self.print: - self.logger.info('Inverse radon transform ended.') - - sinogram /= np.max(sinogram) - return sinogram.T, img - - @abstractmethod - def saveRadonFrame(self, sinogram: np.ndarray) -> None: - """ - Method to be overridden by derived class. - """ - pass - - @abstractmethod - def saveIradonFrame(self, img: np.ndarray, pixel_amount_lines: np.ndarray) -> None: - """ - Method to be overridden by derived class. - """ - pass - - @abstractmethod - def resetIter(self) -> None: - """ - Method to be overridden by derived class. - """ - pass diff --git a/ct/__init__.py b/ct/__init__.py deleted file mode 100644 index 89958a8..0000000 --- a/ct/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .CT import CT -from .interactive import InteractiveCT diff --git a/ct/interactive/__init__.py b/ct/interactive/__init__.py deleted file mode 100644 index c932b1a..0000000 --- a/ct/interactive/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .interactiveCT import InteractiveCT diff --git a/ct/interactive/interactiveCT.py b/ct/interactive/interactiveCT.py deleted file mode 100644 index f54e801..0000000 --- a/ct/interactive/interactiveCT.py +++ /dev/null @@ -1,46 +0,0 @@ -import numpy as np -from typing import Tuple -from ct import CT - - -class InteractiveCT(CT): - def __init__(self, img: np.ndarray, rotate_angle: int, start_angle: int, detectors_number: int, farthest_detectors_distance: int): - """ - img: Image to simulate radon transform - rotate_angle: Emiters and detectors_pos angle for next iteration in degrees - start_angle: Initial degree in degrees - detectors_amount: Amount of detectors_pos - farthest_detectors_distance: Distance in pixels between farthest detectors_pos - """ - super().__init__(img, rotate_angle, start_angle, detectors_number, farthest_detectors_distance) - - self.iter = 0 - self.stopIter = 180 // rotate_angle - self.step = 1 - self.rotate_angle = rotate_angle - self.sinogram_plots = [] - self.result_plots = [] - - def run(self) -> Tuple[np.ndarray, np.ndarray]: - return super().run() - - def getFrames(self) -> Tuple[list, list]: - return self.sinogram_plots, self.result_plots - - def saveRadonFrame(self, sinogram: np.ndarray) -> None: - if not self.iter % self.step and self.iter < self.stopIter: - sinogram = np.array(sinogram.T, copy=True) - self.sinogram_plots.append(sinogram) - self.iter += 1 - - def resetIter(self) -> None: - self.iter = 0 - - def saveIradonFrame(self, img: np.ndarray, pixel_amount_lines: np.ndarray) -> None: - if not self.iter % self.step and self.iter < self.stopIter: - img_iradon = np.array(img, copy=True) - x = img_iradon / pixel_amount_lines - x = np.max(x) - img_iradon /= x if x > 0 else 1 - self.result_plots.append(img_iradon) - self.iter += 1 diff --git a/ct/iradon/__init__.py b/ct/iradon/__init__.py deleted file mode 100644 index 9616a05..0000000 --- a/ct/iradon/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .iradonTransform import iradonTransform diff --git a/ct/iradon/iradonTransform.py b/ct/iradon/iradonTransform.py deleted file mode 100644 index 18806f6..0000000 --- a/ct/iradon/iradonTransform.py +++ /dev/null @@ -1,21 +0,0 @@ -import numpy as np -from typing import Tuple, Callable -from ct.iradon.lineRadiation import LineRadiation -from ct.round import Round - - -def iradonTransform(shape: Tuple[int, int], sinogram: np.ndarray, rotate_angle: float, start_angle: float, - farthest_detectors_distance: int, animate_func: Callable[[np.ndarray, np.ndarray], None] = None) -> np.ndarray: - diameter = np.sqrt(shape[0] ** 2 + shape[1] ** 2) - circle = Round((shape[0] / 2, shape[1] / 2), diameter / 2, sinogram.shape[1], - farthest_detectors_distance, np.deg2rad(start_angle)) - - img = np.zeros(shape) - irad = LineRadiation(img, LineRadiation.Operation.MEAN) - radian_rotate_angle = np.deg2rad(rotate_angle) - for angle in range(sinogram.shape[0]): - animate_func(irad.img, irad.amount) - irad.next(sinogram[angle], circle.emiter, circle.detectors) - circle.rotate(radian_rotate_angle) - - return irad.end() diff --git a/ct/iradon/lineRadiation.py b/ct/iradon/lineRadiation.py deleted file mode 100644 index 445180a..0000000 --- a/ct/iradon/lineRadiation.py +++ /dev/null @@ -1,71 +0,0 @@ -import numpy as np -from typing import Tuple, List, Callable, Union -from skimage.draw import line -from enum import Enum - - -class LineRadiation: - class Operation(Enum): - MEAN = 1 - SQRT = 2 - - def __init__(self, img: np.ndarray, operation_type: Operation): - self.img = img - self.amount = np.ones(img.shape) - - if operation_type == LineRadiation.Operation.MEAN: - self.operation = self.nextMean - self.end_operation = self.endMean - elif operation_type == LineRadiation.Operation.SQRT: - self.operation = self.nextSqrt - self.end_operation = self.endSqrt - - def next(self, detectors_values: np.ndarray, emiter_pos: Tuple[int, int], detectors_pos: List[Tuple[int, int]]) -> None: - self.findLinesPixels(detectors_values, emiter_pos, detectors_pos, self.operation) - - def end(self) -> np.ndarray: - self.end_operation() - return self.img - - def findLinesPixels(self, detectors_values: np.ndarray, emiter_pos: Tuple[int, int], detectors_pos: List[Tuple[int, int]], - operation: Callable[[List[int], List[int], Union[int, float]], None]) -> None: - # local reference for speed up - img_width, img_height = self.img.shape - - for de_index, detector in enumerate(detectors_pos): - rr, cc = line(emiter_pos[1], emiter_pos[0], detector[1], detector[0]) # find pixels - - start = 0 - for i in range(len(rr)): # find correct pixels start index - if 0 <= rr[i] < img_width and 0 <= cc[i] < img_height: - start = i - break - - stop = 0 - for i in range(len(rr) - 1, start + 1, -1): # find correct pixels stop index - if 0 <= rr[i] < img_width and 0 <= cc[i] < img_height: - stop = i - break - - operation(rr[start:stop + 1], cc[start:stop + 1], detectors_values[de_index]) # correct pixels - - def nextMean(self, pixels_x: List[int], pixels_y: List[int], detector_value: Union[int, float]) -> None: - """ - Add emitter-detector line value to the intersected pixels and calculate average. - """ - self.img[pixels_x, pixels_y] += detector_value - self.amount[pixels_x, pixels_y] += 1 - - def endMean(self) -> None: - x = self.img / self.amount - x = np.max(x) - self.img /= x if x > 0 else 1 - - def nextSqrt(self, pixels_x: List[int], pixels_y: List[int], detector_value: Union[int, float]) -> None: - """ - Add emitter-detector line value to the intersected pixels and calculate square root. - """ - self.img[pixels_x, pixels_y] += detector_value - - def endSqrt(self) -> None: - self.img = np.sqrt(self.img) diff --git a/ct/radon/__init__.py b/ct/radon/__init__.py deleted file mode 100644 index d80b798..0000000 --- a/ct/radon/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .radonTransform import radonTransform diff --git a/ct/radon/radiation.py b/ct/radon/radiation.py deleted file mode 100644 index 5b03005..0000000 --- a/ct/radon/radiation.py +++ /dev/null @@ -1,40 +0,0 @@ -import numpy as np -from typing import Tuple, List -from skimage.draw import line - - -class Radiation: - def __init__(self, img: np.ndarray, detectors_amount: int): - self.img = img - self.detectors_received_brightness = np.ndarray(shape=(detectors_amount,)) - - def calculate(self, emiter_pos: Tuple[int, int], detectors_pos: List[Tuple[int, int]]) -> np.ndarray: - """ - Averaging pixels values on the emitter-detector line for every detector. - """ - - # local reference for speed up - img = self.img - img_width, img_height = self.img.shape - detectors_received_brightness = self.detectors_received_brightness - - for de_index, detector in enumerate(detectors_pos): - rr, cc = line(emiter_pos[1], emiter_pos[0], detector[1], detector[0]) # find pixels - - start = 0 - for i in range(len(rr)): # find correct pixels start index - if 0 <= rr[i] < img_width and 0 <= cc[i] < img_height: - start = i - break - - stop = 0 - for i in range(len(rr) - 1, start + 1, -1): # find correct pixels stop index - if 0 <= rr[i] < img_width and 0 <= cc[i] < img_height: - stop = i - break - - pixels_brightness = np.sum(img[rr[start:stop + 1], cc[start:stop + 1]]) - - detectors_received_brightness[de_index] = pixels_brightness / (stop - start + 1) - - return self.detectors_received_brightness diff --git a/ct/radon/radonTransform.py b/ct/radon/radonTransform.py deleted file mode 100644 index 5723098..0000000 --- a/ct/radon/radonTransform.py +++ /dev/null @@ -1,24 +0,0 @@ -import numpy as np -from typing import Callable -from ct.radon.radiation import Radiation -from ct.round import Round - - -def radonTransform(img: np.ndarray, rotate_angle: int, start_angle: int, detectors_number: int, farthest_detectors_distance: int, - animate_func: Callable[[np.ndarray], None] = None) -> np.ndarray: - - diameter = np.sqrt(img.shape[0] ** 2 + img.shape[1] ** 2) - circle = Round((img.shape[0] / 2, img.shape[1] / 2), diameter / 2, detectors_number, - farthest_detectors_distance, np.deg2rad(start_angle)) - - radian_rotate_angle: float = np.deg2rad(rotate_angle) - sinogram = np.zeros(shape=(180 // rotate_angle, detectors_number)) - - rad = Radiation(img, detectors_number) - - for i in range(180 // rotate_angle): - animate_func(sinogram) - sinogram[i] = rad.calculate(circle.emiter, circle.detectors) - circle.rotate(radian_rotate_angle) - - return sinogram diff --git a/ct/round.py b/ct/round.py deleted file mode 100644 index 7f7e8ff..0000000 --- a/ct/round.py +++ /dev/null @@ -1,52 +0,0 @@ -import numpy as np -from typing import Tuple - - -class Round: - """ - Helps simulates rounding emitter and detectors_pos around img. - """ - def __init__(self, center: Tuple[float, float], radius: float, detectors_amount: int, farthest_detectors_distance: int, start_angle: float): - self.center = center - self.radius = radius - self.theta = start_angle - self.angle = 0 - - self.detectors_number = detectors_amount - self.far_detectors_distance = farthest_detectors_distance - - self.emiter = None - self.detectors = [None for i in range(detectors_amount)] - - if self.far_detectors_distance > 2 * radius: - raise ArithmeticError(f"Distance between fartest detectors_pos have to be less or equal to {int(2 * radius)}.") - else: - self.fi = 2 * np.arcsin(self.far_detectors_distance / (2 * self.radius)) - - # for performance reason it is done once - self.angle_step_detectors = [self.theta + np.pi - self.fi / 2 + i * self.fi / (self.detectors_number - 1) - for i in range(detectors_amount)] - - self.calculateEmiterPosition() - self.calculateDetectorsPositions() - - def calculateEmiterPosition(self) -> None: - angle = self.theta + self.angle - x = self.center[0] + self.radius * np.cos(angle) - y = self.center[1] - self.radius * np.sin(angle) - self.emiter = int(np.round(x)), int(np.round(y)) - - def calculateDetectorsPositions(self) -> None: - for i in range(self.detectors_number): - angle = self.angle + self.angle_step_detectors[i] - x = self.center[0] + self.radius * np.cos(angle) - y = self.center[1] - self.radius * np.sin(angle) - self.detectors[i] = (int(np.round(x)), int(np.round(y))) - - def rotate(self, angle: float) -> None: - """ - Rotate round by angle. - """ - self.angle += angle - self.calculateEmiterPosition() - self.calculateDetectorsPositions() diff --git a/gui/__init__.py b/gui/__init__.py deleted file mode 100644 index 9e8355f..0000000 --- a/gui/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .mainWindow import MainWindow diff --git a/gui/dicomWindow/__init__.py b/gui/dicomWindow/__init__.py deleted file mode 100644 index 574af30..0000000 --- a/gui/dicomWindow/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .dicomWindow import DicomSaveDialog, DicomShowDialog diff --git a/gui/dicomWindow/dicomWindow.py b/gui/dicomWindow/dicomWindow.py deleted file mode 100644 index ebe4e88..0000000 --- a/gui/dicomWindow/dicomWindow.py +++ /dev/null @@ -1,211 +0,0 @@ -import numpy as np -from datetime import datetime -from typing import Dict -from conversion import Conversion -from PySide2.QtCore import Qt -import PySide2.QtWidgets as QtWidgets -import pydicom as pd -import pydicom.uid - - -class DicomSaveDialog(QtWidgets.QDialog): - def __init__(self, img: np.ndarray, date_time: datetime): - super().__init__(None) - self.setWindowTitle('Save to DICOM file') - self.img = img - self.date_time = date_time - - self.input_data: Dict[str, None or str] = { - 'PatientID': None, - 'PatientName': None, - 'PatientSex': None, - 'ImageComments': None - } - - self.name_input = None - self.id_input = None - self.sex_input = None - self.comments_input = None - - self.createLayout() - - def createLayout(self) -> None: - self.name_input = QtWidgets.QLineEdit() - self.id_input = QtWidgets.QLineEdit() - self.sex_input = QtWidgets.QComboBox() - self.comments_input = QtWidgets.QPlainTextEdit() - - self.comments_input.setLineWrapMode(QtWidgets.QPlainTextEdit.WidgetWidth) - self.sex_input.addItem('Male') - self.sex_input.addItem('Female') - - cancel = QtWidgets.QPushButton('Cancel') - save = QtWidgets.QPushButton('Save as') - - cancel.clicked.connect(self.close) - save.clicked.connect(self.save) - - grid = QtWidgets.QGridLayout() - grid.addWidget(cancel, 1, 1) - grid.addWidget(save, 1, 2) - - form = QtWidgets.QFormLayout() - form.addRow(QtWidgets.QLabel('Patient ID'), self.id_input) - form.addRow(QtWidgets.QLabel('Patient name'), self.name_input) - form.addRow(QtWidgets.QLabel('Patient sex'), self.sex_input) - form.addRow(QtWidgets.QLabel('Comments'), self.comments_input) - form.addRow(grid) - self.setLayout(form) - - def validateInput(self) -> bool: - mb = QtWidgets.QErrorMessage() - mb.setModal(True) - if self.id_input.text() == '': - mb.showMessage('Patient ID cannot be empty.') - mb.exec_() - return False - elif self.name_input.text() == '': - mb.showMessage('Patient name cannot be empty.') - mb.exec_() - return False - - return True - - def parseInput(self) -> bool: - if not self.validateInput(): - return False - else: - self.input_data["PatientID"] = self.id_input.text() - self.input_data["PatientName"] = self.name_input.text() - self.input_data["PatientSex"] = 'M' if self.sex_input.currentText() == 'Male' else 'F' - self.input_data["ImageComments"] = self.comments_input.toPlainText() - - return True - - def save(self) -> None: - if not self.parseInput(): - return - else: - filename = QtWidgets.QFileDialog.getSaveFileName(self, "Save DICOM", "../..", "*.dcm")[0] - if filename == '': - return - else: - fm = pd.Dataset() - - # CT Image Storage - fm.MediaStorageSOPClassUID = "1.2.840.10008.5.1.4.1.1.2" - fm.MediaStorageSOPInstanceUID = pd.uid.generate_uid() - fm.TransferSyntaxUID = pd.uid.ExplicitVRLittleEndian - fm.ImplementationClassUID = pd.uid.generate_uid() - - ds = pd.FileDataset(None, {}) - - ds.file_meta = fm - - # CT Image Storage - ds.SOPClassUID = "1.2.840.10008.5.1.4.1.1.2" - ds.SOPInstanceUID = pd.uid.generate_uid() - - ds.ContentDate = f"{self.date_time.year:04}{self.date_time.month:02}{self.date_time.day:02}" - ds.ContentTime = f"{self.date_time.hour:02}{self.date_time.minute:02}{self.date_time.second:02}" - - ds.StudyInstanceID = pd.uid.generate_uid() - ds.SeriesInstanceID = pd.uid.generate_uid() - - ds.Modality = "CT" - ds.ConversionType = 'WSD' # workstation - ds.ImageType = r"ORIGINAL\PRIMARY\AXIAL" - - ds.PatientName = self.input_data["PatientName"] - ds.PatientID = self.input_data["PatientID"] - ds.PatientSex = self.input_data["PatientSex"] - ds.ImageComments = self.input_data["ImageComments"] - - ds.PixelData = self.img.astype(np.uint8).tobytes() - ds.Rows, ds.Columns = self.img.shape - ds.SamplesPerPixel = 1 - ds.PixelRepresentation = 0 - ds.PhotometricInterpretation = "MONOCHROME2" - ds.BitsAllocated, ds.BitsStored = 8, 8 - ds.HighBit = 7 - - ds.is_little_endian = True - ds.is_implicit_VR = False - - ds.save_as(f"{filename}.dcm", write_like_original=False) - self.close() - - -class DicomShowDialog(QtWidgets.QDialog): - def __init__(self): - super().__init__(None) - self.setWindowTitle('Load DICOM file') - - self.img = None - self.ct_date_time = None - self.img_fig = None - self.patient_ID = None - self.patient_name = None - self.patient_sex = None - self.image_comments = None - - self.createLayout() - - def createLayout(self) -> None: - self.ct_date_time = QtWidgets.QLabel() - self.patient_ID = QtWidgets.QTextEdit() - self.patient_name = QtWidgets.QTextEdit() - self.patient_sex = QtWidgets.QTextEdit() - - form = QtWidgets.QFormLayout() - form.addRow(self.ct_date_time) - form.addRow(QtWidgets.QLabel('Patient ID'), self.patient_ID) - form.addRow(QtWidgets.QLabel('Patient name'), self.patient_name) - form.addRow(QtWidgets.QLabel('Patient sex'), self.patient_sex) - - self.img_fig = QtWidgets.QLabel(self) - - hbox = QtWidgets.QHBoxLayout() - hbox.setAlignment(Qt.AlignHCenter) - hbox.addWidget(self.img_fig) - - self.image_comments = QtWidgets.QTextEdit() - - close = QtWidgets.QPushButton('Close') - close.clicked.connect(self.close) - - grid = QtWidgets.QGridLayout() - grid.addWidget(close, 1, 1) - - vbox = QtWidgets.QVBoxLayout() - vbox.addLayout(form) - vbox.addLayout(hbox) - vbox.addWidget(self.image_comments) - vbox.addLayout(grid) - - self.setLayout(vbox) - - def exec_(self) -> None: - if self.load(): - super().exec_() - - def load(self) -> bool: - filename = QtWidgets.QFileDialog.getOpenFileName(self, "Open DICOM", "../..", "DICOM File (*.dcm)")[0] - if filename == '': - self.close() - return False - else: - ds = pydicom.dcmread(filename) - ds.file_meta.TransferSyntaxUID = pydicom.uid.ImplicitVRLittleEndian - self.img_fig.setPixmap(Conversion().array2qpixmap(ds.pixel_array)) - - year, month, day = ds.ContentDate[:4], ds.ContentDate[4:6], ds.ContentDate[6:8] - hour, minute, second = ds.ContentTime[:2], ds.ContentTime[2:4], ds.ContentTime[4:6] - - self.ct_date_time.setText(f"{year}/{month}/{day} {hour}:{minute}:{second}") - self.patient_name.setText(str(ds.PatientName)) - self.patient_ID.setText(str(ds.PatientID)) - self.patient_sex.setText(str(ds.PatientSex)) - self.image_comments.setText(str(ds.ImageComments)) - - return True diff --git a/gui/mainWindow/__init__.py b/gui/mainWindow/__init__.py deleted file mode 100644 index 9e8355f..0000000 --- a/gui/mainWindow/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .mainWindow import MainWindow diff --git a/gui/mainWindow/mainWindow.py b/gui/mainWindow/mainWindow.py deleted file mode 100644 index b502bcb..0000000 --- a/gui/mainWindow/mainWindow.py +++ /dev/null @@ -1,342 +0,0 @@ -import numpy as np -from typing import Any, List, Union, Tuple, Callable -from ct import CT, InteractiveCT -from PySide2.QtGui import QPixmap -from PySide2.QtCore import SIGNAL, QObject, Qt -import PySide2.QtWidgets as QtWidgets -from threading import Thread -from skimage import io -from math import sqrt -from os import path -from conversion import Conversion -from gui.dicomWindow import * -from datetime import datetime - - -class MainWindow(QtWidgets.QWidget): - def __init__(self): - QtWidgets.QWidget.__init__(self) - self.resize(800, 800) - self.ct_start_datetime: Union[None or datetime] = None - - self.inputs = { - "img": None, - "sinogram": None, - "result": None, - "fast_mode": False, - "rotate_angle": 1, - "theta_angle": 0, - "detectors_amount": 2, - "detectors_distance": 1, - "animation_img_frames": None, - "animation_sinogram_frames": None, - "animation_result_frames": None, - "animation_sinogram_actual_frame": None, - "animation_img_actual_frame": None, - "animation_result_actual_frame": None - } - - # First layout level - self.plots_layout = { - "object": QtWidgets.QGridLayout(), - "items": { - "img_fig": { - "object": QtWidgets.QLabel(self), - "position": (1, 1) - }, - "radon_fig": { - "object": QtWidgets.QLabel(self), - "position": (1, 2) - }, - "iradon_fig": { - "object": QtWidgets.QLabel(self), - "position": (1, 3), - }, - "animation_slider": { - "object": QtWidgets.QSlider(Qt.Horizontal), - "position": (2, 1), - "signal": "valueChanged(int)", - "slot": lambda x: self.setInputValue("animation_img_actual_frame", x / 100 if x < 99 else 1) - or self.setInputValue("animation_sinogram_actual_frame", x / 100 if x < 99 else 1) - or self.setInputValue("animation_result_actual_frame", x / 100 if x < 99 else 1) - or self.changeFrame("radon_fig") - or self.changeFrame("iradon_fig") - }, - "fast_mode": { - "object": QtWidgets.QCheckBox("Fast mode"), - "position": (2, 3), - "signal": "stateChanged(int)", - "slot": lambda x: self.setInputValue("fast_mode", False if not x else True) - # in the linux, for Qt, 2 means True - }, - } - } - self.buttons_layout = { - "object": QtWidgets.QVBoxLayout(), - "items": { - "load": { - "object": QtWidgets.QPushButton("Load"), - "position": 1, - "signal": "clicked()", - "slot": self.loadImg - }, - "run": { - "object": QtWidgets.QPushButton("Run"), - "position": 2, - "signal": "clicked()", - "slot": self.startComputerTomography - }, - "save_dicom": { - "object": QtWidgets.QPushButton("Save Dicom"), - "position": 3, - "signal": "clicked()", - "slot": self.saveDicom - }, - "show_dicom": { - "object": QtWidgets.QPushButton("Show Dicom"), - "position": 4, - "signal": "clicked()", - "slot": self.showDicom - } - } - } - self.inputs_layout = { - "object": QtWidgets.QGridLayout(), - "items": { - "rotate_angle_label": { - "object": QtWidgets.QLabel("Rotate angle"), - "position": (1, 1) - }, - "rotate_angle": { - "object": QtWidgets.QSpinBox(), - "position": (1, 2), - "signal": "valueChanged(int)", - "slot": lambda x: self.setInputValue("rotate_angle", x) - }, - "theta_label": { - "object": QtWidgets.QLabel("Theta angle"), - "position": (2, 1) - }, - "start_angle": { - "object": QtWidgets.QSpinBox(), - "position": (2, 2), - "signal": "valueChanged(int)", - "slot": lambda x: self.setInputValue("theta_angle", x) - }, - "detectors_num_label": { - "object": QtWidgets.QLabel("Detectors amount"), - "position": (3, 1) - }, - "detectors_num": { - "object": QtWidgets.QSpinBox(), - "position": (3, 2), - "signal": "valueChanged(int)", - "slot": lambda x: self.setInputValue("detectors_amount", x) - }, - "detectors_distance_label": { - "object": QtWidgets.QLabel("Distance"), - "position": (4, 1) - }, - "detectors_distance": { - "object": QtWidgets.QSpinBox(), - "position": (4, 2), - "signal": "valueChanged(int)", - "slot": lambda x: self.setInputValue("detectors_distance", x) - } - } - } - - # Second layout level - self.aggregated_layouts = { - "object": QtWidgets.QVBoxLayout(), - "items": { - "first": { - "reference": self.plots_layout, - "position": 1 - }, - "second": { - "object": QtWidgets.QGridLayout(), - "position": 2, - "items": { - "left": { - "reference": self.inputs_layout, - "position": (1, 1) - }, - "empty": { - "object": QtWidgets.QWidget(), - "position": (1, 2) - }, - "right": { - "reference": self.buttons_layout, - "position": (1, 3) - } - } - } - } - } - - self.plots_layout["items"]["img_fig"]["object"].setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored) - self.plots_layout["items"]["img_fig"]["object"].setScaledContents(True) - self.plots_layout["items"]["img_fig"]["object"].setFrameStyle(QtWidgets.QFrame.Panel | QtWidgets.QFrame.Plain) - self.plots_layout["items"]["img_fig"]["object"].setLineWidth(1) - - self.plots_layout["items"]["radon_fig"]["object"].setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored) - self.plots_layout["items"]["radon_fig"]["object"].setScaledContents(True) - self.plots_layout["items"]["radon_fig"]["object"].setFrameStyle(QtWidgets.QFrame.Panel | QtWidgets.QFrame.Plain) - self.plots_layout["items"]["radon_fig"]["object"].setLineWidth(1) - - self.plots_layout["items"]["iradon_fig"]["object"].setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored) - self.plots_layout["items"]["iradon_fig"]["object"].setScaledContents(True) - self.plots_layout["items"]["iradon_fig"]["object"].setFrameStyle(QtWidgets.QFrame.Panel | QtWidgets.QFrame.Plain) - self.plots_layout["items"]["iradon_fig"]["object"].setLineWidth(1) - - self.plots_layout["items"]["animation_slider"]["object"].setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - self.plots_layout["items"]["animation_slider"]["object"].setDisabled(True) - - self.inputs_layout["items"]["start_angle"]["object"].setMinimum(0) - self.inputs_layout["items"]["start_angle"]["object"].setMaximum(360) - self.inputs_layout["items"]["rotate_angle"]["object"].setMinimum(1) - self.inputs_layout["items"]["rotate_angle"]["object"].setMaximum(179) - self.inputs_layout["items"]["detectors_num"]["object"].setMinimum(2) - self.inputs_layout["items"]["detectors_num"]["object"].setMaximum(1000) - self.inputs_layout["items"]["detectors_distance"]["object"].setMinimum(1) - self.inputs_layout["items"]["detectors_distance"]["object"].setDisabled(True) - - self.buttons_layout["items"]["run"]["object"].setDisabled(True) - self.buttons_layout["items"]["save_dicom"]["object"].setDisabled(True) - - self.createLayout(self.aggregated_layouts) - self.setLayout(self.aggregated_layouts["object"]) - - def createLayout(self, layout: Any) -> None: - def addIf(operation: Callable, widget_dict: dict, widget_object, position: Tuple[int]): - if "position" in widget_dict: - try: - operation(widget_object, *position) - except: - operation(widget_object, position) - else: - operation(widget_object) - - # Python checks function argument, cannot pass undefined or unnecessary args ... - for widget in layout["items"].values(): - if "items" in widget: - self.createLayout(widget) - addIf(layout["object"].addLayout, widget, widget["object"], widget["position"]) - elif "reference" in widget: - self.createLayout(widget["reference"]) - addIf(layout["object"].addLayout, widget, widget["reference"]["object"], widget["position"]) - else: # widget - addIf(layout["object"].addWidget, widget, widget["object"], widget["position"]) - if "signal" and "slot" in widget: - QObject.connect(widget["object"], SIGNAL(widget["signal"]), widget["slot"]) - - def loadImg(self) -> None: - filename = QtWidgets.QFileDialog.getOpenFileName(self, "Open img", "../..", "Image Files (*.png *.jpg *.bmp)") - if filename[0] == '': - return - else: - img = io.imread(path.expanduser(filename[0])) # rgb - - circle_inside_img_radius = sqrt(img.shape[0] ** 2 + img.shape[1] ** 2) // 2 - - fig = self.plots_layout["items"]["img_fig"]["object"] - fig.setPixmap(Conversion().array2qpixmap(img)) - - self.inputs["img"] = Conversion().rgb2greyscale(img) - self.inputs_layout["items"]["detectors_distance"]["object"].setMaximum(2 * circle_inside_img_radius) - self.inputs_layout["items"]["detectors_distance"]["object"].setEnabled(True) - self.buttons_layout["items"]["run"]["object"].setEnabled(True) - - self.plots_layout["items"]["animation_slider"]["object"].setDisabled(True) - - def startComputerTomography(self) -> None: - runStatus = self.buttons_layout["items"]["run"]["object"].isEnabled() - loadStauts = self.buttons_layout["items"]["load"]["object"].isEnabled() - - self.buttons_layout["items"]["run"]["object"].setEnabled(False) - self.buttons_layout["items"]["load"]["object"].setEnabled(False) - self.buttons_layout["items"]["save_dicom"]["object"].setEnabled(False) - - # parallel task - def task(): - try: - selectedCT = CT if self.inputs["fast_mode"] else InteractiveCT - - ct = selectedCT(self.inputs["img"], self.inputs["rotate_angle"], self.inputs["theta_angle"], - self.inputs["detectors_amount"], self.inputs["detectors_distance"]) - - self.ct_start_datetime = datetime.now() - self.inputs["sinogram"], self.inputs["result"] = ct.run() - self.normalizeImg(self.inputs["sinogram"]) - - if not self.inputs["fast_mode"]: - frames = ct.getFrames() - self.inputs["animation_sinogram_frames"], self.inputs["animation_result_frames"] = frames - - self.inputs["animation_sinogram_frames"].append(self.inputs["sinogram"]) - self.inputs["animation_result_frames"].append(self.inputs["result"]) - - self.preprocessFrames(self.inputs["animation_sinogram_frames"]) - self.preprocessFrames(self.inputs["animation_result_frames"]) - - self.plots_layout["items"]["animation_slider"]["object"].setEnabled(True) - self.plots_layout["items"]["animation_slider"]["object"].setValue(100) - else: - self.plots_layout["items"]["animation_slider"]["object"].setDisabled(True) - - self.plots_layout["items"]["radon_fig"]["object"].setPixmap( - self.preprocessFrame(self.inputs["sinogram"])) - self.plots_layout["items"]["iradon_fig"]["object"].setPixmap( - self.preprocessFrame(self.inputs["result"])) - - except Exception as msg: - QtWidgets.QErrorMessage().showMessage(str(msg)) - finally: - self.buttons_layout["items"]["run"]["object"].setEnabled(runStatus) - self.buttons_layout["items"]["load"]["object"].setEnabled(loadStauts) - self.buttons_layout["items"]["save_dicom"]["object"].setEnabled(True) - - thread = Thread(target=task) - thread.start() - - @staticmethod - def preprocessFrames(frames: List[np.ndarray]) -> None: - conv = Conversion(frames[0].shape) - for index, img in enumerate(frames): - frames[index] = conv.array2qpixmap(conv.greyscale2rgb(img)) - - @staticmethod - def preprocessFrame(frame: np.ndarray) -> QPixmap: - conv = Conversion() - return conv.array2qpixmap(conv.greyscale2rgb(frame)) - - @staticmethod - def normalizeImg(img: np.ndarray) -> None: - img *= 255 - - def setInputValue(self, key: str, value: Any) -> None: - self.inputs[key] = value - - def changeFrame(self, label_type: str) -> None: - if label_type == "radon_fig": - frame_id = self.inputs["animation_sinogram_actual_frame"] * (len(self.inputs["animation_sinogram_frames"]) - 1) - frame_id = round(frame_id) - frame = self.inputs["animation_sinogram_frames"][frame_id] - - fig = self.plots_layout["items"]["radon_fig"]["object"] - fig.setPixmap(frame) - elif label_type == "iradon_fig": - frame_id = self.inputs["animation_result_actual_frame"] * (len(self.inputs["animation_result_frames"]) - 1) - frame_id = round(frame_id) - frame = self.inputs["animation_result_frames"][frame_id] - - fig = self.plots_layout["items"]["iradon_fig"]["object"] - fig.setPixmap(frame) - - @staticmethod - def showDicom() -> None: - DicomShowDialog().exec_() - - def saveDicom(self) -> None: - DicomSaveDialog(self.inputs["result"], self.ct_start_datetime).exec_()