Skip to content

Commit

Permalink
Squashed commit of the following:
Browse files Browse the repository at this point in the history
    Interactove ROI from Issue microscope-cockpit#837
    added dialog to apply ROI to other active cameras
    ROI selection now not square, but can be forced with shift-drag
    Added UPDATE_ROI event and changed imageviewer and mosaics to use it
    keep ROI bounding box in camera view until a new image is snapped
    force a moasic update after an roi change so moasics have the correct image size on them
    Allow setting roi interactively
  • Loading branch information
iandobbie committed Jul 19, 2023
1 parent 46eab08 commit 5931e0b
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 23 deletions.
26 changes: 22 additions & 4 deletions cockpit/devices/microscopeCamera.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ def getHandlers(self):
'getExposureTime': self.getExposureTime,
'setExposureTime': self.setExposureTime,
'getSavefileInfo': self.getSavefileInfo,
'setROI': self.setROI,
'getROI': self.getROI,
'getSensorShape': self.getSensorShape,
'makeUI': self.makeUI,
'softTrigger': self.softTrigger},
cockpit.handlers.camera.TRIGGER_SOFT,
Expand Down Expand Up @@ -269,16 +272,25 @@ def getExposureTime(self, name=None, isExact=False):

def getImageSize(self, name):
"""Read the image size from the camera."""
roi = self._proxy.get_roi() # left, bottom, right, top
if not isinstance(roi, ROI):
cockpit.util.logger.log.warning("%s returned tuple not ROI()" % self.name)
roi = ROI(*roi)
roi = self.getROI(name)
binning = self._proxy.get_binning()
if not isinstance(binning, Binning):
cockpit.util.logger.log.warning("%s returned tuple not Binning()" % self.name)
binning = Binning(*binning)
return (roi.width//binning.h, roi.height//binning.v)

def getROI(self, name):
"""Read the ROI from the camera"""
roi = self._proxy.get_roi()
if not isinstance(roi, ROI):
cockpit.util.logger.log.warning("%s returned tuple not ROI()" % self.name)
roi = ROI(*roi)
return roi

def getSensorShape(self, name):
"""Read the sensor shape from the camera"""
sensor_shape = self._proxy.get_sensor_shape()
return sensor_shape

def getSavefileInfo(self, name):
"""Return an info string describing the measurement."""
Expand Down Expand Up @@ -329,6 +341,12 @@ def setExposureTime(self, name, exposureTime):
self._proxy.set_exposure_time(exposureTime / 1000.0)


def setROI(self, name, roi):
result = self._proxy.set_roi(roi)

if not result:
cockpit.util.logger.log.warning("%s could not set ROI" % self.name)

def softTrigger(self, name=None):
if self.enabled:
self._proxy.soft_trigger()
Expand Down
4 changes: 4 additions & 0 deletions cockpit/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@
``CAMERA_ENABLE``
``UPDATE_ROI``
A camera has updated its ROI which might change other displays.
``CLEANUP_AFTER_EXPERIMENT``
``DEVICE_STATUS``
Expand Down Expand Up @@ -185,6 +188,7 @@
LIGHT_SOURCE_ENABLE = 'light source enable'
LIGHT_EXPOSURE_UPDATE = 'light exposure update'
CAMERA_ENABLE = 'camera enable'
UPDATE_ROI = 'update roi'
FILTER_CHANGE = 'filter change'
STAGE_POSITION = 'stage position'
STAGE_MOVER = 'stage mover'
Expand Down
147 changes: 139 additions & 8 deletions cockpit/gui/imageViewer/viewCanvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
HISTOGRAM_HEIGHT = 40

## Drag modes
(DRAG_NONE, DRAG_CANVAS, DRAG_BLACKPOINT, DRAG_WHITEPOINT) = range(4)
(DRAG_NONE, DRAG_CANVAS, DRAG_BLACKPOINT, DRAG_WHITEPOINT, DRAG_ROI) = range(5)


class BaseGL():
Expand Down Expand Up @@ -467,6 +467,14 @@ def __init__(self, parent, *args, **kwargs):
self.panX = 0
self.panY = 0

## ROI
self.roi = None # Current roi
self.roi_drag = None # ROI currently being defined
self.definingROI = False # Are we defining the ROI
self.definedROI = False # New ROI defined but not grabbed.
self.shift_down = False #shift key drags a square ROI


## What kind of dragging we're doing.
self.dragMode = DRAG_NONE

Expand All @@ -485,12 +493,17 @@ def __init__(self, parent, *args, **kwargs):
self.Bind(wx.EVT_MOUSE_EVENTS, self.onMouse)
self.Bind(wx.EVT_MOUSEWHEEL, self.onMouseWheel)
self.Bind(wx.EVT_DPI_CHANGED, self.onDPIchange)

# Right click also creates context menu event, which will pass up
# if unhandled. Bind it to None to prevent the main window
# context menu being displayed after our own.
self.Bind(wx.EVT_CONTEXT_MENU, lambda event: None)
self.painting = False

#shift key to make ROI square requirse knowing if the key is down
self.Bind(wx.EVT_KEY_DOWN, self.onKeyDown)
self.Bind(wx.EVT_KEY_UP, self.onKeyUp)

# Initialise FFT variables
self.showFFT = False

Expand Down Expand Up @@ -554,6 +567,7 @@ def clear(self, shouldDestroy = False):
# actually display the image.
def setImage(self, newImage):
self.imageQueue.put_nowait(newImage)
self.definedROI = False # new image will have new ROI.


## Consume images out of self.imageQueue and either display them or
Expand Down Expand Up @@ -633,8 +647,10 @@ def onPaint(self, event):
self.image.draw(pan=(self.panX, self.panY), zoom=self.zoom)
if self.showCrosshair:
self.drawCrosshair()


if self.definingROI:
self.drawROI()
if self.definedROI:
self.drawROI()
glViewport(0, 0, self.w, Hist_Height//2)
self.histogram.draw()
glColor(0, 1, 0, 1)
Expand Down Expand Up @@ -674,6 +690,24 @@ def drawCrosshair(self):
(self.zoom*self.panX, -1), (self.zoom*self.panX, 1)])
glDrawArrays(GL_LINES, 0, 4)

