Пример #1
0
    def execute(self, context):
        counter_start = Tcounter()
        TerminalProgressBar = BDENTAL_Utils.TerminalProgressBar
        CV2_progress_bar = BDENTAL_Utils.CV2_progress_bar
        self.t1 = threading.Thread(target=TerminalProgressBar,
                                   args=[self.q, counter_start],
                                   daemon=True)
        self.t2 = threading.Thread(target=CV2_progress_bar,
                                   args=[self.q],
                                   daemon=True)

        # self.t1.start()
        self.t2.start()
        self.DicomToMesh()
        # self.t1.join()
        self.t2.join()
        # print("\n")
        # print(self.TimingDict)

        return {"FINISHED"}
Пример #2
0
    def DicomToMesh(self):
        counter_start = Tcounter()
        self.q.put(
            ["GuessTime", "PROGRESS : Extracting mesh...", "", 0.0, 0.1, 2])
        # Load Infos :
        #########################################################################
        BDENTAL_Props = bpy.context.scene.BDENTAL_Props
        # NrrdHuPath = BDENTAL_Props.NrrdHuPath
        Nrrd255Path = self.Nrrd255Path
        print(Nrrd255Path)
        UserProjectDir = AbsPath(BDENTAL_Props.UserProjectDir)
        DcmInfo = self.DcmInfo
        Origin = DcmInfo["Origin"]
        VtkTransform_4x4 = DcmInfo["VtkTransform_4x4"]
        VtkMatrix = list(np.array(VtkTransform_4x4).ravel())
        Treshold = self.Treshold

        StlPath = join(UserProjectDir, f"{self.SegmentName}_SEGMENTATION.stl")
        Thikness = 1
        # Reduction = 0.9
        SmoothIterations = SmthIter = 5

        ############### step 1 : Reading DICOM #########################
        # self.q.put(["GuessTime", "PROGRESS : Reading DICOM...", "", 0, 0.1, 1])

        Image3D = sitk.ReadImage(Nrrd255Path)
        Sz = Image3D.GetSize()
        Sp = Image3D.GetSpacing()
        MaxSp = max(Vector(Sp))
        if MaxSp < 0.3:
            SampleRatio = round(MaxSp / 0.3, 2)
            ResizedImage = ResizeImage(sitkImage=Image3D, Ratio=SampleRatio)
            Image3D = ResizedImage
            # print(f"Image DOWN Sampled : SampleRatio = {SampleRatio}")

        # Convert Hu treshold value to 0-255 UINT8 :
        Treshold255 = HuTo255(Hu=Treshold, Wmin=Wmin, Wmax=Wmax)
        if Treshold255 == 0:
            Treshold255 = 1
        elif Treshold255 == 255:
            Treshold255 = 254

        step1 = Tcounter()
        self.TimingDict["Read DICOM"] = step1 - counter_start
        # print(f"step 1 : Read DICOM ({step1-start})")

        ############### step 2 : Extracting mesh... #########################
        # self.q.put(["GuessTime", "PROGRESS : Extracting mesh...", "", 0.0, 0.1, 2])

        # print("Extracting mesh...")
        vtkImage = sitkTovtk(sitkImage=Image3D)

        ExtractedMesh = vtk_MC_Func(vtkImage=vtkImage, Treshold=Treshold255)
        Mesh = ExtractedMesh

        polysCount = Mesh.GetNumberOfPolys()
        polysLimit = 800000

        # step1 = Tcounter()
        # print(f"before reduction finished in : {step1-start} secondes")
        step2 = Tcounter()
        self.TimingDict["extract mesh"] = step2 - step1
        # print(f"step 2 : extract mesh ({step2-step1})")

        ############### step 3 : mesh Reduction... #########################
        if polysCount > polysLimit:
            # print(f"Hight polygons count, : ({polysCount}) Mesh will be reduced...")
            Reduction = round(1 - (polysLimit / polysCount), 2)
            # print(f"MESH REDUCTION: Ratio = ({Reduction}) ...")
            ReductedMesh = vtkMeshReduction(
                q=self.q,
                mesh=Mesh,
                reduction=Reduction,
                step="Mesh Reduction",
                start=0.1,
                finish=0.7,
            )
            Mesh = ReductedMesh
            # print(f"Reduced Mesh polygons count : {Mesh.GetNumberOfPolys()} ...")
            # step2 = Tcounter()
            # print(f"reduction finished in : {step2-step1} secondes")
        # else:
        # print(f"Original mesh polygons count is Optimal : ({polysCount})...")
        step3 = Tcounter()
        self.TimingDict["Reduct mesh"] = step3 - step2
        # print(f"step 3 : Reduct mesh ({step3-step2})")

        ############### step 4 : mesh Smoothing... #########################
        # print("SMOOTHING...")
        SmoothedMesh = vtkSmoothMesh(
            q=self.q,
            mesh=Mesh,
            Iterations=SmthIter,
            step="Mesh Orientation",
            start=0.7,
            finish=0.75,
        )
        step3 = Tcounter()
        # try:
        #     print(f"SMOOTHING finished in : {step3-step2} secondes...")
        # except Exception:
        #     print(f"SMOOTHING finished in : {step3-step1} secondes (no Reduction!)...")
        step4 = Tcounter()
        self.TimingDict["Smooth mesh"] = step4 - step3
        # print(f"step 4 : Smooth mesh ({step4-step3})")

        ############### step 5 : Set mesh orientation... #########################
        # print("SET MESH ORIENTATION...")
        TransformedMesh = vtkTransformMesh(
            mesh=SmoothedMesh,
            Matrix=VtkMatrix,
        )
        step5 = Tcounter()
        self.TimingDict["Mesh Transform"] = step5 - step4
        # print(f"step 5 : set mesh orientation({step5-step4})")

        ############### step 6 : exporting mesh stl... #########################
        self.q.put([
            "GuessTime",
            "PROGRESS : exporting mesh stl...",
            "",
            0.75,
            0.8,
            2,
        ])

        # print("WRITING...")
        writer = vtk.vtkSTLWriter()
        writer.SetInputData(TransformedMesh)
        writer.SetFileTypeToBinary()
        writer.SetFileName(StlPath)
        writer.Write()

        # step4 = Tcounter()
        # print(f"WRITING finished in : {step4-step3} secondes")
        step6 = Tcounter()
        self.TimingDict["Export mesh"] = step6 - step5
        # print(f"step 6 : Export mesh ({step6-step5})")

        ############### step 7 : Importing mesh to Blender... #########################
        self.q.put(
            ["GuessTime", "PROGRESS : Importing mesh...", "", 0.8, 0.95, 8])

        # print("IMPORTING...")
        # import stl to blender scene :
        bpy.ops.import_mesh.stl(filepath=StlPath)
        obj = bpy.context.object
        obj.name = f"{self.Preffix}_{self.SegmentName}_SEGMENTATION"
        obj.data.name = f"{self.Preffix}_{self.SegmentName}_mesh"

        bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY", center="MEDIAN")

        step7 = Tcounter()
        self.TimingDict["Import mesh"] = step7 - step6
        # print(f"step 7 : Import mesh({step7-step6})")
        ############### step 8 : Add material... #########################
        self.q.put(
            ["GuessTime", "PROGRESS : Add material...", "", 0.95, 0.99, 2])

        # print("ADD COLOR MATERIAL")
        mat = bpy.data.materials.get(obj.name) or bpy.data.materials.new(
            obj.name)
        mat.diffuse_color = self.SegmentColor
        obj.data.materials.append(mat)
        MoveToCollection(obj=obj, CollName="SEGMENTS")
        bpy.ops.object.shade_smooth()

        bpy.ops.object.modifier_add(type="CORRECTIVE_SMOOTH")
        bpy.context.object.modifiers["CorrectiveSmooth"].iterations = 3
        bpy.context.object.modifiers["CorrectiveSmooth"].use_only_smooth = True

        # step5 = Tcounter()
        # print(f"Blender importing finished in : {step5-step4} secondes")

        step8 = Tcounter()
        self.TimingDict["Add material"] = step8 - step7
        # print(f"step 8 : Add material({step8-step7})")

        self.q.put(["End"])
        counter_finish = Tcounter()
        self.TimingDict["Total Time"] = counter_finish - counter_start
