Python项目实战:基于napari的3D可视化(点云+slice)

文章目录

  • 一、napari 简介
  • 二、napari 安装与更新
  • 三、napari【巨巨巨大的一个BUG】
  • 四、napari 使用指南
    • 4.1、菜单栏(File + View + Plugins + Window + Help)
    • 4.2、Window:layer list(参数详解)
    • 4.3、Window:layer controls(points layer + shapes layer + labels layer)
  • 五、项目实战
    • 5.1、查看图像:napari.view_image()
    • 5.2、添加图像:viewer.add_image()
    • 5.3、添加点云:viewer.add_points()
    • 5.4、添加形状:viewer.add_shapes() —— 获取线条坐标(起点和终点)
  • 六、在napari中自定义组件,并与PyQt完成交互

一、napari 简介

基于 Python 编写的快速、交互式多维图像查看器

napari 官网首页:https://napari.org/0.4.18/index.html#
napari 使用案例:https://napari.org/0.4.18/gallery.html#gallery

二、napari 安装与更新

  • 安装napari:pip install napari
  • 更新napari:pip install --upgrade napari

三、napari【巨巨巨大的一个BUG】

  • 【BUG】:点击View - Toggle Full Screen将最大化软件界面,且菜单栏和很多按钮都将不可用。
  • 【影响】:此时,想要任何操作都无法退出最大化,即使关闭后重试,卸载后重试都无法达到,没有试过关机后重试。
  • 【解决方法】:Window + Tab切换窗口,菜单栏可以短暂有效且可点击,瞬间点击View - Toggle Full Screen,可解除BUG。

四、napari 使用指南

4.1、菜单栏(File + View + Plugins + Window + Help)

File(文件)
1 Open File 打开文件
2 Opencv File as Stack 打开文件(适用于大尺度)
3 Open Sample + napari builtins(提供了很多的内置样本) 初学者可以直接导入后研究
4 Preferences 设置(主题 + 快捷键等等)
5 Save Selected Layer(s)(所有帧图像) 保存选定的单层或多层(指定后缀,修改图像格式)
6 Save All Layers(所有帧图像) 保存所有层(指定后缀,修改图像格式)
7 Save Screenshot(单帧图像) 保存当前窗口内容(不显示界面)
8 Save Screenshot with Viewer(单帧图像) 保存整个视图内容(图像 + 界面)
  • Plugins(插件):安装和卸载插件(也可以自定义)
View(视图)
1 Axes
2 Scale Bar 刻度条
3 Toggle Full Screen 切换全屏
4 Toggle Menubar Visibity 切换菜单可见性
5 Toggle Paly 切换面板
6 Toggle Layer Tooltips 切换图层工具提示
7 Toggle Activity Dock 切换活动区
Window(窗口)
1 console 控制面板(命令行窗口)
2 layer controls 图层控制(点层、形状层、标签层)
3 layer list 图层列表
Help(帮助) 直接跳转官网页面
1 Getting started 开始
2 Tutorials 教程
3 Using Layers Guides 使用图层指南
4 Examples Gallery 示例图库
5 Release Notes 版本说明
6 napari homepage napari 主页
7 napari Info napari 信息

4.2、Window:layer list(参数详解)

Python项目实战:基于napari的3D可视化(点云+slice)_第1张图片

4.3、Window:layer controls(points layer + shapes layer + labels layer)

Python项目实战:基于napari的3D可视化(点云+slice)_第2张图片

五、项目实战

5.1、查看图像:napari.view_image()

在napari查看器中显示单个或多个图像,并提供了许多可选参数来自定义图像的显示。

import napari
import tifffile

image_path = 'output_8bit.tif'
marked_image = tifffile.imread(image_path)

viewer = napari.view_image(marked_image, name='image', rgb=False)
napari.run()
# 备注:若不添加napari.run(),将避免程序阻塞问题(一直等待界面关闭)

