通过PyQt5实现图像分割应用程序

PyQt5

前言

最近要写一个工具来展示用 UNet 进行医学图像分割的效果,选择了 PyQt5 这个框架,有点太重量级了,用得脑壳疼,但是不能白头疼,要把学到的东西记录下来。

首先打个草图,按照草图来安排界面的布局:
草稿

界面编写

首先草草地做一个界面出来。

主窗口

PyQt5 的 QtWidgets 包里有个类叫 QMainWindow,用它作为 App 的主窗口,为了将主窗口改成我们想要的形状,我们另外定义一个类,并继承 QMainWindow。

创建 DemoMainWindow.py 文件:

from PyQt5 import QtWidgets

class DemoMainWindow(QtWidgets.QMainWindow):
    def __init__(self, central_widget:QtWidgets.QWidget = None):
        super(DemoMainWindow, self).__init__(None)
        top_bar = self.menuBar()							# 获取主窗口的菜单栏

        bar_item_file = top_bar.addMenu("文件")				# 给菜单栏添加“文件”选项框

		# 给“文件”选项框添加选项
        file_item_open = bar_item_file.addAction("打开")
        file_item_save = bar_item_file.addAction("保存")
        file_item_quit = bar_item_file.addAction("退出")
        
        # 为选项设置快捷键
        file_item_open.setShortcut("Ctrl+O")
        file_item_save.setShortcut("Ctrl+S")
        file_item_quit.setShortcut("Ctrl+Q")

		# 为主窗口设置中心部件
        if central_widget is not None:
            self.setCentralWidget(central_widget)

		# 为主窗口设置窗口尺寸和标题
        self.resize(1280, 720)
        self.setWindowTitle('Demo')

布局

PyQt5 中的部件(QWidget)需要设置其布局,主窗口已经有自己的预设布局,其它自定义的部件需要另外设置布局。

布局(QLayout)中可以通过 addLayout 方法添加子布局,或者通过 addWidget 方法添加部件,为了方便代码的编写,最好为每个部件包装一层布局。

主窗口中要设置中心部件,我们自己来 DIY 这个中心部件。

创建 CentralWidget.py 文件:

from PyQt5 import QtWidgets

class DemoCentralWidget(QtWidgets.QWidget):
    def __init__(self, introduce:QtWidgets.QLayout = None, presentation:QtWidgets.QLayout = None):
        super(DemoCentralWidget, self).__init__()

        layout = QtWidgets.QVBoxLayout()				# 定义一个垂直布局,这个布局作为中心部件的主布局
        introduce_layout = QtWidgets.QHBoxLayout()		# 为介绍框定义一个水平布局
        presentation_layout = QtWidgets.QHBoxLayout()	# 为展示框定义一个水平布局

		# 为介绍框和展示框定义子布局,这里通过参数的传入进行解耦
        if introduce is not None:
            introduce_layout.addLayout(introduce)
            layout.addLayout(introduce_layout)
        if presentation is not None:
            presentation_layout.addLayout(presentation)
            layout.addLayout(presentation_layout)
        
        self.setLayout(layout)		# 为当前这个中心部件设置布局

一些辅助函数

创建 SubWidgets.py 文件:

from PyQt5 import QtWidgets, QtCore, QtGui
from functools import singledispatch
from numpy import ndarray

def build_introduce(width:int=1000, height:int=150, border:QtWidgets.QFrame.Shape=QtWidgets.QFrame.Box, align:QtCore.Qt.AlignmentFlag=QtCore.Qt.AlignLeft) -> QtWidgets.QLayout:
    introduce = QtWidgets.QLabel()
    introduce.setMaximumSize(width, height)
    introduce.resize(width, height)
    if border is not None:
        introduce.setFrameShape(border)
    introduce.setAlignment(align)
    layout = QtWidgets.QHBoxLayout()
    layout.addWidget(introduce)
    return layout

@singledispatch
def build_image_box(data:str | ndarray, width:int=512, height:int=512) -> QtWidgets.QHBoxLayout:
    raise RuntimeError(f'{type(data)} is not supported!')

