Skip to content

Commit

Permalink
draw the preview in RGB, paving way for colorized feedback
Browse files Browse the repository at this point in the history
also fix bug in scaling of shift-click overview function
  • Loading branch information
bunnie committed Feb 2, 2024
1 parent 4d1061e commit d7fc022
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 60 deletions.
115 changes: 66 additions & 49 deletions overview.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from math import ceil

from PyQt5.QtGui import QPixmap, QImage
from PyQt5.QtWidgets import QRadioButton

import cv2
import platform
Expand Down Expand Up @@ -76,9 +77,9 @@ def redraw_overview(self, blend=True, force_full_res=False):

if force_full_res:
self.overview_fullres = canvas
self.overview = cv2.resize(canvas, None, None, fx=THUMB_SCALE, fy=THUMB_SCALE)
self.overview = cv2.cvtColor(cv2.resize(canvas, None, None, fx=THUMB_SCALE, fy=THUMB_SCALE), cv2.COLOR_GRAY2RGB)
else:
self.overview = canvas
self.overview = cv2.cvtColor(canvas, cv2.COLOR_GRAY2RGB)
self.rescale_overview()
if self.show_selection:
self.preview_selection()
Expand All @@ -87,18 +88,37 @@ def redraw_overview(self, blend=True, force_full_res=False):
def rescale_overview(self):
w = self.lbl_overview.width()
h = self.lbl_overview.height()
(y_res, x_res) = self.overview.shape
(y_res, x_res, _planes) = self.overview.shape
# constrain by height and aspect ratio
scaled = cv2.resize(self.overview, (int(x_res * (h / y_res)), h))
height, width = scaled.shape
bytesPerLine = 1 * width
height, width, planes = scaled.shape
bytesPerLine = planes * width
self.lbl_overview.setPixmap(QPixmap.fromImage(
QImage(scaled.data, width, height, bytesPerLine, QImage.Format.Format_Grayscale8)
QImage(scaled.data, width, height, bytesPerLine, QImage.Format.Format_RGB888)
))
self.overview_actual_size = (width, height)
self.overview_scaled = scaled.copy()

def update_selected_rect(self, update_tile=False):
# Extract the list of intersecting tiles and update the UI
closet_tiles = self.schema.get_intersecting_tiles((self.roi_center_ums[0] / 1000, self.roi_center_ums[1] / 1000))
# clear all widgets from the vbox layout
while self.status_layer_select_layout.count():
child = self.status_layer_select_layout.takeAt(0)
if child.widget():
child.widget().deleteLater()
first = True
for (layer, t) in closet_tiles:
md = Schema.meta_from_fname(t['file_name'])
t_center = Point(float(md['x'] + t['offset'][0] / 1000), float(md['y'] + t['offset'][1] / 1000))
b = QRadioButton(str(layer) + f': {t_center[0]:0.3f},{t_center[1]:0.3f}')
if first:
b.setChecked(True)
first = False
self.status_layer_select_layout.addWidget(b)
# TODO: add routines to connect radio button actions to something that acts on it.

# Draw the UI assuming the closest is the selected.
(layer, tile) = self.schema.get_tile_by_coordinate(self.selected_image_centroid)
selected_image = self.schema.get_image_from_layer(layer, thumb=True)
metadata = Schema.meta_from_tile(tile)
Expand All @@ -112,45 +132,35 @@ def update_selected_rect(self, update_tile=False):
)
ui_overlay = self.overview.copy()

# define the rectangle
w = selected_image.shape[1]
h = selected_image.shape[0]
# x/y coords to safe_image_broadcast are unscaled
w = selected_image.shape[1] / THUMB_SCALE
h = selected_image.shape[0] / THUMB_SCALE
tl_x = int(x_c - w/2)
tl_y = int(y_c - h/2)
tl = (tl_x, tl_y)
br = (tl_x + int(w), tl_y + int(h))

# overlay the tile
if update_tile:
safe_image_broadcast(selected_image, ui_overlay, tl[0], tl[1])

# draw the rectangle
h_target = self.lbl_overview.height()
(y_res, x_res) = self.overview.shape
thickness = y_res / h_target # get a 1-pix line after rescaling
cv2.rectangle(
ui_overlay,
tl,
br,
(255, 255, 255),
thickness = ceil(thickness),
lineType = cv2.LINE_4
)
safe_image_broadcast(selected_image, ui_overlay, tl_x, tl_y)

# use the same height-driven rescale as in `rescale_overview()`
# constrain by height and aspect ratio
(y_res, x_res, _planes) = self.overview.shape
h_target = self.lbl_overview.height()
scaled = cv2.resize(ui_overlay, (int(x_res * (h_target / y_res)), h_target))