"""
#########################################################################################################
# 函数说明:napari.view_image(data, channel_axis=None, name=None, colormap=None, blending=None, interpolation=None, gamma=None, 
# is_pyramid=None, rgb=None, scale=None, translate=None, contrast_limits=None, rendering=None)
# 输入参数:
#       - data:要显示的图像数据。可以是以下格式之一:
#                 2D NumPy array:灰度图像数据。
#                 3D NumPy array:3D图像数据,例如多张2D图像叠加形成的图像序列。
#                 4D NumPy array:4D图像数据,例如多通道彩色图像。
#                 List of 2D, 3D, or 4D arrays:多个图像数据列表。
#                 Dask array:支持分块加载的大型图像数据。
#                 ImageData:来自dask_image.imread()等函数的图像数据对象。
#         channel_axis:用于多通道图像的通道轴的索引。默认值为None,表示使用最后一个轴作为通道轴。
#       - name:图像的名称,将在napari查看器中显示。
#       - colormap:图像的颜色映射。可以是字符串表示的颜色映射名称,或是colormap函数。默认值为None,表示使用默认颜色映射。
#         blending:图像的混合模式。可以是字符串表示的混合模式名称,例如"translucent"、"additive"等。默认值为None,表示使用默认混合模式。
#         interpolation:图像的插值方法。可以是字符串表示的插值方法名称,例如"nearest"、"bilinear"、"bicubic"等。默认值为None,表示使用默认插值方法。
#         gamma:图像的gamma值,用于对图像进行伽马校正。默认值为None,表示不进行伽马校正。
#         is_pyramid:布尔值,用于指示是否使用金字塔结构显示图像。默认值为None,表示不使用金字塔结构。
#         rgb:布尔值,用于指示输入图像是否为RGB彩色图像。默认值为None,表示根据输入图像数据自动判断。
#         scale:图像的缩放因子。可以是单个值,表示在所有轴上应用相同的缩放,也可以是每个轴的缩放因子列表。默认值为None,表示不进行缩放。
#         translate:图像的平移量。可以是单个值,表示在所有轴上应用相同的平移,也可以是每个轴的平移量列表。默认值为None,表示不进行平移。
#         contrast_limits:图像的对比度限制,用于控制图像显示的亮度范围。可以是单个值,表示在所有轴上应用相同的对比度限制,也可以是每个轴的对比度限制列表。默认值为None,表示不设置对比度限制。
#         rendering:图像的渲染模式。可以是字符串表示的渲染模式名称,例如"mip"、"translucent"、"attenuated_mip"等。默认值为None,表示使用默认渲染模式。
#########################################################################################################
"""

5.2、添加图像:viewer.add_image()

将单个或多个图像添加到napari查看器中,并提供了多个可选参数来自定义图像的显示。

Python项目实战:基于napari的3D可视化(点云+slice)_第3张图片

import napari
import tifffile

image_path = '561result-1-part.tif'
marked_image = tifffile.imread(image_path)

viewer = napari.Viewer()  # 创建napari视图
viewer.add_image(marked_image, name="image", colormap='red')  # 添加图像(指定红色)
################################################################################
# 隐藏面板
# viewer.window.qt_viewer.controls.hide()  # 隐藏后不可使用该功能(重新打开也不行)
# viewer.window.qt_viewer.layers.hide()

# viewer.window.qt_viewer.controls.close()
# viewer.window.qt_viewer.layers.close()
################################################################################
napari.run()  # 显示napari图形界面
# 备注:若不添加napari.run(),将避免程序阻塞问题(一直等待界面关闭)

"""
#########################################################################################################
# 函数说明:viewer.add_image(data, *, name=None, scale=None, translate=None, contrast_limits=None, 
#                          colormap=None, blending=None, visible=True, opacity=1.0, interpolation='bilinear', 
#                          rendering='mip', rgb=None, colormap_range=None)
# 输入参数:
#       - data:要添加的图像数据。可以是以下格式之一:
#                 2D NumPy array:灰度图像数据。
#                 3D NumPy array:3D图像数据,例如多张2D图像叠加形成的图像序列。
#                 4D NumPy array:4D图像数据,例如多通道彩色图像。
#       - name:图像的名称,将在napari查看器中显示。
#         scale:图像的缩放因子。可以是单个值,表示在所有轴上应用相同的缩放,也可以是每个轴的缩放因子列表。
#         translate:图像的平移量。可以是单个值,表示在所有轴上应用相同的平移,也可以是每个轴的平移量列表。
#         contrast_limits:图像的对比度限制,用于控制图像显示的亮度范围。可以是单个值,表示在所有轴上应用相同的对比度限制,也可以是每个轴的对比度限制列表。
#       - colormap:图像的颜色映射。可以是字符串表示的颜色映射名称,或是colormap函数。
#         blending:图像的混合模式。可以是字符串表示的混合模式名称,例如"translucent"、"additive"等。
#         visible:图像是否可见。布尔值,默认为True。
#         opacity:图像的不透明度。默认为1.0,表示完全不透明。
#         interpolation:图像的插值方法。可以是字符串表示的插值方法名称,例如"nearest"、"bilinear"、"bicubic"等。
#         rendering:图像的渲染模式。可以是字符串表示的渲染模式名称,例如"mip"、"translucent"、"attenuated_mip"等。
#         rgb:布尔值,用于指示输入图像是否为RGB彩色图像。
#         colormap_range:颜色映射的范围。可以是字符串,例如"auto"或"full",表示自动计算颜色映射范围或使用完整范围。
#########################################################################################################
"""

5.3、添加点云:viewer.add_points()