@cockpit.util.threads.callInMainThread
def drawROI(self):
if self.roi_drag:
glColor3f(255, 0, 0)

# Get raw gl co-ordinates
v = [self.indicesToGl(self.roi_drag[1], self.roi_drag[0]),
self.indicesToGl(self.roi_drag[1] + self.roi_drag[3], self.roi_drag[0]),
self.indicesToGl(self.roi_drag[1] + self.roi_drag[3], self.roi_drag[0] + self.roi_drag[2]),
self.indicesToGl(self.roi_drag[1], self.roi_drag[0] + self.roi_drag[2])
]

# Correct co-ordinates for zoom and pan
v_zoompan = [((p[0] + self.panX) * self.zoom, (p[1] + self.panY) * self.zoom) for p in v]

# Draw roi box
glVertexPointerf(v_zoompan)
glDrawArrays(GL_LINE_LOOP, 0, 4)

## Update the size of the canvas by scaling it.
def setSize(self, size):
Expand All @@ -695,12 +729,16 @@ def onMouse(self, event):
elif event.LeftDown():
# Started dragging
self.mouseDragX, self.mouseDragY = self.curMouseX, self.curMouseY
self.mouseLdownX, self.mouseLdownY = self.curMouseX, self.curMouseY
blackPointX = 0.5 * (1+self.histogram.data2gl(self.histogram.lthresh)) * self.w
whitePointX = 0.5 * (1+self.histogram.data2gl(self.histogram.uthresh)) * self.w
# Set drag mode based on current window position
if self.h - self.curMouseY >= (HISTOGRAM_HEIGHT *
self.GetContentScaleFactor()* 2):
self.dragMode = DRAG_CANVAS
if self.definingROI:
self.dragMode = DRAG_ROI
else:
self.dragMode = DRAG_CANVAS
elif abs(self.curMouseX - blackPointX) < abs(self.curMouseX - whitePointX):
self.dragMode = DRAG_BLACKPOINT
else:
Expand All @@ -722,10 +760,60 @@ def onMouse(self, event):
else:
self.histogram.uthresh = threshold
self.image.vmax = threshold
elif self.dragMode == DRAG_ROI:

if self.shift_down:
#shift is down so force square ROI
# Get co-ordinates in canvas units
coords_x = [self.mouseDragX, self.mouseLdownX]
coords_y = [self.mouseDragY, self.mouseLdownY]
roi_xmin, roi_ymin = min(coords_x), min(coords_y)
roi_xmax, roi_ymax = max(coords_x), max(coords_y)
# Convert to data indices
roi_min_ind = self.canvasToIndices(roi_xmin, roi_ymin)
roi_max_ind = self.canvasToIndices(roi_xmax, roi_ymax)
# Get size of roi
roi_maxsize = max((roi_max_ind[0] - roi_min_ind[0], roi_max_ind[1] - roi_min_ind[1]))
roi_size = (roi_maxsize, roi_maxsize)
# Set roi (left, top, width, height)
self.roi_drag = (roi_min_ind[1], roi_min_ind[0],
roi_size[1], roi_size[0])
else:
#Shift is up so use real coords
roi_start=[self.mouseLdownY,self.mouseLdownX]
roi_end=[self.mouseDragY-self.mouseLdownY,
self.mouseDragX-self.mouseLdownX]
(roi_xmin_ind,
roi_ymin_ind)=self.canvasToIndices(roi_start[0],
roi_start[1])
(roi_xmax_ind,
roi_ymax_ind)=self.canvasToIndices(roi_end[0],
roi_end[1])

self.roi_drag =(roi_xmin_ind,roi_ymin_ind,
roi_xmax_ind,roi_ymax_ind)


self.mouseDragX = self.curMouseX
self.mouseDragY = self.curMouseY
elif event.RightDown():
cockpit.gui.guiUtils.placeMenuAtMouse(self, self._menu)
elif event.LeftUp():
if self.definingROI:
camera = self.Parent.Parent.curCamera