Пример #3
0
def Load_Dicom_funtion(context, q):

    ################################################################################################
    start = Tcounter()
    ################################################################################################
    BDENTAL_Props = context.scene.BDENTAL_Props
    UserProjectDir = AbsPath(BDENTAL_Props.UserProjectDir)
    UserDcmDir = AbsPath(BDENTAL_Props.UserDcmDir)

    ################################################################################################

    if not exists(UserProjectDir):

        message = ["The Selected Project Directory Path is not valid ! "]
        ShowMessageBox(message=message, icon="COLORSET_02_VEC")
        return {"CANCELLED"}

    elif not exists(UserDcmDir):

        message = [" The Selected Dicom Directory Path is not valid ! "]
        ShowMessageBox(message=message, icon="COLORSET_02_VEC")
        return {"CANCELLED"}

    elif not os.listdir(UserDcmDir):
        message = ["No valid DICOM Serie found in DICOM Folder ! "]
        ShowMessageBox(message=message, icon="COLORSET_02_VEC")
        return {"CANCELLED"}

    else:
        # Get Preffix and save file :
        DcmInfoDict = eval(BDENTAL_Props.DcmInfo)
        Preffixs = list(DcmInfoDict.keys())

        for i in range(1, 100):
            Preffix = f"BD{i:03}"
            if not Preffix in Preffixs:
                break
        ########################################################
        # SceneVolumes = [obj for obj in bpy.context.scene.objects if obj.name.startswith("BD") and obj.name.endswith("_CTVolume")]
        # if not SceneVolumes :
        #     Preffix = 'BD001'
        # else :
        #     for i in range(1,100) :
        #         Preffix = f"BD{i:03}"

        #         SceneVolPreffixs = [Vol.name[:5] for Vol in SceneVolumes]
        #         if not Preffix in SceneVolPreffixs :
        #             break
        # CleanScanData(Preffix)
        #################################################################
        Split = split(UserProjectDir)
        ProjectName = (Split[-1] or Split[-2])
        BlendFile = f"{ProjectName}_CT-SCAN.blend"
        Blendpath = join(UserProjectDir, BlendFile)

        if not exists(
                Blendpath) or bpy.context.blend_data.filepath == Blendpath:
            bpy.ops.wm.save_as_mainfile(filepath=Blendpath)
        else:
            bpy.ops.wm.save_mainfile()

        # Start Reading Dicom data :
        ######################################################################################
        Series_reader = sitk.ImageSeriesReader()
        MaxSerie, MaxCount = GetMaxSerie(UserDcmDir)
        DcmSerie = Series_reader.GetGDCMSeriesFileNames(UserDcmDir, MaxSerie)

        ##################################### debug_02 ###################################
        debug_01 = Tcounter()
        message = f"MaxSerie ID : {MaxSerie}, MaxSerie Count : {MaxCount} (Time : {round(debug_01-start,2)} secondes)"
        print(message)
        # q.put("Max DcmSerie extracted...")
        ####################################################################################

        # Get StudyInfo :
        reader = sitk.ImageFileReader()
        reader.SetFileName(DcmSerie[0])
        reader.LoadPrivateTagsOn()
        reader.ReadImageInformation()

        Image3D = sitk.ReadImage(DcmSerie)

        # Get Dicom Info :
        Sp = Spacing = Image3D.GetSpacing()
        Sz = Size = Image3D.GetSize()
        Dims = Dimensions = Image3D.GetDimension()
        Origin = Image3D.GetOrigin()
        Direction = Image3D.GetDirection()

        # calculate Informations :
        D = Direction
        O = Origin
        DirectionMatrix_4x4 = Matrix((
            (D[0], D[1], D[2], 0.0),
            (D[3], D[4], D[5], 0.0),
            (D[6], D[7], D[8], 0.0),
            (0.0, 0.0, 0.0, 1.0),
        ))

        TransMatrix_4x4 = Matrix((
            (1.0, 0.0, 0.0, O[0]),
            (0.0, 1.0, 0.0, O[1]),
            (0.0, 0.0, 1.0, O[2]),
            (0.0, 0.0, 0.0, 1.0),
        ))

        VtkTransform_4x4 = TransMatrix_4x4 @ DirectionMatrix_4x4
        P0 = Image3D.TransformContinuousIndexToPhysicalPoint((0, 0, 0))
        P_diagonal = Image3D.TransformContinuousIndexToPhysicalPoint(
            (Sz[0] - 1, Sz[1] - 1, Sz[2] - 1))
        VCenter = (Vector(P0) + Vector(P_diagonal)) * 0.5

        C = VCenter

        TransformMatrix = Matrix((
            (D[0], D[1], D[2], C[0]),
            (D[3], D[4], D[5], C[1]),
            (D[6], D[7], D[8], C[2]),
            (0.0, 0.0, 0.0, 1.0),
        ))

        # Set DcmInfo :

        DcmInfo = {
            "UserProjectDir": RelPath(UserProjectDir),
            "Preffix": Preffix,
            "RenderSz": Sz,
            "RenderSp": Sp,
            "PixelType": Image3D.GetPixelIDTypeAsString(),
            "Wmin": Wmin,
            "Wmax": Wmax,
            "Size": Sz,
            "Dims": Dims,
            "Spacing": Sp,
            "Origin": Origin,
            "Direction": Direction,
            "TransformMatrix": TransformMatrix,
            "DirectionMatrix_4x4": DirectionMatrix_4x4,
            "TransMatrix_4x4": TransMatrix_4x4,
            "VtkTransform_4x4": VtkTransform_4x4,
            "VolumeCenter": VCenter
        }

        tags = {
            "StudyDate": "0008|0020",
            "PatientName": "0010|0010",
            "PatientID": "0010|0020",
            "BirthDate": "0010|0030",
            "WinCenter": "0028|1050",
            "WinWidth": "0028|1051",
        }
        for k, tag in tags.items():

            if tag in reader.GetMetaDataKeys():
                v = reader.GetMetaData(tag)

            else:
                v = ""

            DcmInfo[k] = v
            Image3D.SetMetaData(tag, v)

        ###################################### debug_02 ##################################
        debug_02 = Tcounter()
        message = f"DcmInfo {Preffix} set (Time : {debug_02-debug_01} secondes)"
        print(message)
        # q.put("Dicom Info extracted...")
        ##################################################################################

        #######################################################################################
        # Add directories :
        SlicesDir = join(UserProjectDir, "Slices")
        if not exists(SlicesDir):
            os.makedirs(SlicesDir)
        DcmInfo["SlicesDir"] = RelPath(SlicesDir)

        PngDir = join(UserProjectDir, "PNG")
        if not exists(PngDir):
            os.makedirs(PngDir)

        Nrrd255Path = join(UserProjectDir, f"{Preffix}_Image3D255.nrrd")

        DcmInfo["Nrrd255Path"] = RelPath(Nrrd255Path)

        #######################################################################################
        # set IntensityWindowing  :
        Image3D_255 = sitk.Cast(
            sitk.IntensityWindowing(
                Image3D,
                windowMinimum=Wmin,
                windowMaximum=Wmax,
                outputMinimum=0.0,
                outputMaximum=255.0,
            ),
            sitk.sitkUInt8,
        )

        # Convert Dicom to nrrd file :
        # sitk.WriteImage(Image3D, NrrdHuPath)
        sitk.WriteImage(Image3D_255, Nrrd255Path)

        ################################## debug_03 ######################################
        debug_03 = Tcounter()
        message = (
            f"Nrrd255 Export done!  (Time : {debug_03-debug_02} secondes)")
        print(message)

        # q.put("nrrd 3D image file saved...")
        ##################################################################################

        #############################################################################################
        # MultiThreading PNG Writer:
        #########################################################################################
        def Image3DToPNG(i, slices, PngDir, Preffix):
            img_Slice = slices[i]
            img_Name = f"{Preffix}_img{i:04}.png"
            image_path = join(PngDir, img_Name)
            cv2.imwrite(image_path, img_Slice)
            image = bpy.data.images.load(image_path)
            image.pack()
            # print(f"{img_Name} was processed...")

        #########################################################################################
        # Get slices list :
        MaxSp = max(Vector(Sp))
        if MaxSp < 0.25:
            SampleRatio = round(MaxSp / 0.25, 2)
            Image3D_255 = ResizeImage(sitkImage=Image3D_255, Ratio=SampleRatio)
            DcmInfo["RenderSz"] = Image3D_255.GetSize()
            DcmInfo["RenderSp"] = Image3D_255.GetSpacing()

        Array = sitk.GetArrayFromImage(Image3D_255)
        slices = [np.flipud(Array[i, :, :]) for i in range(Array.shape[0])]
        # slices = [Image3D_255[:, :, i] for i in range(Image3D_255.GetDepth())]

        threads = [
            threading.Thread(
                target=Image3DToPNG,
                args=[i, slices, PngDir, Preffix],
                daemon=True,
            ) for i in range(len(slices))
        ]

        for t in threads:
            t.start()

        for t in threads:
            t.join()

        # os.removedirs(PngDir)
        shutil.rmtree(PngDir)
        DcmInfo["CT_Loaded"] = True
        # Set DcmInfo property :
        DcmInfoDict = eval(BDENTAL_Props.DcmInfo)
        DcmInfoDict[Preffix] = DcmInfo
        BDENTAL_Props.DcmInfo = str(DcmInfoDict)
        BDENTAL_Props.UserProjectDir = RelPath(BDENTAL_Props.UserProjectDir)
        bpy.ops.wm.save_mainfile()
        #################################### debug_04 ####################################
        debug_04 = Tcounter()
        message = (
            f"PNG images exported (Time : {debug_04-debug_03} secondes)")
        print(message)
        # q.put("PNG images saved...")
        ##################################################################################

        #################################### debug_05 ####################################
        debug_05 = Tcounter()
        message = f"{Preffix}_CT-SCAN.blend saved (Time = {debug_05-debug_04} secondes)"
        print(message)
        # q.put("Blender project saved...")
        ##################################################################################

        #############################################################################################
        finish = Tcounter()
        message = f"Data Loaded in {finish-start} secondes"
        print(message)
        # q.put(message)
        #############################################################################################
        message = ["DICOM loaded successfully. "]
        ShowMessageBox(message=message, icon="COLORSET_03_VEC")

        return DcmInfo