@build_image_box.register(str)
def build_image_box_by_file(data:str, width:int=512, height:int=512) -> QtWidgets.QHBoxLayout:
    image_box = QtWidgets.QLabel()
    image_box.setMaximumSize(width, height)
    image_box.resize(width, height)
    image_box.setFrameShape(QtWidgets.QFrame.Box)
    image_box.setAlignment(QtCore.Qt.AlignCenter)
    pixmap = QtGui.QPixmap(data)
    pixmap = pixmap.scaled(64, 64, QtCore.Qt.KeepAspectRatio)
    image_box.setPixmap(pixmap)
    layout = QtWidgets.QHBoxLayout()
    layout.addWidget(image_box)
    return layout

@build_image_box.register(ndarray)
def build_image_box_by_ndarray(data:ndarray, width:int=512, height:int=512) -> QtWidgets.QHBoxLayout:
    image_box = QtWidgets.QLabel()
    image_box.setMaximumSize(width, height)
    image_box.resize(width, height)
    image_box.setFrameShape(QtWidgets.QFrame.Box)
    image_box.setAlignment(QtCore.Qt.AlignCenter)
    image = QtGui.QImage(data, data.shape[0], data.shape[1], QtGui.QImage.Format_RGB32)
    pixmap = QtGui.QPixmap.fromImage(image)
    # pixmap = pixmap.scaled(128, 128, QtCore.Qt.KeepAspectRatio)
    image_box.setPixmap(pixmap)
    layout = QtWidgets.QHBoxLayout()
    layout.addWidget(image_box)
    return layout

def build_attribute_box(surf:float = None, grayscale:float = None) -> QtWidgets.QVBoxLayout:
    layout = QtWidgets.QVBoxLayout()
    surf_label = QtWidgets.QLabel()
    grayscale_label = QtWidgets.QLabel()
    if surf is not None:
        surf_label.setText(f'Surf area:\t{surf}')
    else:
        surf_label.setText(f'Surf area:')
    if grayscale is not None:
        grayscale_label.setText(f'Grayscale:\t{grayscale}')
    else:
        grayscale_label.setText(f'Grayscale:')
    layout.addWidget(surf_label)
    layout.addWidget(grayscale_label)
    return layout

def build_presentation(input_box:QtWidgets.QLayout, output_box:QtWidgets.QLayout, attribute_box:QtWidgets.QLayout) -> QtWidgets.QHBoxLayout:
    layout = QtWidgets.QHBoxLayout()
    layout.addLayout(input_box)
    layout.addLayout(output_box)
    layout.addLayout(attribute_box)
    return layout

查看效果

创建 App.py 文件:

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    introduce = build_introduce(border=None)
    input_box = build_image_box('file.png')
    output_box = build_image_box('image.png')
    attribute_box = build_attribute_box()
    presentation = build_presentation(input_box, output_box, attribute_box)
    central_widget = DemoCentralWidget(introduce, presentation)
    main_window = DemoMainWindow(central_widget)
    main_window.show()
    print(app.exec())

运行 App.py
无功能界面

功能编写

菜单栏按钮和文件拖拽功能

主窗口的菜单栏添加的是 QAction 对象,QAction 常用信号包括 triggered(点击)、hovered(悬停)、toggled(选中)、changed(状态变化)等。这些信号的类是 QtCore.pyqtSignal,可以通过 connect 方法绑定响应动作。

此外,还要实现一个文件拖放的功能,通过 setAcceptDrops(True) 启用该功能,另外还必须实现 dragEnterEvent(self, evn) 这个抽象接口才能拖拽文件。

修改 DemoMainWindow.py 的内容:

from PyQt5 import QtWidgets, QtGui

