Source code for AFL.automation.instrument.OpticalTurbidity

import copy
import datetime
import time
import warnings

import lazy_loader as lazy
import numpy as np
import xarray as xr

from AFL.automation.APIServer.Driver import Driver
from AFL.automation.shared.samplecells import NeutronSampleCell

try:
    from tiled.queries import Eq
except ImportError:
    Eq = None
    warnings.warn("Cannot import from tiled...empty UUID lookup will not work", stacklevel=2)


[docs] class OpticalTurbidity(NeutronSampleCell, Driver): defaults = { **NeutronSampleCell.geometry_defaults, "save_path": "/home/afl642/2305_SINQ_TurbidityImages/", "camera_interface": "http", "camera_url": "http://afl-video:8081/103/current", "camera_index": 0, "empty_uuid": "", }
[docs] def __init__(self, camera=None, overrides=None): """ Initialize OpticalTurbidity calculator driver. Parameters ---------- camera : object, optional Camera object (for example, a NetworkCamera instance). If None, one will be created when using the HTTP interface. overrides : dict, optional Configuration overrides for PersistentConfig. """ self.camera = camera self.empty_img = None self._opencv_capture = None Driver.__init__( self, name="OpticalTurbidity", defaults=self.gather_defaults(), overrides=overrides, ) if self.camera is None and self.config["camera_interface"] == "http": from AFL.automation.instrument.NetworkCamera import NetworkCamera self.camera = NetworkCamera(self.config["camera_url"])
def _collect_image(self, **kwargs): """ Collect an image based on the configured camera interface. Returns ------- tuple `(collected, img)` where `collected` indicates success. """ interface = self.config["camera_interface"] if interface == "http": if self.camera is None: from AFL.automation.instrument.NetworkCamera import NetworkCamera if "camera_url" not in self.config: raise ValueError("camera_url must be set in config when camera_interface='http'") self.camera = NetworkCamera(self.config["camera_url"]) return self.camera.collect(**kwargs) if interface == "opencv": try: cv2_module = lazy.load("cv2", require="AFL-automation[vision]") except Exception as exc: raise ImportError( "opencv-python is required for camera_interface='opencv'. " f"Install with: pip install AFL-automation[vision]. Error: {exc}" ) if "camera_index" not in self.config: raise ValueError("camera_index must be set in config when camera_interface='opencv'") camera_index = self.config["camera_index"] if self._opencv_capture is None: self._opencv_capture = cv2_module.VideoCapture(camera_index) return self._opencv_capture.read() raise ValueError( f"Unsupported camera_interface: '{interface}'. " "Supported values are: 'http', 'opencv'" ) def _reset_camera(self): """Reset the configured camera connection.""" interface = self.config["camera_interface"] if interface == "http": if self.camera is not None: self.camera.camera_reset() return if interface == "opencv": if self._opencv_capture is not None: self._opencv_capture.release() try: cv2_module = lazy.load("cv2", require="AFL-automation[vision]") except Exception as exc: raise ImportError( "opencv-python is required for camera_interface='opencv'. " f"Install with: pip install AFL-automation[vision]. Error: {exc}" ) camera_index = self.config.get("camera_index", 0) self._opencv_capture = cv2_module.VideoCapture(camera_index) def _build_dataset( self, *, name, turbidity_metric, measurement_img, empty_img, mask, cx, cy, empty_from_measurement=False, is_empty_reference=False, ): ds = xr.Dataset() ds.attrs["located_center"] = [cx, cy] ds.attrs["name"] = name ds.attrs["turbidity_metric"] = turbidity_metric ds.attrs["empty_uuid"] = self.config.get("empty_uuid", "") ds.attrs["empty_available"] = not empty_from_measurement ds.attrs["is_empty_reference"] = is_empty_reference ds["turbidity"] = turbidity_metric ds["img"] = (("px", "py"), measurement_img) ds["img_MT"] = (("px", "py"), empty_img) ds["mask"] = (("px", "py"), mask) return ds
[docs] def measure(self, set_empty=False, plotting=False, **kwargs): """ This is an optical turbidity measurement observing the SANS cell. Parameters ---------- set_empty : bool, optional If True, set the current image as the empty reference image. plotting : bool, optional If True, save diagnostic plots. **kwargs : dict Additional arguments passed to image collection. Returns ------- xarray.Dataset Dataset with turbidity measurements and images. When `set_empty=True`, the dataset contains the captured empty image. """ name = kwargs.pop("name", "") print("attempting to collect camera image") self._reset_camera() time.sleep(0.2) collected, img = self._collect_image(**kwargs) print(collected, img) if collected: print("collected image") measurement_img = self.to_grayscale(img, color_order="RGB") else: self._reset_camera() print("trying to reset camera connection") collected, img = self._collect_image(**kwargs) if collected: measurement_img = self.to_grayscale(img, color_order="RGB") print("success on retry") else: raise RuntimeError( "Failed to collect camera image after two attempts. " "Check that the camera is connected and the " f"camera_interface ('{self.config['camera_interface']}') " "settings are correct." ) if set_empty: self.empty_img = measurement_img print("setting empty image", measurement_img) if self.data is not None and "sample_uuid" in self.data: self.config["empty_uuid"] = copy.deepcopy(self.data["sample_uuid"]) return self._build_dataset( name=name, turbidity_metric=1.0, measurement_img=measurement_img, empty_img=measurement_img, mask=np.ones_like(measurement_img, dtype=bool), cx=0, cy=0, empty_from_measurement=False, is_empty_reference=True, ) print("measuring turbidity") empty_img = None empty_from_measurement = False if self.empty_img is not None: empty_img = self.empty_img elif self.config.get("empty_uuid"): if Eq is None: self.log_warning("Cannot load empty image without tiled. Using measurement image for mask.") elif self.data is None or not hasattr(self.data, "tiled_client") or self.data.tiled_client is None: self.log_warning("No tiled client available. Using measurement image for mask.") else: tiled_result = self.data.tiled_client.search(Eq("sample_uuid", self.config["empty_uuid"])) result_items = list(tiled_result.items()) if not result_items: self.log_warning( f"No tiled entry found for empty_uuid={self.config['empty_uuid']}. " "Using measurement image for mask." ) else: item = result_items[-1][-1] try: ds = item.read(optimize_wide_table=False) except TypeError: ds = item.read() if "img_MT" in ds: empty_img = ds["img_MT"].values elif "img" in ds: empty_img = ds["img"].values if empty_img is None: self.log_warning("No empty image available. Using measurement image for mask and normalization.") empty_img = measurement_img empty_from_measurement = True reference_sample = self.extract_sample_image(empty_img, color_order="RGB") measurement_img = self.crop_image( measurement_img, row_crop=reference_sample["row_crop"], col_crop=reference_sample["col_crop"], ) empty_img = reference_sample["cropped_img"] mask = reference_sample["mask"] cx = reference_sample["cx"] cy = reference_sample["cy"] turbidity_metric = self.turbidity_measurement( measurement_img, empty_img, mask, color_order="RGB" )["turbidity_metric"] if plotting: timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") title = f"Turbidity metric {np.round(turbidity_metric, 2)}" self.save_mask_comparison_plot( empty_img, measurement_img, mask, save_path=self.config["save_path"], filename=f"{timestamp}-turbidity0.png", title=title, ) self.save_mask_comparison_plot( empty_img, measurement_img, mask, save_path=self.config["save_path"], filename=f"{timestamp}-turbidity1.png", title=title, invert_mask=True, ) return self._build_dataset( name=name, turbidity_metric=turbidity_metric, measurement_img=measurement_img, empty_img=empty_img, mask=mask, cx=cx, cy=cy, empty_from_measurement=empty_from_measurement, is_empty_reference=False, )
_DEFAULT_CUSTOM_CONFIG = { "_classname": "AFL.automation.instrument.OpticalTurbidity.OpticalTurbidity", "_args": [ { "_classname": "AFL.automation.instrument.NetworkCamera.NetworkCamera", "_args": ["http://afl-video:8081/103/current"], } ], } _DEFAULT_CUSTOM_PORT = 5001 if __name__ == "__main__": from AFL.automation.shared.launcher import *