Пример #4
0
    def execute(self, context):

        Start = Tcounter()
        print("Data Loading START...")

        global ShadersBlendFile
        global GpShader

        BDENTAL_Props = context.scene.BDENTAL_Props

        DataType = BDENTAL_Props.DataType
        if DataType == "DICOM Series":
            DcmInfo = Load_Dicom_funtion(context, self.q)
        if DataType == "3D Image File":
            DcmInfo = Load_3DImage_function(context, self.q)

        UserProjectDir = AbsPath(BDENTAL_Props.UserProjectDir)
        Preffix = DcmInfo["Preffix"]
        Wmin = DcmInfo["Wmin"]
        Wmax = DcmInfo["Wmax"]
        # PngDir = AbsPath(BDENTAL_Props.PngDir)
        print("\n##########################\n")
        print("Data Loading START...")
        VolumeRender(DcmInfo, GpShader, ShadersBlendFile)
        scn = bpy.context.scene
        scn.render.engine = "BLENDER_EEVEE"
        BDENTAL_Props.GroupNodeName = GpShader

        if GpShader == "VGS_Marcos_modified":
            GpNode = bpy.data.node_groups.get(f"{Preffix}_{GpShader}")
            Low_Treshold = GpNode.nodes["Low_Treshold"].outputs[0]
            Low_Treshold.default_value = 600
            WminNode = GpNode.nodes["WminNode"].outputs[0]
            WminNode.default_value = Wmin
            WmaxNode = GpNode.nodes["WmaxNode"].outputs[0]
            WmaxNode.default_value = Wmax

            # newdriver = Low_Treshold.driver_add("default_value")
            # newdriver.driver.type = "AVERAGE"
            # var = newdriver.driver.variables.new()
            # var.name = "Treshold"
            # var.type = "SINGLE_PROP"
            # var.targets[0].id_type = "SCENE"
            # var.targets[0].id = bpy.context.scene
            # var.targets[0].data_path = "BDENTAL_Props.Treshold"
            # newdriver.driver.expression = "Treshold"

        if GpShader == "VGS_Dakir_01":
            # Add Treshold Driver :
            GpNode = bpy.data.node_groups.get(f"{Preffix}_{GpShader}")
            value = (600 - Wmin) / (Wmax - Wmin)
            treshramp = GpNode.nodes["TresholdRamp"].color_ramp.elements[
                0] = value

            # newdriver = treshramp.driver_add("position")
            # newdriver.driver.type = "SCRIPTED"
            # var = newdriver.driver.variables.new()
            # var.name = "Treshold"
            # var.type = "SINGLE_PROP"
            # var.targets[0].id_type = "SCENE"
            # var.targets[0].id = bpy.context.scene
            # var.targets[0].data_path = "BDENTAL_Props.Treshold"
            # newdriver.driver.expression = f"(Treshold-{Wmin})/{Wmax-Wmin}"

        BDENTAL_Props.CT_Rendered = True
        bpy.ops.view3d.view_selected(use_all_regions=False)
        bpy.ops.wm.save_mainfile()

        # post_handlers = bpy.app.handlers.depsgraph_update_post
        # [
        #     post_handlers.remove(h)
        #     for h in post_handlers
        #     if h.__name__ == "BDENTAL_TresholdUpdate"
        # ]
        # post_handlers.append(BDENTAL_TresholdUpdate)

        # bpy.ops.wm.save_mainfile()

        Finish = Tcounter()

        print(f"Finished (Time : {Finish-Start}")

        return {"FINISHED"}
