Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
410 changes: 203 additions & 207 deletions deapi/client.py

Large diffs are not rendered by default.

188 changes: 187 additions & 1 deletion deapi/data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from enum import Enum
from enum import IntEnum
import warnings
from collections import namedtuple

import numpy as np

Expand Down Expand Up @@ -103,6 +104,29 @@ class PixelFormat(Enum):
FLOAT32 = 13 # 32-bit float
AUTO = -1 # Automatically determined by the server

def to_numpy_dtype(self):
"""Convert the PixelFormat to a numpy dtype"""
if self == PixelFormat.UINT8:
return np.uint8
elif self == PixelFormat.UINT16:
return np.uint16
elif self == PixelFormat.FLOAT32:
return np.float32
else:
return np.uint8

@staticmethod
def from_numpy_dtype(dtype):
"""Convert a numpy dtype to a PixelFormat"""
if dtype == np.uint8:
return PixelFormat.UINT8
elif dtype == np.uint16:
return PixelFormat.UINT16
elif dtype == np.float32:
return PixelFormat.FLOAT32
else:
raise ValueError(f"Unsupported numpy dtype: {dtype}")


class DataType(Enum):
"""An Enum describing the data type of the image data returned by the DE API"""
Expand Down Expand Up @@ -345,6 +369,14 @@ def __init__(
self.output_binning_y = output_binning_y
self.output_binning_method = output_binning_method

def __repr__(self):
return (
f"Attributes: width = {self.frameWidth}, "
f"height = {self.frameHeight},"
f" electrons a sec/pixels = {self.eppixps},"
f" acqFinished = {self.acqFinished}"
)


class Histogram:
"""Class to hold the histogram data from an image acquisition
Expand Down Expand Up @@ -385,7 +417,6 @@ def __repr__(self):
f" max={self.max}, "
f"upperMostLocalMaxima={self.upperMostLocalMaxima},"
f" bins={self.bins},"
f" data={self.data})"
)

def plot(self, ax=None):
Expand Down Expand Up @@ -722,3 +753,158 @@ def calculation(self, value):
"""Set the calculation mode for the virtual mask"""
string = f"Scan - Virtual Detector {self.index} Calculation"
self.client[string] = value


ResultBase = namedtuple(
"ResultBase", ["image", "pixel_format", "attributes", "histogram"]
)


class Result(ResultBase):
"""Class to hold the result of an image acquisition"""

def __repr__(self):
return (
f"Result(image shape={self.image.shape},"
f" pixel_format={self.pixel_format}, "
f" attributes={self.attributes},"
f" histogram={self.histogram})"
)

def plot(self, axs=None, color_histogram=True, colorbar=False, **kwargs):
"""Plot the image using matplotlib

Parameters
----------
ax : matplotlib.axes.Axes, optional
Axes object to plot the image on. If not provided, a new figure will be created.
**kwargs
Additional keyword arguments to pass to ax.imshow

Returns
-------
matplotlib.axes.Axes
Axes object containing the image plot
"""
import matplotlib.pyplot as plt
from matplotlib import gridspec

if axs is None:
fig = plt.figure(figsize=(8, 6))
if colorbar:
gs = gridspec.GridSpec(1, 3, width_ratios=[20, 1.5, 5], wspace=0.05)
ax = fig.add_subplot(gs[0])
cax = fig.add_subplot(gs[1])
hax = fig.add_subplot(gs[2])
else:
gs = gridspec.GridSpec(1, 2, width_ratios=[20, 5], wspace=0.1)
ax = fig.add_subplot(gs[0])
hax = fig.add_subplot(gs[1])
cax = None
else:
ax, cax, hax = axs
fig = ax.figure

vmin = kwargs.get("vmin", np.nanmin(self.image))
vmax = kwargs.get("vmax", np.nanmax(self.image))

im = ax.imshow(
self.image,
vmin=vmin,
vmax=vmax,
**{k: v for k, v in kwargs.items() if k not in ("vmin", "vmax")},
)

if colorbar and cax is not None:
fig.colorbar(im, cax=cax, orientation="vertical")
cax.set_yticks([])
cax.set_xticks([])

# The data can have some non-linear stretch applied. The color bar should reflect that but the
# histogram won't...
if (
hasattr(self, "histogram")
and getattr(self, "histogram") is not None
and getattr(self.histogram, "data", None) is not None
):
hist = np.asarray(self.histogram.data)
bins = self.histogram.bins
bin_edges = np.linspace(self.histogram.min, self.histogram.max, bins + 1)
bin_centers = 0.5 * (bin_edges[:-1] + bin_edges[1:])
else:
bins = 256
hist, bin_edges = np.histogram(
self.image.flatten(), bins=bins, range=(vmin, vmax)
)
bin_centers = 0.5 * (bin_edges[:-1] + bin_edges[1:])
# Plot gamma curve over the histogram
# Normalize the bin centers to [0, 1] using vmin/vmax then apply gamma correction.
if (
self.attributes is None
or self.attributes.stretchType == ContrastStretchType.NONE
):
vmin = self.histogram.min
vmax = self.histogram.max
gamma = 1.0
elif self.attributes.stretchType == ContrastStretchType.MANUAL:
vmin = self.attributes.manualStretchMin
vmax = self.attributes.manualStretchMax
gamma = self.attributes.manualStretchGamma

else:
vmin = self.attributes.autoStretchMin
vmax = self.attributes.autoStretchMax
gamma = self.attributes.autoStretchGamma
denom = vmax - vmin

if denom == 0:
norm = np.clip(bin_centers - vmin, 0.0, 1.0)
else:
norm = np.clip((bin_centers - vmin) / denom, 0.0, 1.0)

# Use a standard gamma transform (display mapping): out = in ** (1/gamma)
gamma_curve = norm ** (1.0 / gamma)

# Scale the gamma curve to the histogram amplitude for overlay
scale = float(np.max(hist)) if np.size(hist) else 1.0
gamma_scaled = gamma_curve * scale * 1.05

hax.plot(gamma_scaled, bin_centers, color="C1", linewidth=2)
# Draw histogram. If color_histogram is True, color bars using the image colormap/norm.
if color_histogram:
# Normalize bin centers to [0,1] using vmin/vmax and apply display gamma, then map to colormap
cmap = im.get_cmap()
# Compute normalized values safely
if denom == 0:
normalized = np.clip(bin_centers - vmin, 0.0, 1.0)
else:
normalized = np.clip((bin_centers - vmin) / denom, 0.0, 1.0)
# Apply display gamma (out = in ** (1/gamma))

mapped = np.power(normalized, gamma)
colors_rgba = cmap(mapped)
# Draw horizontal bars colored by the mapped RGBA values
height = bin_edges[1] - bin_edges[0] if len(bin_edges) > 1 else 1.0
hax.barh(
bin_centers,
hist,
height=height,
color=colors_rgba,
align="center",
edgecolor="none",
)
else:
hax.fill_betweenx(bin_centers, 0, hist, color="0.6")

hax.set_xlim(0, scale * 1.05)
hax.set_ylim(self.histogram.min, self.histogram.max)
hax.invert_xaxis()
hax.yaxis.tick_right()
hax.yaxis.set_label_position("right")
hax.set_xlabel("Frequency")
hax.set_ylabel("Detector Units")

ax.set_yticks([])
ax.set_xticks([])

return ax
45 changes: 31 additions & 14 deletions deapi/simulated_server/fake_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,9 @@ def _fake_get_result(self, command):
histo_min = command.command[0].parameter[14].p_float
histo_max = command.command[0].parameter[15].p_float
histo_bins = command.command[0].parameter[16].p_int
output_binning_x = command.command[0].parameter[17].p_int
output_binning_y = command.command[0].parameter[18].p_int
output_binning_method = command.command[0].parameter[19].p_int

pixel_format_dict = {1: np.int8, 5: np.int16, 13: np.float32}

Expand Down Expand Up @@ -613,7 +616,6 @@ def _fake_get_result(self, command):
image = self.fake_data[self.current_navigation_index].astype(
pixel_format_dict[pixel_format]
)
result = image.tobytes()

elif frame_type == 10 or frame_type == 9: # SUMTOTAL or SUMINTERMEDIATE
if self["Exposure Mode"] == "Gain" or self["Exposure Mode"] == "Trial":
Expand All @@ -637,29 +639,37 @@ def _fake_get_result(self, command):
* 1
).astype(pixel_format_dict[pixel_format])
else:
image = np.sum(self.fake_data.signal, axis=1).astype(
image = np.sum(self.fake_data.signal, axis=0).astype(
pixel_format_dict[pixel_format]
)
result = image.tobytes()
elif 11 < frame_type < 17: # virtual image
image = self.virtual_masks[frame_type - 12]
print(image)
if image.shape != (windowWidth, windowHeight):
image = resize(
image, (windowWidth, windowHeight), preserve_range=True
).astype(np.int8)
result = image.tobytes()
elif 17 <= frame_type < 22:
image = self.virtual_masks[frame_type - 17]
calculation_type = self[
f"Scan - Virtual Detector {frame_type-17} Calculation"
]
image = self.fake_data.get_virtual_image(image, method=calculation_type)
image = image.astype(pixel_format_dict[pixel_format])
result = image.tobytes()

else:
raise ValueError(f"Frame type {frame_type} not Supported in PythonDEServer")

if windowWidth == 0:
windowWidth = image.shape[0]
if windowHeight == 0:
windowHeight = image.shape[1]
if image.shape != (windowWidth, windowHeight):
image = resize(
image, (windowWidth, windowHeight), preserve_range=True
).astype(pixel_format_dict[pixel_format])

result = image.tobytes()

# map to right order...
mean_img = np.mean(image)
eppix = mean_img / 208
Expand Down Expand Up @@ -692,18 +702,25 @@ def _fake_get_result(self, command):
0.0, # orange sat warning 23
0.0, # saturation 24
"2.187026", # current time 25
0.0, # autoStretchMin 26
0.0, # autoStretchMax 27
0.0, # autoStretchGamma 28
0.0, # histogram min 29
float(np.min(image)), # histogram max 30
float(np.min(image)), # autoStretchMin 26
float(np.max(image)), # autoStretchMax 27
float(1.0), # autoStretchGamma 28
float(np.min(image)), # histogram min 29
float(np.max(image)), # histogram max 30
float(np.max(image)), # histogram upper local max 31
]
for i in range(histo_bins):
response_mapping.append(int(0))

# Then histogram...
if histo_min == 0 and histo_max == 0:
histo_min = np.min(image)
histo_max = np.max(image)
image_hist, bins = np.histogram(
image.flatten(), bins=histo_bins, range=(histo_min, histo_max)
)
for i in image_hist:
response_mapping.append(int(i))

for val in response_mapping:
ack1 = acknowledge_return.acknowledge.add()
add_parameter(ack1, val)
ans = (acknowledge_return,)
# add the data header packet for how many bytes are in the data
Expand Down
13 changes: 3 additions & 10 deletions deapi/tests/original_tests/test_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,7 @@ def test_set_mask(self, client):
property_name = f"Scan - Virtual Detector {maskID} Shape"
deClient.SetProperty(property_name, "Arbitrary")

if not deClient.SetVirtualMask(maskID, 1024, 1024, mask):
return False

# Define attributes and frame type
attributes = DEAPI.Attributes()
frameType = getattr(DEAPI.FrameType, f"VIRTUAL_MASK{maskID}")

deClient.SetVirtualMask(maskID, 1024, 1024, mask)
# Generate and check the first image
Image, _, _, _ = deClient.GetResult(
frameType, DEAPI.PixelFormat.AUTO, attributes
)
mask = deClient.virtual_masks[1][:]
assert mask.shape == (1024, 1024)
1 change: 1 addition & 0 deletions deapi/tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def load_module():
"live_imaging/viewing_the_sensor.py",
"live_imaging/viewing_the_sensor_tem.py",
"live_imaging/bright_spot_intensity.py",
"visualization/using_get_result.py",
],
)
def test_examples(server, file):
Expand Down
2 changes: 2 additions & 0 deletions deapi/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@


def write_only(func):
@wraps(func)
def wrapper(*args, **kwargs):
if args[0].read_only:
log.error("Client is read-only. Cannot set property.")
Expand All @@ -19,6 +20,7 @@ def wrapper(*args, **kwargs):


def disable_scan(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Disabling scan")
initial_scan = args[0]["Scan - Enable"]
Expand Down
4 changes: 4 additions & 0 deletions examples/visualization/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Visualization
=============
One key thing many people are interested in is how to visualize their data. Below are some examples/ explanations of
how to visualize your data.
11 changes: 11 additions & 0 deletions examples/visualization/creating_a_simple_dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""
Creating a Simple Dashboard with Panel and Matplotlib
------------------------------------------------------

If you haven't already looked at the "The `get_result` Function" example, please do so as it
provides important context for how the `get_result` function can be used to build responsive
visualization tools by offloading the heavy lifting for visualization to the server side.

In this example, we will build a simple dashboard using the `panel` library to create a simple GUI
for defining a 4D STEM acquisition and visualizing the results in real-time.
"""
Loading
Loading