将点的坐标和可选的其他属性添加到napari查看器中,并提供了多个可选参数来自定义点云的显示。

  • viewer.add_points()用于添加点云数据,但不能直接显示。需要先添加viewer.view_image(),然后再显示点云数据。
  • 点云数据:由离散点(x, y, z)坐标的集合组成。

import napari
import tifffile
import numpy as np

# (1)通过tifffile加载tif图像
marked_image = tifffile.imread('marked_image.tif')

# (2)获取图像的长宽高
if len(marked_image.shape) == 3:  # 灰度3D图像:10x10x10
    depth, height, width = marked_image.shape
elif len(marked_image.shape) == 4:  # 彩色3D图像:3x10x10x10
    depth, height, width, _ = marked_image.shape

# (3)根据图像类型自适应变量值
if marked_image.dtype == np.uint8:
    max_gray_value = 255
elif marked_image.dtype == np.uint16:
    max_gray_value = 65535
elif marked_image.dtype == np.uint32:
    max_gray_value = 4294967295
####################################################################
# (4.1)提取指定像素
indices = np.argwhere(marked_image == max_gray_value)

# (4.2)指定范围内的像素值,获取坐标,并绘制为标记点
# min_gray_value = 50
# max_gray_value = max_gray_value - 1
# indices = np.argwhere((marked_image >= min_gray_value) & (marked_image <= max_gray_value))
####################################################################
viewer = napari.Viewer()  # 创建napari查看器
viewer.add_image(marked_image)  # 添加图像到napari视图
viewer.add_points(indices[:, [0, 1, 2]], size=2, face_color='red', shading='spherical', edge_width=0)  # 添加点云
napari.run()  # 显示napari图形界面
# 备注:若不添加napari.run(),将避免程序阻塞问题(一直等待界面关闭)


5.4、添加形状:viewer.add_shapes() —— 获取线条坐标(起点和终点)

注意:shapes层和image_data层是两个独立的层,故线的坐标映射到image_data需要进行高度和宽度限制。
Python项目实战:基于napari的3D可视化(点云+slice)_第4张图片
Python项目实战:基于napari的3D可视化(点云+slice)_第5张图片

import napari
import cv2

# (1)加载图像
image_path = 'blank.png'
image_data = cv2.imread(image_path)
print("height:", image_data.shape[0], "width:", image_data.shape[1])
# (2)创建napari Viewer
viewer = napari.Viewer()
viewer.add_image(image_data)  # 添加图像
# 添加形状(线条 + 线宽 + 颜色)
shapes_layer = viewer.add_shapes(data=None, shape_type='line', edge_width=3, edge_color='red')  
shapes_layer.mode = 'add_line'  # 直接开始绘制线条
napari.run()  # 运行napari
# 备注:若不添加napari.run(),将避免程序阻塞问题(一直等待界面关闭)

# (3)打印坐标
# image_shape = image_data.shape  # 获取图像的形状
line_layer = viewer.layers['Shapes']  # 获取绘制的线的图层
line_coordinates1 = []  # 获取绘制的所有线的坐标
line_coordinates2 = []  # 获取绘制的所有线的坐标:删除超出图像的线条

if line_layer.data:  # 检查图层数据是否存在
    for line in line_layer.data:  # 遍历线的坐标
        coordinates1 = [line[0, 0], line[0, 1], line[1, 0], line[1, 1]]
        line_coordinates1.append(coordinates1)

    for line in line_layer.data:  # 遍历线的坐标
        # 【高度限制】:删除超出图像的线条
        if line[0, 0] < 0:
            line[0, 0] = 0
        elif line[0, 0] > image_data.shape[0]:
            line[0, 0] = image_data.shape[0]
        if line[1, 0] < 0:
            line[1, 0] = 0
        elif line[1, 0] > image_data.shape[0]:
            line[1, 0] = image_data.shape[0]

        # 【宽度限制】:删除超出图像的线条
        if line[0, 1] < 0:
            line[0, 1] = 0
        elif line[0, 1] > image_data.shape[1]:
            line[0, 1] = image_data.shape[1]
        if line[1, 1] < 0:
            line[1, 1] = 0
        elif line[1, 1] > image_data.shape[1]:
            line[1, 1] = image_data.shape[1]

        coordinates2 = [line[0, 0], line[0, 1], line[1, 0], line[1, 1]]
        line_coordinates2.append(coordinates2)

    # 输出所有坐标(shapes:线坐标)
    for idx, coordinates in enumerate(line_coordinates1):
        print(f'Line {idx + 1} coordinates1:', coordinates)

    print("")

    # 输出所有坐标(image:线坐标映射到图像的坐标):高度限制+宽度限制
    for idx, coordinates in enumerate(line_coordinates2):
        print(f'Line {idx + 1} coordinates2:', coordinates)

六、在napari中自定义组件,并与PyQt完成交互