# overlay the selection preview
# draw the immediate selection
thickness = ceil((y_res / h_target) * THUMB_SCALE) # get a 1-pix line after rescaling
self.draw_rect_at_center((x_c, y_c), scaled, thickness = thickness, color = (255, 192, 255))

# overlay the group selection preview
if self.show_selection:
ui_overlay = self.compute_selection_overlay()
scaled = cv2.addWeighted(scaled, 1.0, ui_overlay, 0.5, 0.0)

# blit to viewing portal
height, width = scaled.shape
bytesPerLine = 1 * width
height, width, planes = scaled.shape
bytesPerLine = planes * width
self.lbl_overview.setPixmap(QPixmap.fromImage(
QImage(scaled.data, width, height, bytesPerLine, QImage.Format.Format_Grayscale8)
QImage(scaled.data, width, height, bytesPerLine, QImage.Format.Format_RGB888)
))

# update the status bar output
Expand Down Expand Up @@ -193,11 +203,31 @@ def get_coords_in_range(self):
coords_in_range += [coords]
return coords_in_range

def rect_at_center(self, c):
(x_c, y_c) = c
w = (self.overview_actual_size[0] / self.schema.max_res[0]) * X_RES
h = (self.overview_actual_size[1] / self.schema.max_res[1]) * Y_RES
# define the rectangle
x_c = (self.overview_actual_size[0] / self.schema.max_res[0]) * x_c
y_c = (self.overview_actual_size[1] / self.schema.max_res[1]) * y_c
tl_x = int(x_c - w/2)
tl_y = int(y_c - h/2)
return Rect(Point(tl_x, tl_y), Point(tl_x + int(w), tl_y + int(h)))

def draw_rect_at_center(self, c, img, thickness = 1, color = (128, 128, 128)):
r = self.rect_at_center(c)
cv2.rectangle(
img,
r.tl_int_tup(),
r.br_int_tup(),
color,
thickness = thickness,
lineType = cv2.LINE_4
)

def compute_selection_overlay(self):
if self.selected_image_centroid is None: # edge case of startup, nothing has been clicked yet
return
w = (self.overview_actual_size[0] / self.schema.max_res[0]) * X_RES
h = (self.overview_actual_size[1] / self.schema.max_res[1]) * Y_RES
ui_overlay = np.zeros(self.overview_scaled.shape, self.overview_scaled.dtype)
coords_in_range = self.get_coords_in_range()
for coord in coords_in_range:
Expand All @@ -207,21 +237,7 @@ def compute_selection_overlay(self):
(float(metadata['x']) * 1000 + float(tile['offset'][0]),
float(metadata['y']) * 1000 + float(tile['offset'][1]))
)
# define the rectangle
x_c = (self.overview_actual_size[0] / self.schema.max_res[0]) * x_c
y_c = (self.overview_actual_size[1] / self.schema.max_res[1]) * y_c
tl_x = int(x_c - w/2)
tl_y = int(y_c - h/2)
tl = (tl_x, tl_y)
br = (tl_x + int(w), tl_y + int(h))
cv2.rectangle(
ui_overlay,
tl,
br,
(128, 128, 128),
thickness = 1,
lineType = cv2.LINE_4
)
self.draw_rect_at_center((x_c, y_c), ui_overlay)
return ui_overlay

def preview_selection(self):
Expand All @@ -230,9 +246,10 @@ def preview_selection(self):
ui_overlay = self.compute_selection_overlay()
composite = cv2.addWeighted(self.overview_scaled, 1.0, ui_overlay, 0.5, 0.0)

height, width, planes = self.overview_scaled.shape
bytesPerLine = planes * width
self.lbl_overview.setPixmap(QPixmap.fromImage(
QImage(composite.data, self.overview_scaled.shape[1], self.overview_scaled.shape[0], self.overview_scaled.shape[1],
QImage.Format.Format_Grayscale8)
QImage(composite.data, width, height, bytesPerLine, QImage.Format.Format_RGB888)
))

# ASSUME: tile is X_RES, Y_RES in resolution
Expand Down
5 changes: 5 additions & 0 deletions prims.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ def __normalize__(self):
def __eq__(self, r):
return r.tl == self.tl and r.br == self.br

def tl_int_tup(self):
return (int(self.tl[0]), int(self.tl[1]))
def br_int_tup(self):
return (int(self.br[0]), int(self.br[1]))

