"""Port of src/coilSense/automatic/selectROI.m (one Qt window instead of two figures)."""
from __future__ import annotations
from functools import lru_cache
import numpy as np
from monalisa._mat import disp, mround, vec
from monalisa.array._block_reshape import block_reshape
from monalisa.display._qt import mpl_canvas, qt_app, run_dialog
# projection -> (summed axis, row axis, column axis) of MATLAB's xRMS / yRMS / zRMS images
VIEWS = {"Along X": (0, 1, 2), "Along Y": (1, 2, 0), "Along Z": (2, 0, 1)}
AXES = "xyz"
def projection(data, view) -> np.ndarray:
"""Normalized sum along one axis, rows and columns as in MATLAB (permute, then sum(., 3))."""
s, r, c = VIEWS[view]
p = np.sum(np.transpose(data, (r, c, s)), axis=2)
lo, hi = p.min(), p.max()
return (p - lo) / (hi - lo) if hi > lo else p * 0
def border_extraction(edges, N_u) -> np.ndarray:
"""Rectangle edges (pixel-edge coordinates in [0.5, N + 0.5]) to the returned 1-based voxel
indices ``[xMin, xMax; yMin, yMax; zMin, zMax]`` (MATLAB borderExtraction)."""
out = np.zeros((3, 2))
for k in range(3):
N = N_u[k]
for j in range(2):
out[k, j] = mround(1 + ((edges[k][j] - 0.5) / N) * (N - 1))
return out
[docs]
def select_roi(dataRMS, dataMIP, N_u, autoFlag=None, boxPos=None):
"""Bounding box of the region of interest, drawn on the RMS and MIP images summed along x, y
and z (6 images; moving one rectangle updates the others, and the limits can be typed in).
``boxPos`` = ``[xMin, xWidth; yMin, yWidth; zMin, zWidth]`` is the initial box (default
``[2, min(N_u) - 4]`` on every axis). Waits until Confirm.
Returns ``[xMin, xMax; yMin, yMax; zMin, zMax]`` (1-based). Closing the window without
confirming returns the initial box. Without a window: pass ``borders`` to
coil_sense_non_cart_mask_automatic.
"""
N_u = vec(N_u)
dataRMS = block_reshape(dataRMS, N_u)
dataMIP = block_reshape(dataMIP, N_u)
if boxPos is None:
boxPos = np.tile([2.0, np.min(N_u) - 4], (3, 1))
boxPos = np.asarray(boxPos, dtype=np.float64)
if autoFlag is None:
autoFlag = False
coordinates = np.column_stack([boxPos[:, 0], boxPos[:, 0] + boxPos[:, 1]])
dlg = dialog_class()(dataRMS, dataMIP, N_u, boxPos, editable=not autoFlag)
if run_dialog(dlg):
coordinates = border_extraction(dlg.edges, N_u)
b = coordinates.astype(int).tolist()
disp(f"select_roi: borders = [[{b[0][0]}, {b[0][1]}], [{b[1][0]}, {b[1][1]}], [{b[2][0]}, {b[2][1]}]]")
else:
disp("Use confirm to use the new values. Don't close the figures!")
return coordinates
@lru_cache(maxsize=1)
def dialog_class():
from matplotlib.widgets import RectangleSelector
from qtpy.QtWidgets import QDialog, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QVBoxLayout
class SelectROIDialog(QDialog):
"""``edges[k] = [low, high]`` per axis x, y, z, in pixel-edge coordinates."""
def __init__(self, dataRMS, dataMIP, N_u, boxPos, editable=True):
qt_app("Pass borders to coil_sense_non_cart_mask_automatic, or use autoFlag=True.")
super().__init__()
self.setWindowTitle("select_roi")
self.N_u = [float(n) for n in N_u]
self.edges = [[boxPos[k, 0], boxPos[k, 0] + boxPos[k, 1]] for k in range(3)]
self.fig, self.canvas = mpl_canvas(11, 7)
self.selectors = []
for i, (name, data) in enumerate((("RMS", dataRMS), ("MIP", dataMIP))):
for j, view in enumerate(VIEWS):
ax = self.fig.add_subplot(2, 3, 3 * i + j + 1)
p = projection(data, view)
ax.imshow(p, cmap="gray", extent=(0.5, p.shape[1] + 0.5, p.shape[0] + 0.5, 0.5))
_, r, c = VIEWS[view]
ax.set_xlabel(AXES[c].upper())
ax.set_ylabel(AXES[r].upper())
ax.set_title(f"{name} summed {view.lower()}", fontsize=9)
sel = RectangleSelector(ax, self._moved(view), interactive=True, useblit=False,
props={"edgecolor": "c", "facecolor": "none", "linewidth": 1.5},
ignore_event_outside=True)
self.selectors.append((view, sel))
self.fields = {}
grid = QGridLayout()
for k, axis in enumerate(AXES):
grid.addWidget(QLabel(f"{axis.upper()} min / max"), 0, 2 * k, 1, 2)
for j, side in enumerate(("Min", "Max")):
f = QLineEdit()
f.setToolTip(f"{axis.upper()}{side} of the mask")
f.setReadOnly(not editable)
f.editingFinished.connect(lambda k=k, j=j: self._typed(k, j))
self.fields[(k, j)] = f
grid.addWidget(f, 1, 2 * k + j)
ok = QPushButton("Confirm Selection")
ok.clicked.connect(self.accept)
bar = QHBoxLayout()
bar.addLayout(grid)
bar.addStretch()
bar.addWidget(ok)
lay = QVBoxLayout(self)
lay.addWidget(self.canvas)
lay.addLayout(bar)
self.update_all()
self.resize(1100, 800)
def _moved(self, view):
def onselect(press, _release):
sel = next((s for v, s in self.selectors if v == view and s.ax is press.inaxes), None)
if sel is None:
return
x0, x1, y0, y1 = sel.extents
_, r, c = VIEWS[view]
self.edges[c] = [x0, x1]
self.edges[r] = [y0, y1]
self.update_all()
return onselect
def _typed(self, k, j):
try:
v = float(self.fields[(k, j)].text())
except ValueError:
self.update_all()
return
self.set_edge(k, j, v)
def set_edge(self, k, j, v):
self.edges[k][j] = min(max(0.5, v), self.N_u[k] + 0.5)
self.update_all()
def update_all(self):
for view, sel in self.selectors:
_, r, c = VIEWS[view]
sel.extents = (*self.edges[c], *self.edges[r])
for (k, j), f in self.fields.items():
f.setText(f"{self.edges[k][j]:g}")
self.canvas.draw_idle()
return SelectROIDialog