class DemoMainWindow(QtWidgets.QMainWindow):
    def __init__(self, central_widget:QtWidgets.QWidget = None):
        super(DemoMainWindow, self).__init__(None)
        top_bar = self.menuBar()

        bar_item_file = top_bar.addMenu('文件')
        file_item_open = bar_item_file.addAction('打开')
        file_item_save = bar_item_file.addAction('保存')
        file_item_quit = bar_item_file.addAction('退出')
        
        file_item_open.setShortcut('Ctrl+O')
        file_item_save.setShortcut('Ctrl+S')
        file_item_quit.setShortcut('Ctrl+Q')
        
        file_item_open.triggered.connect(self.open_file)

        if central_widget is not None:
            self.setCentralWidget(central_widget)
        self.resize(1280, 720)
        self.setWindowTitle('Demo')

        self.setAcceptDrops(True)
	
	def open_file(self):
		file,_ = QtWidgets.QFileDialog.getOpenFileName(self, 'Dicom文件选择', '.', '*.dcm')
		print(file)
	
	def dragEnterEvent(self, evn):
		evn.accept()

	def dropEvent(self, evn):
		file = evn.mimeData().text().replace('file:///', '')
		print(file)

算了不写了

写累了,直接贴代码了。省略号的地方需要根据需要进行补充。

App.py

from DemoMainWindow import DemoMainWindow
from CentralWidget import DemoCentralWidget
from PyQt5.QtWidgets import QApplication
from SubWidgets import *

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    introduce = build_introduce(content='说明:\nCtrl+O 打开 dcm 文件后将自动展现分割结果\n或者将文件拖入窗口', border=None)
    input_box = build_image_box('....png')
    input_box.setObjectName('input_box')
    # print(input_box)
    output_box = build_image_box('....png')
    output_box.setObjectName('output_box')
    attribute_box = build_attribute_box()
    attribute_box.setObjectName('attribute_box')
    presentation = build_presentation(input_box, output_box, attribute_box)
    central_widget = DemoCentralWidget(introduce, presentation)
    main_window = DemoMainWindow(central_widget)
    main_window.show()
    print(app.exec())

CentralWidget.py

from PyQt5 import QtWidgets

class DemoCentralWidget(QtWidgets.QWidget):
    def __init__(self, introduce:QtWidgets.QLayout = None, presentation:QtWidgets.QLayout = None):
        super(DemoCentralWidget, self).__init__()
        layout = QtWidgets.QVBoxLayout()
        introduce_layout = QtWidgets.QHBoxLayout()
        presentation_layout = QtWidgets.QHBoxLayout()

        if introduce is not None:
            introduce_layout.addLayout(introduce)
            layout.addLayout(introduce_layout)
        if presentation is not None:
            presentation_layout.addLayout(presentation)
            layout.addLayout(presentation_layout)

        self.setLayout(layout)

DemoMainWindow.py

from PyQt5 import QtWidgets, QtGui
from pydicom import dcmread
from PIL import Image
from numpy import uint8, int32, expand_dims, array, clip, sum, concatenate
from unets import NestedUNet
from torch import Tensor, cuda, load, ones_like, zeros_like, where, float32
from torchvision import transforms
from collections import OrderedDict