def intersects(self, p: Point):
return round(self.tl.x, ROUNDING) <= round(p.x, ROUNDING) \
and round(self.tl.y, ROUNDING) <= round(p.y, ROUNDING) \
Expand Down
20 changes: 15 additions & 5 deletions schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,21 +509,31 @@ def rect_mm_from_center(coord: Point):
)

# This routine returns a sorted dictionary of intersecting tiles, keyed by layer draw order,
# that intersect with `coord`. This routine does *not* adjust the intersection computation
# by the `offset` field, so that you don't "lose" a tile as you move it over the border
# of tiling zones.
# that intersect with `coord`.
def get_intersecting_tiles(self, coord_mm):
center = Point(coord_mm[0], coord_mm[1])
rect = Schema.rect_mm_from_center(center)
result = {}
for (layer, t) in self.schema['tiles'].items():
md = self.meta_from_fname(t['file_name'])
t_center = Point(float(md['x']), float(md['y']))
t_center = Point(float(md['x'] + t['offset'][0] / 1000), float(md['y'] + t['offset'][1] / 1000))
t_rect = Schema.rect_mm_from_center(t_center)
if rect.intersection(t_rect) is not None:
result[layer] = t

return sorted(result.items())
offset_coords_mm = []
for (layer, t) in result.items():
metadata = Schema.meta_from_fname(t['file_name'])
offset_coords_mm += [(
metadata['x'] + t['offset'][0] / 1000,
metadata['y'] + t['offset'][1] / 1000,
(layer, t)
)]
s = sorted(offset_coords_mm, key= lambda s: math.sqrt((s[0] - center[0])**2 + (s[1] - center[1])**2))
retlist = []
for (_x, _y, (layer, t)) in s:
retlist += [(layer, t)]
return retlist

def center_coord_from_tile(self, tile):
md = self.meta_from_tile(tile)
Expand Down
10 changes: 7 additions & 3 deletions stitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class MainWindow(QMainWindow):
from overview import redraw_overview, rescale_overview, update_selected_rect,\
centroid_to_tile_bounding_rect_mm, snap_range, check_res_bounds,\
pix_to_um_absolute, um_to_pix_absolute, preview_selection, get_coords_in_range,\
compute_selection_overlay
compute_selection_overlay, draw_rect_at_center, rect_at_center

def __init__(self):
super().__init__()
Expand All @@ -99,6 +99,8 @@ def __init__(self):
self.status_bar.setMinimumWidth(150)
self.status_bar.setMaximumWidth(350)

status_overall_layout = QVBoxLayout()

status_fields_layout = QFormLayout()
self.status_centroid_ui = QLabel("0, 0")
self.status_layer_ui = QLabel("0")
Expand Down Expand Up @@ -136,9 +138,11 @@ def __init__(self):
status_fields_layout.addRow("Filter:", self.status_filter_ui)
status_fields_layout.addRow("Select Pt 1:", self.status_select_pt1_ui)
status_fields_layout.addRow("Select Pt 2:", self.status_select_pt2_ui)

status_overall_layout = QVBoxLayout()
status_overall_layout.addLayout(status_fields_layout)

self.status_layer_select_layout = QVBoxLayout()
status_overall_layout.addLayout(self.status_layer_select_layout)

self.status_restitch_selection_button = QPushButton("Restitch Selection")
self.status_restitch_selection_button.clicked.connect(self.restitch_selection)
self.status_flag_manual_review_button = QPushButton("Flag for Manual Review")
Expand Down
8 changes: 5 additions & 3 deletions utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ def pad_images_to_same_size(images):

return images_padded

# `img`: image to copy onto canvas
# `canvas`: destination for image copy
# `x`, `y`: top left corner coordinates of canvas destination (may be unsafe values), given in unscaled canvas units
# `img`: image to copy onto canvas - this is SCALED to `scale` (could be thumbnail or full-res)
# `canvas`: destination for image copy (could be overview or full-res)
# `x`, `y`: top left corner coordinates of canvas destination (may be unsafe values), given in UNSCALED canvas units
# (e.g., does not consider thumbnailing optimizations in coordinate transforms; they are applied inside this function)
# `mask`: optional mask, must have dimensions identical to `canvas`. Used
# to track what regions of the canvas has valid data for averaging. A non-zero value means
Expand All @@ -117,6 +117,8 @@ def pad_images_to_same_size(images):
# the image copy will start at an offset that would correctly map the `img` pixels into the
# available canvas area
def safe_image_broadcast(img, canvas, x, y, result_mask=None, scale=THUMB_SCALE):
if len(canvas.shape) == 3 and len(img.shape) != 3:
img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
SCALE = 0.05
w = img.shape[1]
h = img.shape[0]
Expand Down

0 comments on commit d7fc022

Please sign in to comment.