Пример #5
0
def Load_3DImage_function(context, q):

    BDENTAL_Props = context.scene.BDENTAL_Props
    UserProjectDir = AbsPath(BDENTAL_Props.UserProjectDir)
    UserImageFile = AbsPath(BDENTAL_Props.UserImageFile)

    CtVolumeList = [
        obj for obj in bpy.context.scene.objects
        if obj.name.startswith("BD") and obj.name.endswith("_CTVolume")
    ]
    Preffix = f"BD{(len(CtVolumeList)+1):02}"

    #######################################################################################
    # 1rst check if paths are valid and supported :

    if not exists(UserProjectDir):

        message = ["The Selected Project Directory Path is not valid ! "]
        ShowMessageBox(message=message, icon="COLORSET_02_VEC")
        return {"CANCELLED"}

    if not exists(UserImageFile):
        message = [" The Selected Image File Path is not valid ! "]

        ShowMessageBox(message=message, icon="COLORSET_02_VEC")
        return {"CANCELLED"}

    reader = sitk.ImageFileReader()
    IO = reader.GetImageIOFromFileName(UserImageFile)
    FileExt = os.path.splitext(UserImageFile)[1]

    if not IO:
        message = [
            f"{FileExt} files are not Supported! for more info about supported files please refer to Addon wiki "
        ]
        ShowMessageBox(message=message, icon="COLORSET_01_VEC")
        return {"CANCELLED"}

    Image3D = sitk.ReadImage(UserImageFile)
    Depth = Image3D.GetDepth()

    if Depth == 0:
        message = [
            "Can't Build 3D Volume from 2D Image !",
            "for more info about supported files,",
            "please refer to Addon wiki"
        ]
        ShowMessageBox(message=message, icon="COLORSET_01_VEC")
        return {"CANCELLED"}

    ImgFileName = os.path.split(UserImageFile)[1]
    BDENTAL_nrrd = HU_Image = False
    if ImgFileName.startswith("BD") and ImgFileName.endswith(
            "_Image3D255.nrrd"):
        BDENTAL_nrrd = True
    if Image3D.GetPixelIDTypeAsString() in [
            "32-bit signed integer", "16-bit signed integer"
    ]:
        HU_Image = True

    if not BDENTAL_nrrd and not HU_Image:
        message = [
            "Only Images with Hunsfield data or BDENTAL nrrd images are supported !"
        ]
        ShowMessageBox(message=message, icon="COLORSET_01_VEC")
        return {"CANCELLED"}
    ###########################################################################################################

    else:

        start = Tcounter()
        ####################################
        Image3D = sitk.ReadImage(UserImageFile)
        # Get Dicom Info :
        Sp = Spacing = Image3D.GetSpacing()
        Sz = Size = Image3D.GetSize()
        Dims = Dimensions = Image3D.GetDimension()
        Origin = Image3D.GetOrigin()
        Direction = Image3D.GetDirection()

        # calculate Informations :
        D = Direction
        O = Origin
        DirectionMatrix_4x4 = Matrix((
            (D[0], D[1], D[2], 0.0),
            (D[3], D[4], D[5], 0.0),
            (D[6], D[7], D[8], 0.0),
            (0.0, 0.0, 0.0, 1.0),
        ))

        TransMatrix_4x4 = Matrix((
            (1.0, 0.0, 0.0, O[0]),
            (0.0, 1.0, 0.0, O[1]),
            (0.0, 0.0, 1.0, O[2]),
            (0.0, 0.0, 0.0, 1.0),
        ))

        VtkTransform_4x4 = TransMatrix_4x4 @ DirectionMatrix_4x4
        P0 = Image3D.TransformContinuousIndexToPhysicalPoint((0, 0, 0))
        P_diagonal = Image3D.TransformContinuousIndexToPhysicalPoint(
            (Sz[0] - 1, Sz[1] - 1, Sz[2] - 1))
        VCenter = (Vector(P0) + Vector(P_diagonal)) * 0.5

        C = VCenter

        TransformMatrix = Matrix((
            (D[0], D[1], D[2], C[0]),
            (D[3], D[4], D[5], C[1]),
            (D[6], D[7], D[8], C[2]),
            (0.0, 0.0, 0.0, 1.0),
        ))

        # Set DcmInfo :

        DcmInfo = {
            "Preffix": Preffix,
            "RenderSz": Sz,
            "RenderSp": Sp,
            "PixelType": Image3D.GetPixelIDTypeAsString(),
            "Wmin": Wmin,
            "Wmax": Wmax,
            "Size": Sz,
            "Dims": Dims,
            "Spacing": Sp,
            "Origin": Origin,
            "Direction": Direction,
            "TransformMatrix": TransformMatrix,
            "DirectionMatrix_4x4": DirectionMatrix_4x4,
            "TransMatrix_4x4": TransMatrix_4x4,
            "VtkTransform_4x4": VtkTransform_4x4,
            "VolumeCenter": VCenter
        }

        tags = {
            "StudyDate": "0008|0020",
            "PatientName": "0010|0010",
            "PatientID": "0010|0020",
            "BirthDate": "0010|0030",
            "WinCenter": "0028|1050",
            "WinWidth": "0028|1051",
        }
        reader = sitk.ImageFileReader()
        reader.SetFileName(UserImageFile)
        reader.LoadPrivateTagsOn()
        reader.ReadImageInformation()

        for k, tag in tags.items():

            if tag in reader.GetMetaDataKeys():
                v = reader.GetMetaData(tag)

            else:
                v = ""

            DcmInfo[k] = v
            Image3D.SetMetaData(tag, v)

        #######################################################################################
        # Add directories :
        SlicesDir = join(UserProjectDir, "Slices")
        if not exists(SlicesDir):
            os.makedirs(SlicesDir)
        DcmInfo["SlicesDir"] = RelPath(SlicesDir)

        PngDir = join(UserProjectDir, "PNG")
        if not exists(PngDir):
            os.makedirs(PngDir)

        Nrrd255Path = join(UserProjectDir, f"{Preffix}_Image3D255.nrrd")

        DcmInfo["Nrrd255Path"] = RelPath(Nrrd255Path)

        if BDENTAL_nrrd:
            Image3D_255 = Image3D

        else:

            #######################################################################################
            # set IntensityWindowing  :
            Image3D_255 = sitk.Cast(
                sitk.IntensityWindowing(
                    Image3D,
                    windowMinimum=Wmin,
                    windowMaximum=Wmax,
                    outputMinimum=0.0,
                    outputMaximum=255.0,
                ),
                sitk.sitkUInt8,
            )

        # Convert Dicom to nrrd file :
        # sitk.WriteImage(Image3D, NrrdHuPath)
        sitk.WriteImage(Image3D_255, Nrrd255Path)

        #############################################################################################
        # MultiThreading PNG Writer:
        #########################################################################################
        def Image3DToPNG(i, slices, PngDir, Preffix):
            img_Slice = slices[i]
            img_Name = f"{Preffix}_img{i:04}.png"
            image_path = join(PngDir, img_Name)
            cv2.imwrite(image_path, img_Slice)
            image = bpy.data.images.load(image_path)
            image.pack()
            # print(f"{img_Name} was processed...")

        #########################################################################################
        # Get slices list :
        MaxSp = max(Vector(Sp))
        if MaxSp < 0.25:
            SampleRatio = round(MaxSp / 0.25, 2)
            Image3D_255 = ResizeImage(sitkImage=Image3D_255, Ratio=SampleRatio)
            DcmInfo["RenderSz"] = Image3D_255.GetSize()
            DcmInfo["RenderSp"] = Image3D_255.GetSpacing()

        Array = sitk.GetArrayFromImage(Image3D_255)
        slices = [np.flipud(Array[i, :, :]) for i in range(Array.shape[0])]
        # slices = [Image3D_255[:, :, i] for i in range(Image3D_255.GetDepth())]

        threads = [
            threading.Thread(
                target=Image3DToPNG,
                args=[i, slices, PngDir, Preffix],
                daemon=True,
            ) for i in range(len(slices))
        ]

        for t in threads:
            t.start()

        for t in threads:
            t.join()

        # os.removedirs(PngDir)
        shutil.rmtree(PngDir)
        DcmInfo["CT_Loaded"] = True

        # Set DcmInfo property :
        DcmInfoDict = eval(BDENTAL_Props.DcmInfo)
        DcmInfoDict[Preffix] = DcmInfo
        BDENTAL_Props.DcmInfo = str(DcmInfoDict)
        if Preffix == "BD01":
            BlendFile = f"{Preffix}_CT-SCAN.blend"
            Blendpath = join(UserProjectDir, BlendFile)
            bpy.ops.wm.save_as_mainfile(filepath=Blendpath)
        else:
            bpy.ops.wm.save_mainfile()
        #############################################################################################
        finish = Tcounter()
        print(f"Data Loaded in {finish-start} second(s)")
        #############################################################################################

        return DcmInfo