class DemoMainWindow(QtWidgets.QMainWindow):
    def __init__(self, central_widget:QtWidgets.QWidget = None, threshold:float = 0.9):
        super(DemoMainWindow, self).__init__(None)
        top_bar = self.menuBar()

        bar_item_file = top_bar.addMenu('文件')
        file_item_open = bar_item_file.addAction('打开')
        file_item_save = bar_item_file.addAction('保存')
        file_item_quit = bar_item_file.addAction('退出')
        
        file_item_open.setShortcut('Ctrl+O')
        file_item_save.setShortcut('Ctrl+S')
        file_item_quit.setShortcut('Ctrl+Q')
        
        file_item_open.triggered.connect(self.open_file)

        if central_widget is not None:
            self.setCentralWidget(central_widget)
        self.resize(1280, 720)
        self.setWindowTitle('Demo')

        self.threshold = threshold
        unet = ...
        self.device = 'cuda:0' if cuda.is_available() else 'cpu'
        
        state_dict = load('...')

        unet.load_state_dict(state_dict )
        self.unet = unet.to(self.device)
        self.transforms = transforms.Compose([transforms.ToTensor(), transforms.Resize((512, 512), antialias=True)])
        self.topil = transforms.ToPILImage()
        self.setAcceptDrops(True)
    
    def open_file(self):
        central_widget = self.centralWidget()
        input_box = central_widget.findChild(QtWidgets.QLayout, 'input_box')
        output_box = central_widget.findChild(QtWidgets.QLayout, 'output_box')
        attribute_box = central_widget.findChild(QtWidgets.QLayout, 'attribute_box')
        input_image_box = input_box.itemAt(0).widget()
        output_image_box = output_box.itemAt(0).widget()
        attribute_box_item = attribute_box.itemAt(0).widget()
        attribute_box_surf = attribute_box.itemAt(1).widget()
        attribute_box_grayscale = attribute_box.itemAt(2).widget()

        file,_ = QtWidgets.QFileDialog.getOpenFileName(self, 'Dicom文件选择', '.', '*.dcm')
        attribute_box_item.setText(f'Item: {file}')
        dcm_file = dcmread(file)
        space_x, space_y = dcm_file.PixelSpacing[0], dcm_file.PixelSpacing[1]
        dcm_array = dcm_file.pixel_array
        dcm_array = dcm_array.clip(-1000, 400)
        dcm_array = dcm_array - dcm_array.min()
        dcm_array = dcm_array/(dcm_array.max())
        dcm_array = dcm_array*255

        image = Image.fromarray(uint8(dcm_array)).convert('L')
        image.save(r'.temp/input_dcm.jpg')
        image = expand_dims(array(image), 2)

        input_image_box.setPixmap(QtGui.QPixmap(r'.temp/input_dcm.jpg'))

        data:Tensor = self.transforms(dcm_array)
        
        data = data.unsqueeze(0).to(self.device)
        if self.device != 'cpu':
            data = data.type(cuda.FloatTensor)
        else:
            data = data.type(float32)
        
        predicts = self.unet(data)
        predicts = predicts - predicts.min()
        predicts = (predicts + 1e-7)/(predicts.max() + 1e-7)
        ones_ref = ones_like(predicts)
        zeros_ref = zeros_like(predicts)
        predicts = where(predicts >= self.threshold, ones_ref, predicts)
        predicts = where(predicts < self.threshold, zeros_ref, predicts).detach().cpu()
        mask = predicts.squeeze(0)
        mask_area = mask.count_nonzero().item()
        surf_area = mask_area*(space_x*space_y)*0.01
        mask = self.topil(mask)
        mask.save(r'.temp/output_mask.jpg')
        mask = expand_dims(array(mask), 2)
        
        image = int32(image)
        mask = int32(mask)
        
        mask_ones = clip(mask, 0, 1)
        grayscale_array = expand_dims(dcm_file.pixel_array, 2)*mask_ones
        grayscale = (sum(grayscale_array))/(mask_area+1e-7)
        
        red = clip(image + mask, 0, 255)
        green = clip(image - mask, 0, 255)
        blue = clip(image - mask, 0, 255)

        output = uint8(concatenate((red, green, blue), 2))
        
        Image.fromarray(output).convert('RGB').save(r'.temp/output_dcm.jpg')
        output_image_box.setPixmap(QtGui.QPixmap(r'.temp/output_dcm.jpg'))
        attribute_box_surf.setText(f'Surf: {surf_area:.4f} cm²')
        attribute_box_grayscale.setText(f'Mean: {grayscale:.4f}')

    def dragEnterEvent(self, evn):
        evn.accept()


    def dropEvent(self, evn):
        file = evn.mimeData().text().replace('file:///', '')
        if file[-4:] != '.dcm':
            return

        central_widget = self.centralWidget()
        input_box = central_widget.findChild(QtWidgets.QLayout, 'input_box')
        output_box = central_widget.findChild(QtWidgets.QLayout, 'output_box')
        attribute_box = central_widget.findChild(QtWidgets.QLayout, 'attribute_box')
        input_image_box = input_box.itemAt(0).widget()
        output_image_box = output_box.itemAt(0).widget()
        attribute_box_item = attribute_box.itemAt(0).widget()
        attribute_box_surf = attribute_box.itemAt(1).widget()
        attribute_box_grayscale = attribute_box.itemAt(2).widget()
        
        attribute_box_item.setText(f'Item: {file}')
        dcm_file = dcmread(file)
        space_x, space_y = dcm_file.PixelSpacing[0], dcm_file.PixelSpacing[1]
        dcm_array = dcm_file.pixel_array
        dcm_array = dcm_array.clip(-1000, 400)
        dcm_array = dcm_array - dcm_array.min()
        dcm_array = dcm_array/(dcm_array.max())
        dcm_array = dcm_array*255

        image = Image.fromarray(uint8(dcm_array)).convert('L')
        image.save(r'.temp/input_dcm.jpg')
        image = expand_dims(array(image), 2)

        input_image_box.setPixmap(QtGui.QPixmap(r'.temp/input_dcm.jpg'))

        data:Tensor = self.transforms(dcm_array)
        
        data = data.unsqueeze(0).to(self.device)
        if self.device != 'cpu':
            data = data.type(cuda.FloatTensor)
        else:
            data = data.type(float32)
        
        predicts = self.unet(data)
        predicts = predicts - predicts.min()
        predicts = (predicts + 1e-7)/(predicts.max() + 1e-7)
        ones_ref = ones_like(predicts)
        zeros_ref = zeros_like(predicts)
        predicts = where(predicts >= self.threshold, ones_ref, predicts)
        predicts = where(predicts < self.threshold, zeros_ref, predicts).detach().cpu()
        mask = predicts.squeeze(0)
        mask_area = mask.count_nonzero().item()
        surf_area = mask_area*(space_x*space_y)*0.01
        mask = self.topil(mask)
        mask.save(r'.temp/output_mask.jpg')
        mask = expand_dims(array(mask), 2)
        
        image = int32(image)
        mask = int32(mask)
        
        mask_ones = clip(mask, 0, 1)
        grayscale_array = expand_dims(dcm_file.pixel_array, 2)*mask_ones
        grayscale = (sum(grayscale_array))/(mask_area+1e-7)
        
        red = clip(image + mask, 0, 255)
        green = clip(image - mask, 0, 255)
        blue = clip(image - mask, 0, 255)

        output = uint8(concatenate((red, green, blue), 2))
        
        Image.fromarray(output).convert('RGB').save(r'.temp/output_dcm.jpg')
        output_image_box.setPixmap(QtGui.QPixmap(r'.temp/output_dcm.jpg'))
        attribute_box_surf.setText(f'Surf: {surf_area:.4f} cm²')
        attribute_box_grayscale.setText(f'Mean: {grayscale:.4f}')

