Skip to content

Commit

Permalink
Merge pull request #1693 from bnmajor/masking-fixes
Browse files Browse the repository at this point in the history
Masking fixes
  • Loading branch information
saransh13 authored Apr 26, 2024
2 parents 1319dde + fa69960 commit a901601
Show file tree
Hide file tree
Showing 10 changed files with 108 additions and 53 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
}
- {
name: "MacOSX",
os: macos-latest,
os: macos-12,
package: 'dmg'
}
- {
Expand All @@ -41,7 +41,7 @@ jobs:
path: hexrdgui

- name: Install conda
uses: conda-incubator/setup-miniconda@v2
uses: conda-incubator/setup-miniconda@v3
with:
auto-update-conda: true
python-version: '3.11'
Expand Down
14 changes: 6 additions & 8 deletions hexrdgui/calibration/polarview.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,14 +401,13 @@ def apply_image_processing(self):
# masks should also be distorted as well.

# We only apply "visible" masks to the display image
img = self.apply_visible_masks(img)
disp_img = self.apply_tth_distortion(img)
img = self.apply_tth_distortion(img)
disp_img = self.apply_visible_masks(img)
self.display_image = disp_img

# Both "visible" and "boundary" masks are applied to the
# computational image
comp_img = self.apply_boundary_masks(img)
comp_img = self.apply_tth_distortion(comp_img)
comp_img = self.apply_boundary_masks(disp_img)
self.computation_img = comp_img

def apply_snip(self, img):
Expand Down Expand Up @@ -484,14 +483,13 @@ def reapply_masks(self):
# masks should also be distorted as well.

# We only apply "visible" masks to the display image
img = self.apply_visible_masks(self.snipped_img)
disp_img = self.apply_tth_distortion(img)
img = self.apply_tth_distortion(self.snipped_img)
disp_img = self.apply_visible_masks(img)
self.display_image = disp_img

# Both "visible" and "boundary" masks are applied to the
# computational image
comp_img = self.apply_boundary_masks(img)
comp_img = self.apply_tth_distortion(comp_img)
comp_img = self.apply_boundary_masks(disp_img)
self.computation_img = comp_img

@property
Expand Down
12 changes: 12 additions & 0 deletions hexrdgui/image_canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -1645,6 +1645,18 @@ def draw_mask_boundaries(self, axis, det=None):
verts = [v for k, v in mask.data if k == det]
elif self.mode == ViewType.polar or self.mode == ViewType.stereo:
verts = create_polar_line_data_from_raw(instr, mask.data)
if self.mode == ViewType.polar:
# Check for any major jumps in eta. That probably means
# the border is wrapping around.
for i, vert in enumerate(verts):
# If there are two points far away, assume there
# is a gap in the eta range.
eta_diff = np.abs(np.diff(vert[:, 1]))
delta_eta_est = np.nanmedian(eta_diff)
tolerance = delta_eta_est * 10
big_gaps, = np.nonzero(eta_diff > tolerance)
verts[i] = np.insert(vert, big_gaps + 1, np.nan, axis=0)

if self.mode == ViewType.stereo:
# Now convert from polar to stereo
for i, vert in enumerate(verts):
Expand Down
7 changes: 2 additions & 5 deletions hexrdgui/interactive_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@
from matplotlib.path import Path
from matplotlib.transforms import Affine2D

from skimage.draw import polygon

from hexrdgui.constants import (
KEY_ROTATE_ANGLE_FINE, KEY_TRANSLATE_DELTA, ViewType
)
from hexrdgui.hexrd_config import HexrdConfig
from hexrdgui.utils import has_nan
from hexrdgui.utils.polygon import polygon_to_mask


class InteractiveTemplate:
Expand Down Expand Up @@ -223,9 +222,7 @@ def mask(self):
for c, r in zip(cols, rows):
c = c[~np.isnan(c)]
r = r[~np.isnan(r)]
rr, cc = polygon(r, c, shape=self.img.shape)
mask = np.zeros(self.img.shape, dtype=bool)
mask[rr, cc] = True
mask = ~polygon_to_mask(np.vstack([c, r]).T, self.img.shape)
master_mask = np.logical_xor(master_mask, mask)
self.img[~master_mask] = 0
return master_mask
Expand Down
40 changes: 22 additions & 18 deletions hexrdgui/masking/create_polar_mask.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
import numpy as np

from skimage.draw import polygon
from hexrdgui.constants import ViewType

from hexrdgui.create_hedm_instrument import create_view_hedm_instrument
from hexrdgui.calibration.polarview import PolarView
from hexrdgui.hexrd_config import HexrdConfig
from hexrdgui.masking.constants import MaskType
from hexrdgui.utils import add_sample_points
from hexrdgui.utils.conversions import pixels_to_angles
from hexrdgui.utils.conversions import cart_to_angles, pixels_to_cart
from hexrdgui.utils.polygon import polygon_to_mask
from hexrdgui.utils.tth_distortion import apply_tth_distortion_if_needed


def convert_raw_to_polar(instr, det, line):
# This accepts an instrument rather than creating one for performance

# Make sure there at least 300 sample points so that the conversion
# Make sure there at least 500 sample points so that the conversion
# looks correct.
line = add_sample_points(line, 300)
# This needs to be greater than the number of sample points when going
# from polar to raw, so we can ensure that points along borders get added.
line = add_sample_points(line, 500)

panel = instr.detectors[det]
cart = pixels_to_cart(line, panel)
xys, _ = panel.clip_to_panel(cart, buffer_edges=False)

kwargs = {
'ij': line,
'panel': instr.detectors[det],
'eta_period': HexrdConfig().polar_res_eta_period,
'tvec_s': instr.tvec,
'tvec_s': instr.tvec
}

return [pixels_to_angles(**kwargs)]
line = cart_to_angles(xys, panel, **kwargs)
line = apply_tth_distortion_if_needed(line, in_degrees=True)
return [line] if line.size else None


def create_polar_mask(line_data):
Expand Down Expand Up @@ -89,11 +92,11 @@ def create_polar_mask(line_data):


def _pixel_perimeter_to_mask(r, c, shape):
# The arguments are all forwarded to skimage.draw.polygon
rr, cc = polygon(r, c, shape=shape)
mask = np.ones(shape, dtype=bool)
mask[rr, cc] = False
return mask
polygon = np.vstack([c, r]).T
if polygon.size < 2:
return np.ones(shape, dtype=bool)

return polygon_to_mask(polygon, shape)


def _split_coords_1d(x, gap1, gap2):
Expand Down Expand Up @@ -145,7 +148,8 @@ def create_polar_line_data_from_raw(instr, value):
# This accepts an instrument rather than creating one for performance
line_data = []
for det, data in value:
line_data.extend(convert_raw_to_polar(instr, det, data))
if polar := convert_raw_to_polar(instr, det, data):
line_data.extend(polar)
return line_data


Expand Down
49 changes: 35 additions & 14 deletions hexrdgui/masking/create_raw_mask.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import numpy as np

from skimage.draw import polygon
from hexrdgui.constants import ViewType
from skimage import measure

from hexrdgui.create_hedm_instrument import create_hedm_instrument
from hexrdgui.hexrd_config import HexrdConfig
from hexrdgui.masking.constants import MaskType
from hexrdgui.utils import add_sample_points
from hexrdgui.utils import (
add_sample_points,
remove_duplicate_neighbors,
)
from hexrdgui.utils.conversions import angles_to_pixels
from hexrdgui.utils.polygon import polygon_to_mask
from hexrdgui.utils.tth_distortion import apply_tth_distortion_if_needed


Expand Down Expand Up @@ -56,13 +58,35 @@ def convert_polar_to_raw(line_data, reverse_tth_distortion=True):
for line in line_data:
for key, panel in instr.detectors.items():
raw = angles_to_pixels(line, panel, tvec_s=instr.tvec)
if all([np.isnan(x) for x in raw.flatten()]):
continue

# Go ahead and get rid of nan coordinates. They cause trouble
# with scikit image's polygon.
# Remove nans
raw = raw[~np.isnan(raw.min(axis=1))]
raw_line_data.append((key, raw))
if raw.size == 0:
continue

# Remove duplicate neighbors
raw = remove_duplicate_neighbors(raw)

# Keep raw points off the detector, to ensure we
# can draw the polygon correctly.
# Then, find contours along the polygon.
# We will create a higher resolution shape so that we
# can keep resolution from the mask coordinates
res = 2
mask_shape = np.array(panel.shape) * res

mask = ~polygon_to_mask(raw * res, mask_shape)
if not mask.any():
# The mask did not affect this panel.
continue

# Add borders so that border coordinates are kept.
contours = measure.find_contours(np.pad(mask, 1))
for contour in contours:
# Add 0.5 so all coordinates will be positive before rescaling,
# then remove that 0.5 again afterward.
contour = ((contour[:, [1, 0]] - 1) + 0.5) / res - 0.5
raw_line_data.append((key, contour))

return raw_line_data

Expand All @@ -74,11 +98,8 @@ def create_raw_mask(line_data):
img = HexrdConfig().image(det, 0)
final_mask = np.ones(img.shape, dtype=bool)
for _, data in det_lines:
rr, cc = polygon(data[:, 1], data[:, 0], shape=img.shape)
if len(rr) >= 1:
mask = np.ones(img.shape, dtype=bool)
mask[rr, cc] = False
final_mask = np.logical_and(final_mask, mask)
mask = polygon_to_mask(data, img.shape)
final_mask = np.logical_and(final_mask, mask)
masks.append((det, final_mask))
return masks

Expand Down
10 changes: 6 additions & 4 deletions hexrdgui/masking/mask_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,19 +351,21 @@ def update_name(self, old_name, new_name):
mask.name = new_name
self.masks[new_name] = mask

def masks_to_panel_buffer(self, selection, buff_val):
def masks_to_panel_buffer(self, selection):
# Set the visible masks as the panel buffer(s)
# We must ensure that we are using raw masks
for det, mask in HexrdConfig().raw_masks_dict.items():
detector_config = HexrdConfig().detector(det)
buffer_default = {'status': 0}
buffer = detector_config.setdefault('buffer', buffer_default)
buffer_value = detector_config['buffer'].get('value', None)
if isinstance(buffer_value, np.ndarray) and buff_val.ndim == 2:
if isinstance(buffer_value, np.ndarray) and buffer_value.ndim == 2:
if selection == 'Logical AND with buffer':
mask = np.logical_and(mask, buffer_value)
# Need to invert so True is invalid
mask = ~np.logical_and(~mask, ~buffer_value)
elif selection == 'Logical OR with buffer':
mask = np.logical_or(mask, buffer_value)
# Need to invert so True is invalid
mask = ~np.logical_or(~mask, ~buffer_value)
buffer['value'] = mask

def clear_all(self):
Expand Down
2 changes: 1 addition & 1 deletion hexrdgui/masking/mask_manager_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ def masks_to_panel_buffer(self):

selection = options.currentText()

MaskManager().masks_to_panel_buffer(selection, buff_val)
MaskManager().masks_to_panel_buffer(selection)
msg = 'Masks set as panel buffers.'
QMessageBox.information(self.parent, 'HEXRD', msg)

Expand Down
11 changes: 10 additions & 1 deletion hexrdgui/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,14 @@ def set_combobox_enabled_items(cb, enable_list):
cb.setCurrentIndex(new_index)


def remove_duplicate_neighbors(points):
# Remove any points from this 2D array that are duplicates with
# their next neighbor.
rolled = np.roll(points, -1, axis=0)
delete_indices = np.all(np.isclose(rolled - points, 0), axis=1)
return np.delete(points, delete_indices, axis=0)


def add_sample_points(points, min_output_length):
"""Add extra sample points to a 2D array of points
Expand All @@ -480,7 +488,8 @@ def add_sample_points(points, min_output_length):
rolled = np.roll(points, -1, axis=0)

# Generate the extra points between each point and its neighbor
output = np.linspace(points, rolled, num=num_reps)
# Adding endpoint=False significantly reduces our number of duplicates.
output = np.linspace(points, rolled, num=num_reps, endpoint=False)

# Transform back into the correct shape and return
return output.T.reshape(2, -1).T
Expand Down
12 changes: 12 additions & 0 deletions hexrdgui/utils/polygon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from PIL import Image, ImageDraw
import numpy as np


def polygon_to_mask(coords, mask_shape):
# This comes out very slightly different than scikit-image's polygon
# method, with the only differences being along the edges (plus or minus
# a pixel).
img = Image.fromarray(np.ones(mask_shape, dtype=bool))
ImageDraw.Draw(img).polygon(coords.flatten().tolist(), outline=0, fill=0)

return np.array(img)

0 comments on commit a901601

Please sign in to comment.