在napari中自定义组件与PyQt新建组件的方法相同,区别是需要将插件的主窗口添加到napari界面的控制面板中:self.viewer.window.add_dock_widget(widget, area='right') # 添加到napari的右侧,其中:widget是插件的主窗口。

Python项目实战:基于napari的3D可视化(点云+slice)_第6张图片
Python项目实战:基于napari的3D可视化(点云+slice)_第7张图片
Python项目实战:基于napari的3D可视化(点云+slice)_第8张图片

import tifffile
import napari
import numpy as np
import sys
import os
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, \
    QPushButton, QFileDialog, QTextEdit, QSlider, QCheckBox
from PyQt5.QtCore import Qt


class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Window")
        ##########################################################
        layout = QVBoxLayout()
        button_layout = QHBoxLayout()

        self.load_button = QPushButton("Load Image", self)
        self.load_button.clicked.connect(self.load_image)
        self.image_name_label = QLabel("")

        button_layout.addWidget(self.load_button)
        button_layout.addWidget(self.image_name_label)
        layout.addLayout(button_layout)
        ##########################################################
        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)
        ##########################################################
        # 初始化参数
        self.image_path = ""
        self.image_name = ""
        ##########################################################

    def load_image(self):
        file_dialog = QFileDialog()
        image_path, _ = file_dialog.getOpenFileName(self, "Select Image", "", "Image Files (*.tif *.png *.jpg *.jpeg)")

        if image_path:
            self.image_path = image_path
            self.image_name = os.path.basename(image_path)
            self.image_name_label.setText(self.image_name)
            self.image_slices = tifffile.imread(image_path)

            self.napari_gray_view()  # 调用napari_gray_view方法来显示第一个切片

    def napari_gray_view(self):
        # (1)napari:创建视图、添加图层、显示视图
        self.viewer = napari.Viewer()  # 创建napari视图
        self.viewer.add_image(self.image_slices, name='image')  # 添加napari图层
	    # napari.run()  # 显示napari图形界面
	    # 备注:若不添加napari.run(),将避免程序阻塞问题(一直等待界面关闭)
        #########################################################################
        # (2)自定义组件
        #########################################################################
        # (2.1)创建滑动条
        self.slider = QSlider()  # 新建滑动条
        self.slider.setMinimum(0)  # 设置滑动条的最小值
        self.slider.setMaximum(np.max(self.image_slices))  # 设置滑动条的最大值
        self.slider.setValue(0)  # 设置滑动条的初始值
        # (2.2)创建标签
        self.slider_label = QLabel(str(self.slider.value()))
        # (2.3)复选框
        self.checkbox = QCheckBox("checkbox")  # 复选框
        self.slider.setEnabled(False)  # 复选框的初始状态:False
        self.input_box = QLineEdit()  # 新建输入框
        self.input_box.setEnabled(True)  # 复选框的初始状态:True
        self.input_label = QLabel("range: " + str(np.min(self.image_slices)) + "/" + str(np.max(self.image_slices)))  # 输入框标签

        # (3)创建布局并将滑动条和标签添加到布局中
        layout = QVBoxLayout()
        layout.addWidget(self.slider)
        layout.addWidget(self.slider_label)
        layout.addWidget(self.checkbox)
        layout.addWidget(self.input_box)
        layout.addWidget(self.input_label)
        # (4)创建一个QWidget作为插件的主窗口
        widget = QWidget()
        widget.setLayout(layout)
        # (5)连接复选框的状态变化信号与槽函数
        self.checkbox.stateChanged.connect(self.onCheckboxStateChanged)
        self.slider.valueChanged.connect(self.napari_update_gray)  # 根据滑动条的值,显示第一个切片
        self.input_box.returnPressed.connect(self.napari_update_gray)  # 根据输入框的值,显示第一个切片

        # (6)将插件的主窗口添加到napari界面的控制面板中
        self.viewer.window.add_dock_widget(widget, area='right')  # 添加到napari的右侧
        #########################################################################
        # (7)napari:显示视图
        self.viewer.window.show()

    def onCheckboxStateChanged(self, state):
        if state == Qt.Checked:
            self.slider.setEnabled(True)
            self.input_box.setEnabled(False)
        else:
            self.slider.setEnabled(False)
            self.input_box.setEnabled(True)

    def napari_update_gray(self):
        # (1)获取napari视图中的图层对象,并获取图像数据
        napari_image = self.image_slices.data
        # (2)获取当前切片滑动条的值
        current_slice = int(self.viewer.dims.current_step[0])
        # (3)获取当前灰度滑动条的值
        self.slider_label.setText(str(self.slider.value()))
        if self.checkbox.isChecked():
            current_gray = self.slider.value()
        else:
            current_gray = int(self.input_box.text())
        print("current_slice:", current_slice, "current_gray:", current_gray)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())

你可能感兴趣的:(深度学习,3D可视化,napari,图像处理,可视化,python)