SubWidgets.py

from PyQt5 import QtWidgets, QtCore, QtGui
from functools import singledispatch
from numpy import ndarray

def build_introduce(width:int=1000, height:int=150, content:str='', border:QtWidgets.QFrame.Shape=QtWidgets.QFrame.Box, align:QtCore.Qt.AlignmentFlag=QtCore.Qt.AlignLeft) -> QtWidgets.QLayout:
    introduce = QtWidgets.QLabel()
    introduce.setMaximumSize(width, height)
    introduce.resize(width, height)
    if border is not None:
        introduce.setFrameShape(border)
    introduce.setAlignment(align)
    introduce.setText(content)
    introduce.setFont(QtGui.QFont('Arial', 16))
    layout = QtWidgets.QHBoxLayout()
    layout.addWidget(introduce)
    return layout

@singledispatch
def build_image_box(data:str | ndarray, width:int=512, height:int=512) -> QtWidgets.QHBoxLayout:
    raise RuntimeError(f'{type(data)} is not supported!')

@build_image_box.register(str)
def build_image_box_by_file(data:str, width:int=512, height:int=512) -> QtWidgets.QHBoxLayout:
    image_box = QtWidgets.QLabel()
    image_box.setMaximumSize(width, height)
    image_box.resize(width, height)
    image_box.setFrameShape(QtWidgets.QFrame.Box)
    image_box.setAlignment(QtCore.Qt.AlignCenter)
    pixmap = QtGui.QPixmap(data)
    pixmap = pixmap.scaled(64, 64, QtCore.Qt.KeepAspectRatio)
    image_box.setPixmap(pixmap)
    layout = QtWidgets.QHBoxLayout()
    layout.addWidget(image_box)
    return layout