# Set ROI in camera, correcting for current roi
if self.roi:
roi = (self.roi[0] + self.roi_drag[0], self.roi[1] + self.roi_drag[1], self.roi_drag[2], self.roi_drag[3])
else:
roi = self.roi_drag

camera.setROI(roi)
events.publish(events.UPDATE_ROI,camera.name)

self.roi = roi
self.definingROI = False
self.definedROI = True
elif event.Entering() and self.TopLevelParent.IsActive():
self.SetFocus()
else:
Expand All @@ -742,6 +830,9 @@ def getMenuActions(self):
('Set histogram parameters', self.onSetHistogram),
('Toggle clip highlighting', self.image.toggleClipHighlight),
('', None),
('Set ROI', self.onDefineROI),
('Clear ROI', self.onClearROI),
('', None),
('Toggle alignment crosshair', self.toggleCrosshair),
('Toggle sync view', self.toggleSyncViews),
("Toggle FFT mode", self.toggleFFT),
Expand Down Expand Up @@ -774,6 +865,34 @@ def toggleSyncViews(self, event=None):
self.setView,
)

def onDefineROI(self, event = None):
self.roi_drag = None
self.definingROI = True

def onClearROI(self, event = None):
camera = self.Parent.Parent.curCamera
sensor_shape = camera.getSensorShape()
roi = (0,0,sensor_shape[0], sensor_shape[1])
camera.setROI(roi)
self.roi = roi
self.definedROI = False
events.publish(events.UPDATE_ROI,camera.name)
self.Refresh()

def onKeyDown(self, event):
keycode = event.GetKeyCode()
if keycode == wx.WXK_SHIFT:
self.shift_down = True
event.Skip()
self.Refresh()

def onKeyUp(self, event):
keycode = event.GetKeyCode()
if keycode == wx.WXK_SHIFT:
self.shift_down = False
event.Skip()
self.Refresh()

def toggleCrosshair(self, event=None):
self.showCrosshair = not(self.showCrosshair)

Expand All @@ -799,10 +918,22 @@ def canvasToGl(self, x, y):
## Convert gl co-ordinates to indices into the data.
# Note: pass in x,y, but returns row-major datay, datax
def glToIndices(self, glx, gly):
datax = (1 + glx) * self.imageShape[1] // 2
datay = self.imageShape[0]-((1 + gly) * self.imageShape[0] // 2)
return (datay, datax)

# Vertical and horizontal modifiers for non-square images.
hlim = self.imageShape[1] / max(self.imageShape)
vlim = self.imageShape[0] / max(self.imageShape)
datax = int((1 + glx / hlim) * self.imageShape[1] // 2)
datay = int(self.imageShape[0]-((1 + gly / vlim) * self.imageShape[0] // 2))
datax_clamped = max(0, min(datax, self.imageShape[1]))
datay_clamped = max(0, min(datay, self.imageShape[0]))
return (datay_clamped, datax_clamped)

## Convert data indices to gl co-ordinates.
# Note: pass in row-major datay, datax, but returns x,y
def indicesToGl(self, datay, datax):
glx = 2 / self.imageShape[1] * datax - 1
gly = 2 / self.imageShape[0] * (self.imageShape[0] - datay) - 1

return(glx, gly)

## Convert window co-ordinates to indices into the data.
def canvasToIndices(self, x, y):
Expand Down
38 changes: 37 additions & 1 deletion cockpit/gui/mainWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,43 @@ def __init__(self, *args, **kwargs):

self.SetDropTarget(viewFileDropTarget.ViewFileDropTarget(self))


#subscribe to camera UPDATE_ROI events so we can ask to apply it to all
#cameras or not.
self.updateROI = False
events.subscribe(events.UPDATE_ROI, self.updateCamROI)

#function fired on camera ROI update. It looks to see if there are
#other active cameras and asks if you want to set all cameras to that
#ROI or not.
def updateCamROI(self,camName):
#track if already updating cameras, only need to ask once
if self.updateROI:
return
self.updateROI=True
asked = False
update = False
active= depot.getHandlerWithName(camName)
cameras = sorted(depot.getHandlersOfType(depot.CAMERA),
key=lambda c: c.name)
roi=active.getROI()
for camera in cameras:
if camera.isEnabled and (camera.name is not camName):
#another active camera camera so ask.
title = "Apply to other Cameras?"
msg = (
" You have added a new ROI to camera '%s'."
" Do you wish to apply the same ROI to all "
" other cameras?" % (camName)
)
if not asked:
asked = True
if cockpit.gui.guiUtils.getUserPermission(msg, title):
#update ROI on this camera.
update = True
if update:
camera.setROI(roi)
self.updateROI=False

## User clicked the "view last file" button; open the last experiment's
# file in an image viewer. A bit tricky when there's multiple files
# generated due to the splitting logic. We just view the first one in
Expand Down
Loading

0 comments on commit 5931e0b

Please sign in to comment.