"""pyCERR napari viewer.
napari-based visualization of scan, structure, dose and deformation vector
fields, with the associated magicgui widgets (window/level, structure
edit/create/export, registration QA mirror-scope, dose colorbar).
Entry points:
showNapari(planC, ...) - open the napari viewer
captureToFile(...) - render axial/sagittal/coronal screenshots to file
For Jupyter/Colab (no Qt/napari) use cerr.viewer.pycerr_nbviewer; for the
PyQt desktop viewer use cerr.viewer.pycerr_gui.
"""
import importlib
import os
import pathlib
import typing
import warnings
from typing import Annotated
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.colors import ListedColormap
from matplotlib.figure import Figure
from skimage import measure
import cerr.contour.rasterseg as rs
import cerr.dataclasses.scan as scn
import cerr.dataclasses.structure as cerrStr
import cerr.plan_container as pc
from cerr.utils import custom_colormaps
if importlib.util.find_spec('napari') is not None:
import napari
from napari.layers import Labels, Image
from napari.types import LayerDataTuple
from qtpy.QtWidgets import QTabBar
from magicgui import magicgui
from magicgui.widgets import FunctionGui
import vispy.color
from napari.utils import DirectLabelColormap, Colormap
warnings.filterwarnings("ignore", category=FutureWarning)
window_dict = {
'--- Select ---': (0, 300),
'Abd/Med': (-10, 330),
'Head': (45, 125),
'Liver': (80, 305),
'Lung': (-500, 1500),
'Spine': (30, 300),
'Vrt/Bone': (400, 1500),
'PET SUV': (5, 10)
}
[docs]
def get_scan_layers(gui):
"""Filter Image layers based on the scan dataclass"""
if not hasattr(gui, 'viewer'):
return []
scan_layers = []
for layer in gui.viewer.layers:
if isinstance(layer, napari.layers.Image):
if layer.metadata.get('dataclass') == 'scan':
scan_layers.append(layer)
return scan_layers
[docs]
def getLabelsDict(vwr):
"""Returns a mapping from display label strings to structure indices.
Iterates over all layers in the Napari viewer and collects those that are
Labels layers with a saved ``structNum``. Each key is a string of the form
``"<index>_<structureName>"`` and each value is the corresponding integer
structure index in ``planC``.
Args:
vwr (napari.Viewer): The active Napari viewer whose layers are searched.
Returns:
dict: Mapping of ``"<index>_<structureName>"`` strings to integer
structure indices.
"""
labelDict = {}
ind = 1
for lyr in vwr.layers:
if isinstance(lyr, Labels) and lyr.metadata['dataclass'] == 'structure' and lyr.metadata['structNum'] is not None:
strName = lyr.metadata['planC'].structure[lyr.metadata['structNum']].structureName
labelDict[str(ind) + '_' + strName] = lyr.metadata['structNum']
ind += 1
return labelDict
[docs]
def getLabelsList(vwr):
"""Returns a list of display label strings for all saved structures in the viewer.
Convenience wrapper around :func:`getLabelsDict` that returns only the keys
(i.e. the ``"<index>_<structureName>"`` strings) as an ordered list.
Args:
vwr (napari.Viewer): The active Napari viewer whose layers are searched.
Returns:
list[str]: Ordered list of ``"<index>_<structureName>"`` label strings.
"""
labelDict = getLabelsDict(vwr)
allLabelNames = list(labelDict.keys())
return allLabelNames
[docs]
def checkerboard_indices(shape, tile_size, evenTiles=True):
"""
Generates row and column indices for a checkerboard pattern.
Args:
shape (tuple): Shape of the 2D array (rows, cols).
tile_size (int): Size of each square tile.
Returns:
tuple: Two lists, one for row indices and one for column indices.
Each list contains tuples of slice objects representing the
indices for the checkerboard pattern.
"""
rows, cols = shape
row_indices = []
col_indices = []
for i in range(rows // tile_size + 1):
for j in range(cols // tile_size + 1):
start_row = i * tile_size
end_row = min(start_row + tile_size, rows)
start_col = j * tile_size
end_col = min(start_col + tile_size, cols)
if (i + j) % 2 == 0: # Even tiles
row_indices.append(slice(start_row, end_row))
col_indices.append(slice(start_col, end_col))
return row_indices, col_indices
[docs]
def getRCSwithinScanExtents(r, c, s, numRows, numCols, numSlcs,
offset, axNum):
"""Computes a bounding box of voxel indices clamped to the scan volume.
Given a cursor position ``(r, c, s)`` and a half-size ``offset``, computes
a rectangular region around the cursor along the two in-plane axes determined
by ``axNum``, then clamps every edge to the valid index range of the volume.
Args:
r (int or float): Row index of the cursor position.
c (int or float): Column index of the cursor position.
s (int or float): Slice index of the cursor position.
numRows (int): Total number of rows in the scan volume.
numCols (int): Total number of columns in the scan volume.
numSlcs (int): Total number of slices in the scan volume.
offset (int): Half-size of the bounding region in voxels.
axNum (int): Axis perpendicular to the current viewing plane (0, 1, or 2).
Returns:
tuple[int, int, int, int, int, int]: ``(rMin, rMax, cMin, cMax, sMin, sMax)``
— the clamped bounding-box indices for rows, columns, and slices.
"""
r = int(r)
c = int(c)
s = int(s)
halfOff = int(offset / 2)
halfOff = offset
if axNum == 2:
rMin = r - 2*offset
rMax = r
cMin = c - int(offset*1.5)
cMax = c
sMin = s
sMax = s + 1
elif axNum == 1:
rMin = r - 2*offset
rMax = r
cMin = c
cMax = c + 1
sMin = s - int(offset*1.5)
sMax = s
elif axNum == 0:
rMin = r
rMax = r + 1
cMin = c - 2*offset
cMax = c
sMin = s - int(offset*1.5)
sMax = s
if rMin < 0:
rMin = 0
if rMax < 0:
rMax = 0
if rMin >= numRows:
rMin = numRows - 1
if rMax >= numRows:
rMax = numRows - 1
if cMin < 0:
cMin = 0
if cMax < 0:
cMax = 0
if cMin >= numCols:
cMin = numCols - 1
if cMax >= numCols:
cMax = numCols - 1
if sMin < 0:
sMin = 0
if sMax < 0:
sMax = 0
if sMin >= numSlcs:
sMin = numSlcs - 1
if sMax >= numSlcs:
sMax = numSlcs - 1
return rMin, rMax, cMin, cMax, sMin, sMax
[docs]
def updateMirror(viewer, baseLayer, movLayer, mrrScpLayerBase,
mrrScpLayerMov, mirrorLine, mirrorSize, displayType):
"""Refreshes the mirror-scope overlay layers and border lines for registration QA.
Called on every cursor move event to re-extract cropped sub-volumes from the
base and moving image layers at the current cursor position, resize/flip them
according to ``displayType``, write the result into the temporary
``mrrScpLayerBase`` / ``mrrScpLayerMov`` image layers, and redraw the
``mirrorLine`` shapes that mark the split boundary.
Args:
viewer (napari.Viewer): The active Napari viewer.
baseLayer (napari.layers.Image): The fixed/reference image layer.
movLayer (napari.layers.Image): The moving/registered image layer.
mrrScpLayerBase (napari.layers.Image): Temporary overlay layer for the
cropped base image patch.
mrrScpLayerMov (napari.layers.Image): Temporary overlay layer for the
cropped moving image patch.
mirrorLine (napari.layers.Shapes): Shapes layer used to draw the split
boundary line.
mirrorSize (float): Approximate radius of the mirror region in mm.
displayType (str): One of ``'Mirrorscope'``, ``'Sidebyside'``, or
``'AlternateGrid'``.
Returns:
None
"""
#currPt = viewer.cursor.position
currPt = mrrScpLayerBase.metadata['currentPos']
#baseLyrInd = getLayerIndex(scanNum,'scan',scan_layer)
#movLyrInd = getLayerIndex(scanNum,'scan',scan_layer)
rb,cb,sb = np.round(baseLayer.world_to_data(currPt))
numRows, numCols, numSlcs = baseLayer.data.shape #planC.scan[baseInd].getScanSize()
rb = max(min(rb,numRows-1),1)
cb = max(min(cb,numCols-1),1)
sb = max(min(sb,numSlcs-1),1)
planC = baseLayer.metadata['planC']
baseScanNum = baseLayer.metadata['scanNum']
movScanNum = movLayer.metadata['scanNum']
# Convert mirror size from physical units to number of voxels
baseSpacing = planC.scan[baseScanNum].getScanSpacing()
movSpacing = planC.scan[movScanNum].getScanSpacing()
axNum = viewer.dims.order[0]
colsOffset = int(mirrorSize / (baseSpacing[0]*10))
rowsOffset = int(mirrorSize / (baseSpacing[1]*10))
slcsOffset = int(mirrorSize / (baseSpacing[2]*10))
mirrorSizeBase = colsOffset
rMinB, rMaxB, cMinB, cMaxB, sMinB, sMaxB = getRCSwithinScanExtents(rb,cb,sb,numRows, numCols, numSlcs,
mirrorSizeBase, axNum)
rm,cm,sm = np.round(movLayer.world_to_data(currPt))
numRows, numCols, numSlcs = movLayer.data.shape #planC.scan[movInd].getScanSize()
rm = max(min(rm,numRows-1),1)
cm = max(min(cm,numCols-1),1)
sm = max(min(sm,numSlcs-1),1)
axNum = viewer.dims.order[0]
colsOffset = int(mirrorSize / (movSpacing[0]*10))
rowsOffset = int(mirrorSize / (movSpacing[1]*10))
slcsOffset = int(mirrorSize / (movSpacing[2]*10))
mirrorSizeMov = colsOffset
rMinM, rMaxM, cMinM, cMaxM, sMinM, sMaxM = getRCSwithinScanExtents(rm,cm,sm,numRows, numCols, numSlcs,
mirrorSizeMov, axNum)
if rMinB == rMaxB == 0:
rMaxB = 1
elif rMinB == rMaxB:
rMinB = rMinB - 1
if cMinB == cMaxB == 0:
cMaxB = 1
elif cMinB == cMaxB:
cMinB = cMinB-1
if rMinM == rMaxM == 0:
rMaxM = 1
elif rMinM == rMaxM:
rMinM = rMinM - 1
if cMinM == cMaxM == 0:
cMaxM = 1
elif cMinM == cMaxM:
cMinM = cMinM-1
# planC = baseLayer.metadata['planC']
# baseScanNum = baseLayer.metadata['scanNum']
# movScanNum = movLayer.metadata['scanNum']
xb,yb,zb = planC.scan[baseScanNum].getScanXYZVals()
yb = -yb
dxB,dyB,dzB = planC.scan[baseScanNum].getScanSpacing()
xm,ym,zm = planC.scan[movScanNum].getScanXYZVals()
ym = -ym
dxM,dyM,dzM = planC.scan[movScanNum].getScanSpacing()
deltaXmov = dxM * (cMaxM - cMinM)
deltaXbase = dxB * (cMaxB - cMinB)
deltaYmov = dyM * (rMaxM - rMinM)
#croppedScanBase = baseLayer.data[rMinB:rMaxB,cMinB:cMaxB,int(sb)]
#croppedScanMov = movLayer.data[rMinM:rMaxM,cMinM:cMaxM,int(sm)]
if displayType == 'Mirrorscope':
if viewer.dims.order[0] == 2:
croppedScanBase = baseLayer.data[rMinB:rMaxB,cMinB:cMaxB,int(sb)]
croppedScanMov = movLayer.data[rMinM:rMaxM,cMinM:cMaxM,int(sm)]
croppedScanMov = np.flip(croppedScanMov,axis=1)
mirrorAffineM = np.array([[dyM, 0, 0, ym[rMinM]], [0, dxM, 0, xm[cMinM]+deltaXmov], [0, 0, dzM, zm[int(sm)]], [0, 0, 0, 1]])
mirrorAffineB = np.array([[dyB, 0, 0, yb[rMinB]], [0, dxB, 0, xb[cMinB]], [0, 0, dzB, zb[int(sb)]], [0, 0, 0, 1]])
elif viewer.dims.order[0] == 1:
croppedScanBase = baseLayer.data[rMinB:rMaxB,int(cb),sMinB:sMaxB]
croppedScanMov = movLayer.data[rMinM:rMaxM,int(cm),sMinM:sMaxM]
croppedScanMov = np.flip(croppedScanMov,axis=0)
mirrorAffineM = np.array([[dyM, 0, 0, ym[rMinM]+deltaYmov], [0, dxM, 0, xm[int(cm)]], [0, 0, dzM, zm[sMinM]], [0, 0, 0, 1]])
mirrorAffineB = np.array([[dyB, 0, 0, yb[rMinB]], [0, dxB, 0, xb[int(cb)]], [0, 0, dzB, zb[sMinB]], [0, 0, 0, 1]])
else:
croppedScanBase = baseLayer.data[int(rb),cMinB:cMaxB,sMinB:sMaxB]
croppedScanMov = movLayer.data[int(rm),cMinM:cMaxM,sMinM:sMaxM]
croppedScanMov = np.flip(croppedScanMov,axis=0)
mirrorAffineM = np.array([[dyB, 0, 0, yb[int(rm)]], [0, dxM, 0, xm[cMinM]+deltaXmov], [0, 0, dzM, zm[sMinM]], [0, 0, 0, 1]])
mirrorAffineB = np.array([[dyM, 0, 0, ym[int(rb)]], [0, dxB, 0, xb[cMinB]], [0, 0, dzB, zb[sMinB]], [0, 0, 0, 1]])
elif displayType == 'Sidebyside':
if viewer.dims.order[0] == 2:
croppedScanBase = baseLayer.data[:,:int(cb),int(sb)]
croppedScanMov = movLayer.data[:,int(cm):,int(sm)]
mirrorAffineB = np.array([[dyB, 0, 0, yb[0]], [0, dxB, 0, xb[0]], [0, 0, dzB, zb[int(sb)]], [0, 0, 0, 1]])
mirrorAffineM = np.array([[dyM, 0, 0, ym[0]], [0, dxM, 0, xm[int(cm)]], [0, 0, dzM, zm[int(sm)]], [0, 0, 0, 1]])
elif viewer.dims.order[0] == 1:
croppedScanBase = baseLayer.data[:int(rb),int(cb),:]
croppedScanMov = movLayer.data[int(rm):,int(cm),:]
mirrorAffineB = np.array([[dyB, 0, 0, yb[0]], [0, dxB, 0, xb[int(cb)]], [0, 0, dzB, zb[0]], [0, 0, 0, 1]])
mirrorAffineM = np.array([[dyM, 0, 0, ym[int(rm)]], [0, dxM, 0, xm[int(cm)]], [0, 0, dzM, zm[0]], [0, 0, 0, 1]])
else:
croppedScanBase = baseLayer.data[int(rb),:int(cb),:]
croppedScanMov = movLayer.data[int(rm),int(cm):,:]
mirrorAffineB = np.array([[dyB, 0, 0, yb[int(rb)]], [0, dxB, 0, xb[0]], [0, 0, dzB, zb[0]], [0, 0, 0, 1]])
mirrorAffineM = np.array([[dyM, 0, 0, ym[int(rm)]], [0, dxM, 0, xm[int(cm)]], [0, 0, dzM, zm[0]], [0, 0, 0, 1]])
else: # displayType == 'AlternateGrid':
if viewer.dims.order[0] == 2:
baseShape = baseLayer.data[:,:,int(sb)].shape
movShape = movLayer.data[:,:,int(sm)].shape
croppedScanBase = np.ones(baseShape) * np.nan
croppedScanMov = movLayer.data[:,:,int(sm)].copy()
croppedScanBaseOrig = baseLayer.data[:,:,int(sb)].copy()
mirrorAffineB = np.array([[dyB, 0, 0, yb[0]], [0, dxB, 0, xb[0]], [0, 0, dzB, zb[int(sb)]], [0, 0, 0, 1]])
mirrorAffineM = np.array([[dyM, 0, 0, ym[0]], [0, dxM, 0, xm[0]], [0, 0, dzM, zm[int(sm)]], [0, 0, 0, 1]])
elif viewer.dims.order[0] == 1:
baseShape = baseLayer.data[:,int(cb),:].shape
movShape = movLayer.data[:,int(cm),:].shape
croppedScanBase = np.ones(baseShape) * np.nan
croppedScanMov = movLayer.data[:,int(cm),:].copy()
croppedScanBaseOrig = baseLayer.data[:,int(cb),:].copy()
mirrorAffineB = np.array([[dyB, 0, 0, yb[0]], [0, dxB, 0, xb[int(cb)]], [0, 0, dzB, zb[0]], [0, 0, 0, 1]])
mirrorAffineM = np.array([[dyM, 0, 0, ym[0]], [0, dxM, 0, xm[int(cm)]], [0, 0, dzM, zm[0]], [0, 0, 0, 1]])
else:
baseShape = baseLayer.data[int(rb),:,:].shape
movShape = movLayer.data[int(rm),:,:].shape
croppedScanBase = np.ones(baseShape) * np.nan
croppedScanMov = movLayer.data[int(rm),:,:].copy()
croppedScanBaseOrig = baseLayer.data[int(rb),:,:].copy()
mirrorAffineB = np.array([[dyB, 0, 0, yb[int(rb)]], [0, dxB, 0, xb[0]], [0, 0, dzB, zb[0]], [0, 0, 0, 1]])
mirrorAffineM = np.array([[dyM, 0, 0, ym[int(rm)]], [0, dxM, 0, xm[0]], [0, 0, dzM, zm[0]], [0, 0, 0, 1]])
block_size = mirrorSize
checkBaseRows, checkBaseCols = checkerboard_indices(baseShape, block_size)
checkMovRows, checkMovCols = checkerboard_indices(movShape, block_size)
for i in range(len(checkBaseRows)):
croppedScanBase[checkBaseRows[i],checkBaseCols[i]] = croppedScanBaseOrig[checkBaseRows[i],checkBaseCols[i]]
for i in range(len(checkMovRows)):
croppedScanMov[checkBaseRows[i],checkBaseCols[i]] = np.nan
#croppedScanMov[row_indices2, col_indices2] = np.nan
#croppedScanBase = baseLayer.data[:,:,int(sb)]
#croppedScanMov = movLayer.data[:,:,int(sm)]
# mirrorAffineB = np.array([[dyB, 0, 0, yb[0]], [0, dxB, 0, xb[0]], [0, 0, dzB, zb[int(sb)]], [0, 0, 0, 1]])
# mirrorAffineM = np.array([[dyM, 0, 0, ym[0]], [0, dxM, 0, xm[0]], [0, 0, dzM, zm[int(sm)]], [0, 0, 0, 1]])
mrrScpLayerBase.affine.affine_matrix = mirrorAffineB
mrrScpLayerMov.affine.affine_matrix = mirrorAffineM
#cropNumRows, cropNumCols, cropNumSlcs = croppedScan.shape
if viewer.dims.order[0] == 2:
mrrScpLayerBase.data = croppedScanBase[:,:,None]
mrrScpLayerMov.data = croppedScanMov[:,:,None]
elif viewer.dims.order[0] == 0:
mrrScpLayerBase.data = croppedScanBase[None,:,:]
mrrScpLayerMov.data = croppedScanMov[None,:,:]
else:
mrrScpLayerBase.data = croppedScanBase[:,None,:]
mrrScpLayerMov.data = croppedScanMov[:,None,:]
#mrrScpLayerBase.refresh()
mrrScpLayerBase.contrast_limits = baseLayer.contrast_limits
mrrScpLayerBase.contrast_limits_range = baseLayer.contrast_limits_range
mrrScpLayerBase.colormap = baseLayer.colormap
mrrScpLayerMov.contrast_limits = movLayer.contrast_limits
mrrScpLayerMov.contrast_limits_range = movLayer.contrast_limits_range
mrrScpLayerMov.colormap = movLayer.colormap
mrrScpLayerBase.refresh()
mrrScpLayerMov.refresh()
if displayType == 'Mirrorscope':
if viewer.dims.order[0] == 2:
data = [[[rMinB-(rMaxB-rMinB)*0.1,cb,sb], [rMinB,cb,sb]]]
data.append([[rMaxB,cb,sb], [rMaxB+(rMaxB-rMinB)*0.1,cb,sb]])
data.append([[rMinB,cMinB,sb], [rMinB,2*cMaxB-cMinB,sb]])
data.append([[rMinB,cMinB,sb], [rMaxB,cMinB,sb]])
data.append([[rMaxB,cMinB,sb], [rMaxB,2*cMaxB-cMinB,sb]])
data.append([[rMinB,2*cMaxB-cMinB,sb], [rMaxB,2*cMaxB-cMinB,sb]])
elif viewer.dims.order[0] == 0:
data = [[[rb,cb,sMinB-(sMaxB-sMinB)*0.1], [rb,cb,sMinB]]]
data.append([[rb,cb,sMaxB], [rb,cb,sMaxB+(sMaxB-sMinB)*0.1]])
data.append([[rb,cMinB,sMinB], [rb,cMinB,sMaxB]])
data.append([[rb,2*cMaxB-cMinB,sMinB], [rb,2*cMaxB-cMinB,sMaxB]])
data.append([[rb,cMinB,sMinB], [rb,2*cMaxB-cMinB,sMinB]])
data.append([[rb,cMinB,sMaxB], [rb,2*cMaxB-cMinB,sMaxB]])
else:
data = [[[rb,cb,sMinB-(sMaxB-sMinB)*0.1], [rb,cb,sMinB]]]
data.append([[rb,cb,sMaxB], [rb,cb,sMaxB+(sMaxB-sMinB)*0.1]])
data.append([[rMinB,cb,sMinB], [rMinB,cb,sMaxB]])
data.append([[2*rMaxB-rMinB,cb,sMinB], [2*rMaxB-rMinB,cb,sMaxB]])
data.append([[rMinB,cb,sMinB], [2*rMaxB-rMinB,cb,sMinB]])
data.append([[rMinB,cb,sMaxB], [2*rMaxB-rMinB,cb,sMaxB]])
mirrorLine.data = np.asarray(data)
mirrorLine.refresh()
elif displayType == 'Sidebyside':
if viewer.dims.order[0] == 2:
data = [[[0,cb,sb], [5,cb,sb]]]
data.append([[numRows-5,cb,sb], [numRows,cb,sb]])
elif viewer.dims.order[0] == 0:
data = [[[rb,cb,-5], [rb,cb,0]]]
data.append([[rb,cb,numSlcs], [rb,cb,numSlcs+5]])
else:
data = [[[rb,cb,-5], [rb,cb,0]]]
data.append([[rb,cb,numSlcs], [rb,cb,numSlcs+5]])
mirrorLine.data = np.asarray(data)
mirrorLine.refresh()
#elif displayType == 'AlternateGrid':
#mirrorLine.data = np.asarray([])
#mirrorLine.refresh()
return
[docs]
def mirror_scope_callback(layer, event):
"""Mouse-drag callback that drives the mirror-scope registration QA overlay.
Registered as a ``mouse_drag_callbacks`` handler on the mirror-scope image
layers. On the initial click and on every subsequent drag event it reads the
current cursor world-coordinate from the viewer, stores it in the layer
metadata, and calls :func:`updateMirror` to refresh the cropped overlay
patches and boundary lines.
Args:
layer (napari.layers.Image): The mirror-scope image layer that received
the mouse event. Its ``metadata`` dict must contain keys
``'mirrorline'``, ``'displayType'``, ``'mirrorSize'``,
``'viewer'``, ``'baseLayer'``, ``'movLayer'``,
``'mrrScpLayerBase'``, and ``'mrrScpLayerMov'``.
event: The napari mouse event object (used as a generator via ``yield``
to distinguish click from drag).
Yields:
None: Yields control back to napari between the click and each move
event so the viewer can process intermediate frames.
Returns:
None
"""
# on click
#print('mouse clicked')
# mrrScpLayerBase.visible = True
# mrrScpLayerMov.visible = True
# mirrorLine.visible = True
dragged = False
clicked = True
if layer is None:
return
mirrorLine = layer.metadata['mirrorline']
displayType = layer.metadata['displayType']
mirrorSize = layer.metadata['mirrorSize']
viewer = layer.metadata['viewer']
baseLayer = layer.metadata['baseLayer']
movLayer = layer.metadata['movLayer']
mrrScpLayerBase = layer.metadata['mrrScpLayerBase']
mrrScpLayerMov = layer.metadata['mrrScpLayerMov']
#mrrScpLayerBase.visible = True
#mrrScpLayerMov.visible = True
#mirrorLine.visible = True
#mrrScpLayerBase.mouse_pan = False
#mrrScpLayerMov.mouse_pan = False
#mrrScpLayerBase.interactive = True
#mrrScpLayerMov.interactive = True
# Get the center of grid
# planC = baseLayer.metadata['planC']
# scanNum = baseLayer.metadata['scanNum']
# x,y,z = planC.scan[scanNum].getScanXYZVals()
# y = -y
currPt = viewer.cursor.position
#if 'currentPos' not in mrrScpLayerBase.metadata:
mrrScpLayerBase.metadata['currentPos'] = currPt #event.pos
mrrScpLayerMov.metadata['currentPos'] = currPt #event.pos
mrrScpLayerBase.metadata['currentAxis'] = viewer.dims.order[0] #event.pos
mrrScpLayerMov.metadata['currentAxis'] = viewer.dims.order[0] #event.pos
updateMirror(viewer, baseLayer, movLayer, mrrScpLayerBase, mrrScpLayerMov,
mirrorLine, mirrorSize, displayType)
yield
# on move
while event.type == 'mouse_move':
dragged = True
mrrScpLayerBase.metadata['currentPos'] = viewer.cursor.position #event.pos
mrrScpLayerMov.metadata['currentPos'] = viewer.cursor.position #event.pos
updateMirror(viewer, baseLayer, movLayer, mrrScpLayerBase, mrrScpLayerMov,
mirrorLine, mirrorSize, displayType)
yield
# on release
if dragged:
pass
return
[docs]
def showNapari(planC, scan_nums=0, struct_nums=[], dose_nums=[], vectors_dict={}, displayMode = '2d'):
"""Routine to display images in the Napari viewer. This routine requires a display (physical or virtual).
Args:
planC (cerr.plan_container.PlanC): pyCERR's plan container object
scan_nums (list or int): scan indices to display from planC.scan
struct_nums (list or int): structure indices to display from planC.structure
dose_nums (list or int): dose indices to display from planC.dose
vectors_dict: A dictionary whose fields are "vectors" and "features".
vectors must be an array of size nx2x3, where n is the number of vectors.
The 1st element along the 2nd dimension contains (row,col,slc) representing the start co-ordinate
The 2nd element along the 2nd dimension contains (yDeform,xDeform,zDeform) representing the lengths of
vectors along y, x and z axis in CERR virtual coordinates.
i.e. vectors[i,0,:] = [rStartV[i], cStartV[i], sStartV[i]]
vectors[i,1,:] = [yDeformV[i], xDeformV[i], zDeformV[i]]
displayMode: '2d': contours are displayed by labels layer
'3d' contours are displayed by surface layer.
Returns:
napari.Viewer: Napari Viewer object
List[napari.layers.Image]: List of scan layers corresponding to input scan_nums
List[napari.layers.Labels]: List of structure layers corresponding to input struct_nums
List[napari.layers.Image]: List of dose layers corresponding to input dose_nums
List[napari.layers.Vectors]: List containing DVF layer corresponding to input vector_dict
"""
if isinstance(scan_nums, (np.number, int, float)):
scan_nums = [scan_nums]
if isinstance(struct_nums, (np.number, int, float)):
struct_nums = [struct_nums]
if isinstance(dose_nums, (np.number, int, float)):
dose_nums = [dose_nums]
# Get Scan affines
assocScanV = []
for str_num in struct_nums:
assocScanV.append(scn.getScanNumFromUID(planC.structure[str_num].assocScanUID, planC))
allScanNums = [s for s in scan_nums]
allScanNums.extend(assocScanV)
allScanNums = np.unique(allScanNums)
scanAffineDict = {}
for scan_num in allScanNums:
x,y,z = planC.scan[scan_num].getScanXYZVals()
y = -y # negative since napari viewer y increases from top to bottom
dx = x[1] - x[0]
dy = y[1] - y[0]
dz = z[1] - z[0]
#if tiled:
# if scan_num > 0 and np.mod(scan_num,2) == 0:
# x += x[-1] + 2
scan_affine = np.array([[dy, 0, 0, y[0]], [0, dx, 0, x[0]], [0, 0, dz, z[0]], [0, 0, 0, 1]])
scanAffineDict[scan_num] = scan_affine
#from qtpy import QtWidgets, QtCore
#QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)
# above two lines are needed to allow to undock the widget with
# additional viewers - for Multiview
viewer = napari.Viewer(title='pyCERR')
## ======= Multiview - TBD =====
#from cerr import multiViewHelper
#dock_widget = multiViewHelper.MultipleViewerWidget(viewer)
#cross = multiViewHelper.CrossWidget(viewer)
#viewer.window.add_dock_widget(dock_widget, name="pyCERR")
#viewer.window.add_dock_widget(cross, name="Cross", area="left")
#starinterp_colormap = vispy.color.Colormap(custom_colormaps.starInterp())
starinterp_colormap = Colormap(
colors=custom_colormaps.starInterp(),
name="star (interp)",
nan_color=[0, 0, 0., 0] # transparent color
)
scan_colormaps = ["gray",("star (interp)", starinterp_colormap),
"bop orange","bop purple", "cyan", "green", "blue"] * 5
scan_layers = []
for i, scan_num in enumerate(scan_nums):
sa = planC.scan[scan_num].getScanArray()
scan_affine = scanAffineDict[scan_num]
opacity = 0.5
scan_name = planC.scan[scan_num].scanInfo[0].imageType
file_name = os.path.basename(planC.scan[scan_num].scanInfo[0].scanFileName)
if file_name[-4:] == '.dcm':
file_name = os.path.dirname(planC.scan[scan_num].scanInfo[0].scanFileName)
file_name = os.path.basename(file_name)
elif '.nii' in file_name:
separatorIndex = file_name.index('.nii')
file_name = file_name[:separatorIndex+1]
if scan_name == 'CT SCAN':
center = 0
width = 300
else:
minScan = np.nanpercentile(sa, 5)
scanNoBkgdV = sa[sa > minScan]
center = np.nanmedian(scanNoBkgdV)
lowerVal = np.nanpercentile(scanNoBkgdV, 5)
upperVal = np.nanpercentile(scanNoBkgdV, 95)
width = 2 * np.nanmax([center - lowerVal, upperVal - center])
if len(file_name) > 25:
file_name = file_name[:10] + ' ... ' + file_name[-10:]
scanDisplayStr = file_name + ' (' + scan_name + ')'
scanWindow = {'name': "--- Select ---",
'center': center,
'width': width}
scan_layers.append(viewer.add_image(sa,name=scanDisplayStr,affine=scan_affine,
opacity=opacity, colormap=scan_colormaps[i],
blending="additive",interpolation2d="linear",
interpolation3d="linear",
metadata = {'dataclass': 'scan',
'planC': planC,
'scanNum': scan_num,
'window': scanWindow}
))
scan_layers[-1].contrast_limits = [center-width/2, center+width/2]
scan_layers[-1].contrast_limits_range = [center-width/2, center+width/2]
dose_layers = []
for dose_num in dose_nums:
doseArray = planC.dose[dose_num].doseArray.copy()
dose_name = planC.dose[dose_num].fractionGroupID
xd,yd,zd = planC.dose[dose_num].getDoseXYZVals()
yd = -yd # negative since napari viewer y increases from top to bottom
dx = xd[1] - xd[0]
dy = yd[1] - yd[0]
dz = zd[1] - zd[0]
dose_affine = np.array([[dy, 0, 0, yd[0]], [0, dx, 0, xd[0]], [0, 0, dz, zd[0]], [0, 0, 0, 1]])
minDose = doseArray.min()
maxDose = doseArray.max()
centerDose = (minDose + maxDose) / 2
widthDose = (maxDose - minDose)
doseWindow = {"name": "--- Select ---",
"center": centerDose,
"width": widthDose}
assocScanNum = scn.getScanNumFromUID(planC.dose[dose_num].assocScanUID, planC)
dose_lyr = viewer.add_image(doseArray,name=dose_name, affine=dose_affine,
opacity=0.5,colormap=('starInterp', starinterp_colormap),
blending="additive",interpolation2d="linear",
interpolation3d="linear",
metadata = {'dataclass': 'dose',
'planC': planC,
'doseNum': dose_num,
'assocScanNum': assocScanNum,
'window': doseWindow}
)
dose_layers.append(dose_lyr)
dose_layers[-1].contrast_limits = [centerDose-widthDose/2, centerDose+widthDose/2]
dose_layers[-1].contrast_limits_range = [centerDose-widthDose/2, centerDose+widthDose/2]
# reference: https://gist.github.com/AndiH/c957b4d769e628f506bd
tableau20 = [(31, 119, 180), (174, 199, 232), (255, 127, 14), (255, 187, 120),
(44, 160, 44), (152, 223, 138), (214, 39, 40), (255, 152, 150),
(148, 103, 189), (197, 176, 213), (140, 86, 75), (196, 156, 148),
(227, 119, 194), (247, 182, 210), (127, 127, 127), (199, 199, 199),
(188, 189, 34), (219, 219, 141), (23, 190, 207), (158, 218, 229)] * 4
# Get x,y,z ranges for scaling results of marching cubes mesh
# mins = np.array([min(y), min(x), min(z)])
# maxes = np.array([max(y), max(x), max(z)])
# ranges = maxes - mins
struct_layer = []
for i,str_num in enumerate(struct_nums):
# Get scan affine
scan_num = scn.getScanNumFromUID(planC.structure[str_num].assocScanUID, planC)
scan_affine = scanAffineDict[scan_num]
#colr = np.asarray(tableau20[i])/255
colr = np.array(planC.structure[str_num].structureColor) / 255
colr = np.append(colr,1)
str_name = planC.structure[str_num].structureName
if displayMode.lower() == '3d':
#mins = np.array([min(y), min(x), min(z)])
#maxes = np.array([max(y), max(x), max(z)])
#ranges = maxes - mins
mask3M = rs.getStrMask(str_num,planC)
verts, faces, _, _ = measure.marching_cubes(volume=mask3M, level=0.5)
#verts_scaled = verts * ranges / np.array(mask3M.shape) - mins
cmap = vispy.color.Colormap([colr,colr])
isocenter = cerrStr.calcIsocenter(str_num, planC)
labl = viewer.add_surface((verts, faces),opacity=0.5,shading="flat",
affine=scan_affine, name=str_name,
colormap=cmap,
metadata = {'dataclass': 'structure',
'planC': planC,
'structNum': str_num,
'assocScanNum': scan_num,
'isocenter': isocenter})
struct_layer.append(labl)
elif displayMode.lower() == '2d':
cmap = DirectLabelColormap(color_dict={None: None, int(1): colr, int(0): np.array([0,0,0,0])})
#polygons = getContourPolygons(str_num, scan_num, planC)
# polygons =cerrStr.getContourPolygons(str_num, planC, rcsFlag=True)
# shp = viewer.add_shapes(polygons, shape_type='polygon', edge_width=2,
# edge_color=colr, face_color=[0]*4,
# affine=scan_affine, name=str_name)
# show as labels
mask3M = rs.getStrMask(str_num,planC)
isocenter = cerrStr.calcIsocenter(str_num, planC)
mask3M[mask3M] = 1 #int(str_num + 1)
# From napari 0.4.19 onwards
# from napari.utils import DirectLabelColormap
shp = viewer.add_labels(mask3M, name=str_name, affine=scan_affine,
blending='translucent',
colormap = cmap,
opacity = 1,
metadata = {'dataclass': 'structure',
'planC': planC,
'structNum': str_num,
'assocScanNum': scan_num,
'isocenter': isocenter})
shp.contour = 2
struct_layer.append(shp)
dvf_layer = []
if vectors_dict and 'vectors' in vectors_dict:
vectors = vectors_dict['vectors'].copy()
vectors[:,1,0] = -vectors[:,1,0]
feats = vectors_dict['features']
dvfScanNum = 0 # default
if 'scanNum' in vectors_dict:
dvfScanNum = vectors_dict['scanNum']
scan_affine = scanAffineDict[dvfScanNum]# {'length': lengthV, 'dx': vectors[:,1,1], 'dy': vectors[:,1,0], 'dz': vectors[:,1,2]}
vect_layr = viewer.add_vectors(vectors, edge_width=0.3, opacity=0.8,
length=1, name="Deformation Vector Field",
vector_style="arrow",
ndim=3, features=feats,
edge_colormap='husl',
affine = scan_affine,
metadata = {'dataclass': 'dvf',
'assocScanNum': dvfScanNum,
'planC': planC
}
)
dvf_layer.append(vect_layr)
viewer.dims.ndisplay = 2
if displayMode == '3d':
viewer.dims.ndisplay = 3
if len(scan_nums) > 0:
scan_num = 0
orientPos = ['L', 'P', 'S']
orientNeg = ['R', 'A', 'I']
flipDict = {}
for i in range(len(orientPos)):
flipDict[orientPos[i]] = orientNeg[i]
flipDict[orientNeg[i]] = orientPos[i]
ori = planC.scan[scan_num].getScanOrientation()
labels_list = [flipDict[dir] + ' --> ' + dir for dir in ori]
labels_list = [labels_list[1], labels_list[0], labels_list[2]]
viewer.dims.axis_labels = labels_list
viewer.dims.order = (2, 0, 1)
#viewer.dims.displayed_order = (2,0,1)
viewer.scale_bar.visible = True
viewer.scale_bar.unit = "cm"
#viewer.axes.visible = True
#if len(struct_layer)> 0:
image_window_widget = initialize_image_window_widget()
struct_add_widget = initialize_struct_create_widget()
struct_save_widget = initialize_struct_edit_widget()
struct_export_widget = initialize_struct_export_widget(viewer)
dose_select_widget = initialize_dose_select_widget()
dose_colorbar_widget = initialize_dose_colorbar_widget()
dvf_colorbar_widget = initialize_dvf_colorbar_widget()
reg_qa_widget = initialize_reg_qa_widget()
def set_center_slice(label):
# update viewer to display the central slice and capture screenshot
strNum = label.metadata['structNum']
scanNum = label.metadata['assocScanNum']
isocenter = label.metadata['isocenter']
viewer.layers.selection.active = label
viewer.dims.set_point(2, isocenter[2])
viewer.dims.set_point(1, isocenter[0])
viewer.dims.set_point(0, - isocenter[1])
return
def image_changed(image):
if image is None:
return
imgType = image.metadata['dataclass'] if 'dataclass' in image.metadata else ''
if not imgType in ['scan', 'dose']:
return
#if 'structNum' in image.metadata:
# return
if 'window' not in image.metadata:
return
#image = widgt[0].value
# Set active layer to the one selected
viewer.layers.selection.active = image
#window_option = widgt.CT_Window.value
#center = window_dict[window_option][0]
#width = window_dict[window_option][1]
#
windows_name = image.metadata['window']['name']
center = image.metadata['window']['center']
width = image.metadata['window']['width']
minVal = center - width/2
maxVal = center + width/2
rangeVal = [minVal, maxVal]
image_window_widget.Center.value = center
image_window_widget.Width.value = width
image_window_widget.CT_Window.value = windows_name
#image.contrast_limits_range = rangeVal
image.contrast_limits = rangeVal
update_colorbar(image)
return
def window_changed(CT_Window):
if CT_Window is None:
return
ctrWidth = window_dict[CT_Window]
if CT_Window != '--- Select ---':
image_window_widget.Center.value = ctrWidth[0]
image_window_widget.Width.value = ctrWidth[1]
image_window_widget.CT_Window.value = CT_Window
return
def center_width_changed(ctrWidth):
if ctrWidth is None:
return
image_window_widget.CT_Window.value = '--- Select ---'
return
def structure_export_format_changed(file_format):
if file_format is None:
return
if file_format == 'DICOM':
fileExt = '*.dcm'
else:
fileExt = '*.nii.gz'
struct_export_widget.file_name.filter = fileExt
struct_export_widget.file_name.value = ''
return
def struct_export_widget_reset_choices(widgt):
allStructureLabels = getLabelsList(viewer)
if set(struct_export_widget.structures.choices) != set(allStructureLabels):
struct_export_widget.structures.choices = allStructureLabels
return
def label_changed(widgt):
label = widgt[0].value
if label is None:
# Set active layer to scan
viewer.layers.selection.active = scan_layers[0]
return
if viewer.dims.ndisplay == 2 and isinstance(label.metadata['isocenter'][0], (np.number, int, float)):
set_center_slice(label)
return
def layer_active(event):
if not hasattr(event, 'value'):
return
layer = event.value
if not hasattr(layer, 'metadata'):
return
imgType = layer.metadata['dataclass'] if 'dataclass' in layer.metadata else ''
if 'structNum' in layer.metadata and viewer.dims.ndisplay == 2:
struct_save_widget[0].value = layer
if isinstance(layer.metadata['isocenter'][0], (np.number, int, float)):
set_center_slice(layer)
elif imgType in ['scan', 'dose']:
#update_colorbar(layer)
image_changed(layer)
def dose_changed(widgt):
if widgt.image is None:
return
#mz_canvas = dose_colorbar_widget
dose = widgt[0].value
update_colorbar(dose)
def cmap_changed(event):
#print(event.value())
update_colorbar(viewer.layers.selection.active)
return
def contrast_changed(event):
image = event.source
#contrast_limits = image.contrast_limits
#minClipValue = contrast_limits[0]
#maxClipValue = contrast_limits[1]
#dataclass = image.metadata['dataclass']
#if dataclass == 'scan':
# data = planC.scan[image.metadata['scanNum']].getScanArray()
#elif dataclass == 'dose':
# data = planC.dose[image.metadata['doseNum']].doseArray.copy()
#data[data <= minClipValue] = minClipValue
#data[data >= maxClipValue] = maxClipValue
#image.data = data
#contrast_limits_range = image.contrast_limits_range
#center = (contrast_limits_range[0] + contrast_limits_range[1]) / 2
#width = contrast_limits_range[1] - contrast_limits_range[0]
#scanWindow = {'name': "--- Select ---",
# 'center': center,
# 'width': width}
#image.metadata['window'] = scanWindow
#image_window_widget.Center.value = scanWindow['center']
#image_window_widget.Width.value = scanWindow['width']
#image_window_widget.CT_Window.value = scanWindow['name']
update_colorbar(image)
#image.refresh()
return
def reset_mirror_position():
lyrNames = [lyr.name for lyr in viewer.layers]
if 'Mirror-Scope-base' in lyrNames:
mrrScpLayerBaseInd = lyrNames.index('Mirror-Scope-base')
mrrScpLayerMovInd = lyrNames.index('Mirror-Scope-mov')
mrrScpLayerBase = viewer.layers[mrrScpLayerBaseInd]
mrrScpLayerMov = viewer.layers[mrrScpLayerMovInd]
currentAxis = mrrScpLayerBase.metadata['currentAxis']
if currentAxis != viewer.dims.order[0]:
currentPos = [(rng[0] + rng[1])/2 for rng in viewer.dims.range]
currentAxis = viewer.dims.order[0]
else:
currentPos = list(mrrScpLayerBase.metadata['currentPos'])
currentAxis = mrrScpLayerBase.metadata['currentAxis']
currentPos[currentAxis] = viewer.dims.point[currentAxis]
mrrScpLayerBase.metadata['currentAxis'] = currentAxis
mrrScpLayerMov.metadata['currentAxis'] = currentAxis
mrrScpLayerBase.metadata['currentPos'] = currentPos
mrrScpLayerMov.metadata['currentPos'] = currentPos
mirror_scope_changed(reg_qa_widget)
return
def dims_order_changed(event):
dims = event.source
if dims.order == (1,0,2): # Axial
viewer.dims.order = (2,0,1)
elif dims.order == (0,1,2):
viewer.dims.order = (0,2,1)
#mirror_scope_changed(reg_qa_widget)
reset_mirror_position( )
update_colorbar(viewer.layers.selection.active)
return
def dims_point_changed(event):
# Update Mirrorscope
reset_mirror_position()
return
def update_colorbar(image):
if image is None:
for lyr in viewer.layers:
if lyr.metadata['dataclass'] in ['scan','structure']:
image = lyr
break
if image is None:
return
# get Image units
imgType = image.metadata['dataclass'] if 'dataclass' in image.metadata else ''
units = ''
if imgType == 'scan':
planC = image.metadata['planC']
scanNum = image.metadata['scanNum']
units = planC.scan[scanNum].scanInfo[0].imageUnits
elif imgType == 'dose':
planC = image.metadata['planC']
doseNum = image.metadata['doseNum']
scanNum = image.metadata['assocScanNum']
units = planC.dose[doseNum].doseUnits
elif imgType == 'dvf':
planC = image.metadata['planC']
scanNum = image.metadata['assocScanNum']
featureName = image._edge.color_properties.name
units = featureName
elif imgType == 'structure':
planC = image.metadata['planC']
scanNum = image.metadata['assocScanNum']
featureName = ''
units = ''
else:
return
with plt.style.context('dark_background'):
#mz_canvas = FigureCanvasQTAgg(Figure(figsize=(1, 0.1)))
if imgType in ['scan', 'dose']:
minVal = image.contrast_limits[0]
maxVal = image.contrast_limits[1]
mz_canvas = dose_colorbar_widget
norm = mpl.colors.Normalize(vmin=minVal, vmax=maxVal)
elif imgType in ['dvf']:
minVal = image.properties[featureName].min() #image.edge_contrast_limits[0]
maxVal = image.properties[featureName].max() #image.edge_contrast_limits[1]
mz_canvas = dvf_colorbar_widget
norm = mpl.colors.Normalize(vmin=minVal, vmax=maxVal)
else:
mz_canvas = dose_colorbar_widget
mz_axes = mz_canvas.figure.axes
# Delete axes children
for axNum in range(len(mz_axes)):
#if imgType == 'structure' and axNum == 0:
# continue
children = mz_axes[axNum].get_children()
text_objects = [child for child in children if isinstance(child, plt.Text)]
for text in text_objects:
text.set_text('')
for child in children:
del child
if len(mz_axes) == 0 and imgType in ['scan', 'dose', 'structure']:
mz_canvas.figure.add_axes([0.1, 0.3, 0.2, 0.4]) #mz_canvas.figure.subplots()
mz_canvas.figure.add_axes([0.2, 0.1, 0.4, 0.1]) # orientation display axis
mz_axes = mz_canvas.figure.axes
elif len(mz_axes) == 0 and imgType in ['dvf']: # imgType = 'dvf'
mz_canvas.figure.add_axes([0.1, 0.3, 0.2, 0.4])
mz_axes = mz_canvas.figure.axes
if imgType in ['scan', 'dose']:
colors = ListedColormap(image.colormap.colors)
elif imgType == 'dvf':
colors = ListedColormap(image.edge_colormap.colors)
if imgType in ['scan', 'dose', 'dvf']:
cb1 = mpl.colorbar.ColorbarBase(mz_axes[0], cmap=colors,
norm=norm,
orientation='vertical')
mz_axes[0].get_xaxis().set_visible(False)
cb1.set_label(units)
# Draw arrows for patient direction
orientPos = ['L', 'P', 'S']
orientNeg = ['R', 'A', 'I']
flipDict = {}
for i in range(len(orientPos)):
flipDict[orientPos[i]] = orientNeg[i]
flipDict[orientNeg[i]] = orientPos[i]
oriStr = planC.scan[scanNum].getScanOrientation()
viewOrder = viewer.dims.order
mz_axes[1].arrow(0.2, 0.2, 0.6, 0, width=0.02, head_width=0.2, head_length=0.2)
mz_axes[1].arrow(0.2, 0.2, 0, 0.5, width=0.02, head_width=0.2, head_length=0.2)
mz_axes[1].set_xlim(0,1)
mz_axes[1].set_ylim(0,1)
mz_axes[1].axis('off')
if viewOrder[0] == 2:
xOri = oriStr[viewOrder[1]]
yOri = flipDict[oriStr[viewOrder[2]]]
elif viewOrder[0] == 1:
xOri = oriStr[viewOrder[0]]
yOri = flipDict[oriStr[viewOrder[1]]]
else:
xOri = oriStr[viewOrder[0]]
yOri = flipDict[oriStr[viewOrder[1]]]
mz_axes[1].text(0, 1, yOri, fontsize=12, color='cyan')
mz_axes[1].text(1.1, 0.1, xOri, fontsize=12, color='cyan')
mz_canvas.draw()
mz_canvas.flush_events()
#mz_axes.axis('image')
#mz_axes.imshow(dose_layers[-1].colormap.colors)
#mz_canvas.figure.tight_layout()
return
def mirror_scope_changed(widgt):
viewer = widgt[0].value
baseLayer = widgt[1].value
movLayer = widgt[2].value
displayType = widgt[3].value
mirrorSize = widgt[4].value
movOpacity = widgt[5].value/100
baseOpacity = 1 - movOpacity
layerNames = []
for lyr in viewer.layers:
layerNames.append(lyr.name)
lyr.visible = False
baseLayer.opacity = baseOpacity
movLayer.opacity = movOpacity
baseLayer.visible = True
movLayer.visible = True
if 'Mirror-Scope-base' not in layerNames:
return
mrrBaseInd = layerNames.index('Mirror-Scope-base')
mrrMovInd = layerNames.index('Mirror-Scope-mov')
mrrLineInd = layerNames.index('Mirror-line')
mrrScpLayerBase = viewer.layers[mrrBaseInd]
mrrScpLayerMov = viewer.layers[mrrMovInd]
mirrorLine = viewer.layers[mrrLineInd]
mrrScpLayerBase.metadata['mirrorSize'] = mirrorSize
mrrScpLayerMov.metadata['mirrorSize'] = mirrorSize
#baseLayer.refresh()
#movLayer.refresh()
#reg_qa_widget.call_button.text = displayType
if displayType == '--- OFF ---':
widgt[4].visible = False
widgt[5].visible = False
# Delete mirror layers
if isinstance(mrrLineInd, (int, np.integer)):
del viewer.layers[mrrLineInd]
if isinstance(mrrMovInd, (int, np.integer)):
del viewer.layers[mrrMovInd]
if isinstance(mrrBaseInd, (int, np.integer)):
del viewer.layers[mrrBaseInd]
# Set scan layer as active
viewer.layers.selection.active = baseLayer
return
if displayType == 'Mirrorscope':
widgt[4].visible = True
widgt[5].visible = False
mrrScpLayerBase.visible = True
mrrScpLayerMov.visible = True
mirrorLine.visible = True
mrrScpLayerBase.mouse_pan = False
mrrScpLayerMov.mouse_pan = False
elif displayType == 'Sidebyside':
widgt[4].visible = False
widgt[5].visible = False
mrrScpLayerBase.visible = True
mrrScpLayerMov.visible = True
mirrorLine.visible = True
mrrScpLayerBase.mouse_pan = False
mrrScpLayerMov.mouse_pan = False
elif displayType == 'Toggle':
widgt[4].visible = False
widgt[5].visible = True
mrrScpLayerBase.visible = False
mrrScpLayerMov.visible = False
mirrorLine.visible = False
mrrScpLayerBase.mouse_pan = True
mrrScpLayerMov.mouse_pan = True
elif displayType == 'AlternateGrid':
widgt[4].visible = True
widgt[5].visible = False
mrrScpLayerBase.visible = True
mrrScpLayerMov.visible = True
mirrorLine.visible = False
mrrScpLayerBase.mouse_pan = True
mrrScpLayerMov.mouse_pan = True
if 'currentPos' in mrrScpLayerBase.metadata:
updateMirror(viewer, baseLayer, movLayer, mrrScpLayerBase,
mrrScpLayerMov, mirrorLine, mirrorSize, displayType)
return
# Change slice to center of that structure
struct_save_widget.changed.connect(label_changed)
struct_save_widget.called.connect(struct_export_widget_reset_choices)
image_window_widget.image.changed.connect(image_changed)
image_window_widget.CT_Window.changed.connect(window_changed)
image_window_widget.Center.changed.connect(center_width_changed)
image_window_widget.Width.changed.connect(center_width_changed)
scanWidget = viewer.window.add_dock_widget([image_window_widget], area='left', name="Window", tabify=True)
struct_export_widget.file_format.changed.connect(structure_export_format_changed)
structWidget = viewer.window.add_dock_widget([struct_add_widget, struct_save_widget, struct_export_widget], area='left', name="Segmentation", tabify=True)
colorbars_dock = viewer.window.add_dock_widget([dose_colorbar_widget], area='right', name="Image Colorbar", tabify=True)
dvf_dock = viewer.window.add_dock_widget([dvf_colorbar_widget], area='right', name="DVF Colorbar", tabify=True)
reg_qa_dock = viewer.window.add_dock_widget(reg_qa_widget, area='left', name="Reg QA", tabify=True)
reg_qa_widget.changed.connect(mirror_scope_changed)
#colorbars_dock.resize(5, 20)
# This line sets the index of the active DockWidget
scanWidget.parent().findChildren(QTabBar)[0].setCurrentIndex(0)
viewer.layers.events.inserted.connect(struct_add_widget.reset_choices)
viewer.layers.events.inserted.connect(struct_save_widget.reset_choices)
viewer.layers.events.removed.connect(struct_add_widget.reset_choices)
viewer.layers.events.removed.connect(struct_save_widget.reset_choices)
viewer.layers.selection.events.active.connect(layer_active)
viewer.layers.selection.events.changed.connect(layer_active)
viewer.layers.events.changed.connect(layer_active)
viewer.dims.events.order.connect(dims_order_changed)
viewer.dims.events.point.connect(dims_point_changed)
for dose_lyr in dose_layers:
#dose_lyr.events.contrast_limits_range.connect(layer_active)
dose_lyr.events.colormap.connect(cmap_changed)
dose_lyr.events.contrast_limits_range.connect(cmap_changed)
dose_lyr.events.contrast_limits.connect(contrast_changed)
for scan_lyr in scan_layers:
#scan_lyr.events.contrast_limits_range.connect(layer_active)
scan_lyr.events.colormap.connect(cmap_changed)
scan_lyr.events.contrast_limits_range.connect(cmap_changed)
scan_lyr.events.contrast_limits.connect(contrast_changed)
for vect_layr in dvf_layer:
vect_layr.events.edge_color.connect(cmap_changed)
#dose_select_widget.changed.connect(dose_changed)
viewer.layers.selection.active = scan_layers[0]
update_colorbar(scan_layers[0])
if len(dvf_layer) > 0:
update_colorbar(dvf_layer[0])
#from napari_orthogonal_views.ortho_view_manager import show_orthogonal_views
#show_orthogonal_views(viewer)
viewer.show(block=False)
#napari.run()
# Set Image colorbar active
colorbars_dock.parent().findChildren(QTabBar)[2].setCurrentIndex(0)
return viewer, scan_layers, struct_layer, dose_layers, dvf_layer
[docs]
def captureToFile(planC, fileName, scanNums, strNums, doseNums, vectorDict, centerStr, dispOpts=[]):
"""Renders axial, sagittal, and coronal screenshots of a structure and saves them to a file.
Opens a Napari viewer via :func:`showNapari`, navigates to the slices with the
largest cross-section of ``centerStr`` along each axis, captures screenshots,
then saves all three views as a single image file using matplotlib.
Args:
planC (cerr.plan_container.PlanC): pyCERR's plan container object.
fileName (str): Output image file path (e.g. ``'output.png'``).
scanNums (int or list[int]): Scan index or indices to display.
strNums (int or list[int]): Structure index or indices to display.
doseNums (int or list[int]): Dose index or indices to display.
vectorDict (dict): Deformation vector field dictionary passed to
:func:`showNapari` (may be empty).
centerStr (int): Index of the structure in ``planC.structure`` used to
determine which slice to show in each view.
dispOpts (list or dict, optional): Display options controlling opacity,
windowing, and contour width for scan, structure, and dose layers.
Defaults to ``[]`` (no overrides).
Returns:
None
"""
def saveViews(images, filename, titles):
fig, axes = plt.subplots(1, len(images), figsize=(12, 4))
for ax, img, title in zip(axes, images, titles):
ax.imshow(img, cmap='gray')
ax.set_title(title)
ax.axis('off')
plt.tight_layout()
plt.savefig(filename, dpi=150)
plt.close()
if isinstance(scanNums, (int, float, np.number)):
scanNums = [scanNums]
if isinstance(strNums, (int, float, np.number)):
strNums = [strNums]
if isinstance(doseNums, (int, float, np.number)):
doseNums = [doseNums]
# Display scan
viewer, scan_layer, struct_layer, dose_layer, dvf_layer = \
showNapari(planC, scanNums, strNums, doseNums, vectors_dict=vectorDict, displayMode='2d')
numScans = len(scanNums)
numStrs = len(strNums)
numDoses = len(doseNums)
for i,_ in enumerate(scanNums):
if 'scan' in dispOpts and 'opacity' in dispOpts['scan'][i]:
scan_layer[i].opacity = dispOpts['scan'][i]
else:
scan_layer[i].opacity = 1/numScans
if 'scan' in dispOpts and 'window' in dispOpts['scan'][i]:
window = dispOpts['scan'][i]['window']
if isinstance(window, (str)) and (window in window_dict):
center = window_dict[window][0]
width = window_dict[window][1]
scan_layer[i].contrast_limits = [center-width/2, center+width/2]
scan_layer[i].contrast_limits_range = [center-width/2, center+width/2]
scan_layer[i].refresh()
for i,_ in enumerate(strNums):
if 'structure' in dispOpts and 'opacity' in dispOpts['structure'][i]:
struct_layer[i].opacity = dispOpts['structure'][i]['opacity']
else:
struct_layer[i].opacity = 0.5
if 'structure' in dispOpts:
struct_layer[i].contour = dispOpts['structure'][i]['width']
else:
struct_layer[i].contour = 0
struct_layer[i].refresh()
for i,_ in enumerate(doseNums):
if 'dose' in dispOpts and 'opacity' in dispOpts['dose'][i]:
dose_layer[i].opacity = dispOpts['dose'][i]['opacity']
else:
dose_layer[i].opacity = 0.5/numDoses
dose_layer[i].refresh()
# Show structure central slice
assocScanNum = planC.structure[centerStr].getStructureAssociatedScan(planC)
mask3M = rs.getStrMask(centerStr, planC)
rV, cV, sV = np.where(mask3M)
screenshots = []
# update viewer to display the central slice and capture screenshot
xV, yV, zV = planC.scan[assocScanNum].getScanXYZVals()
# Axial
uniqueSlices, counts = np.unique(sV, return_counts=True)
maxSizeInd = uniqueSlices[np.argmax(counts)]
#midSliceInd = int(np.round(np.median(uniqueSlices))) #corrected
viewer.dims.set_point(2, zV[maxSizeInd])
viewer.dims.order = (1, 2, 0) # required to refresh viewer
viewer.dims.order = (2, 0, 1)
screenshots.append(viewer.screenshot())
# Sagittal
uniqueCols, counts = np.unique(cV, return_counts=True)
maxSizeInd = uniqueCols[np.argmax(counts)]
#midSliceInd = int(np.round(np.median(uniqueCols)))
viewer.dims.set_point(1, xV[maxSizeInd])
viewer.dims.order = (1, 2, 0)
screenshots.append(viewer.screenshot())
# Coronal
uniqueRows, counts = np.unique(rV, return_counts=True)
maxSizeInd = uniqueRows[np.argmax(counts)]
#midSliceInd = int(np.round(np.median(uniqueRows)))
viewer.dims.set_point(0, -yV[maxSizeInd])
viewer.dims.order = (0, 2, 1)
screenshots.append(viewer.screenshot())
titles = ['Axial', 'Sagittal', 'Coronal']
saveViews(screenshots, fileName, titles)
viewer.close()
return