@build_image_box.register(ndarray)
def build_image_box_by_ndarray(data:ndarray, width:int=512, height:int=512) -> QtWidgets.QHBoxLayout:
    image_box = QtWidgets.QLabel()
    image_box.setMaximumSize(width, height)
    image_box.resize(width, height)
    image_box.setFrameShape(QtWidgets.QFrame.Box)
    image_box.setAlignment(QtCore.Qt.AlignCenter)
    image = QtGui.QImage(data, data.shape[0], data.shape[1], QtGui.QImage.Format_Grayscale8)
    pixmap = QtGui.QPixmap.fromImage(image)
    image_box.setPixmap(pixmap)
    layout = QtWidgets.QHBoxLayout()
    layout.addWidget(image_box)
    return layout

def build_attribute_box(item:str = None, surf:float = None, grayscale:float = None) -> QtWidgets.QVBoxLayout:
    layout = QtWidgets.QVBoxLayout()
    item_label = QtWidgets.QLabel()
    surf_label = QtWidgets.QLabel()
    grayscale_label = QtWidgets.QLabel()
    if item is not None:
        item_label.setText(f'Item: {item_label}')
    else:
        item_label.setText(f'Item:')
    item_label.setFont(QtGui.QFont('Arial', 16))
    if surf is not None:
        surf_label.setText(f'Surf: {surf}')
    else:
        surf_label.setText(f'Surf:')
    surf_label.setFont(QtGui.QFont('Arial', 16))
    if grayscale is not None:
        grayscale_label.setText(f'Mean: {grayscale}')
    else:
        grayscale_label.setText(f'Mean:')
    grayscale_label.setFont(QtGui.QFont('Arial', 16))
    layout.addWidget(item_label)
    layout.addWidget(surf_label)
    layout.addWidget(grayscale_label)
    return layout

def build_presentation(input_box:QtWidgets.QLayout, output_box:QtWidgets.QLayout, attribute_box:QtWidgets.QLayout) -> QtWidgets.QHBoxLayout:
    layout = QtWidgets.QHBoxLayout()
    layout.addLayout(input_box)
    layout.addLayout(output_box)
    layout.addLayout(attribute_box)
    return layout

  • 11
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python中,可以使用PyQt5库进行图像分割用户界面设计。PyQt5是一个用于创建GUI应用程序Python绑定库,它是Qt库的Python版本。 要使用PyQt5进行图像分割用户界面设计,首先需要安装PyQt5库。可以使用pip命令来安装: ``` pip install PyQt5 ``` 安装完成后,可以使用以下代码创建一个简单的图像分割用户界面: ```python import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton class ImageSegmentationUI(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Image Segmentation") self.setGeometry(100, 100, 400, 300) self.label = QLabel(self) self.label.setText("Click the button to start image segmentation") self.label.setGeometry(50, 50, 300, 30) self.button = QPushButton(self) self.button.setText("Start Segmentation") self.button.setGeometry(150, 100, 100, 30) self.button.clicked.connect(self.start_segmentation) def start_segmentation(self): # Add your image segmentation code here pass if __name__ == "__main__": app = QApplication(sys.argv) ui = ImageSegmentationUI() ui.show() sys.exit(app.exec_()) ``` 在上面的代码中,我们创建了一个继承自QMainWindow的ImageSegmentationUI类。在该类中,我们创建了一个标签(QLabel)用于显示提示信息,并创建了一个按钮(QPushButton)用于触发图像分割操作。当按钮被点击时,会调用start_segmentation方法,你可以在该方法中添加你的图像分割代码。 运行上述代码,将会显示一个窗口,其中包含一个标签和一个按钮。点击按钮后,会调用start_segmentation方法,你可以在该方法中添加你的图像分割代码。 希望以上代码对你有所帮助!如果你有任何问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值