|
|
|
""" Sensospot Data Parser
|
|
|
|
|
|
|
|
Parsing the numerical output from Sensovations Sensospot image analysis.
|
|
|
|
"""
|
|
|
|
|
|
|
|
import pathlib
|
|
|
|
from typing import Any, Dict, Union, Optional
|
|
|
|
from xml.etree.ElementTree import Element as ElementType
|
|
|
|
|
|
|
|
import numpy
|
|
|
|
import pandas
|
|
|
|
from defusedxml import ElementTree
|
|
|
|
|
|
|
|
from . import columns
|
|
|
|
|
|
|
|
PathLike = Union[str, pathlib.Path]
|
|
|
|
|
|
|
|
|
|
|
|
def _search_params_file(folder: PathLike) -> Optional[PathLike]:
|
|
|
|
"""searches for a exposure settings file in a folder"""
|
|
|
|
folder_path = pathlib.Path(folder)
|
|
|
|
params_folder = folder_path / "Parameters"
|
|
|
|
if not params_folder.is_dir():
|
|
|
|
return None
|
|
|
|
param_files = list(params_folder.glob("**/*.svexp"))
|
|
|
|
if len(param_files) == 1:
|
|
|
|
return param_files[0]
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _get_channel_data(channel_node: ElementType) -> Dict[str, Any]:
|
|
|
|
# child.tag == "ChannelConfig1"
|
|
|
|
exposure_id = int(channel_node.tag[-1])
|
|
|
|
# channel_description == "[Cy3|Cy5] Green"
|
|
|
|
description = channel_node.attrib["Description"]
|
|
|
|
exposure_channel = description.rsplit(" ", 1)[-1]
|
|
|
|
# floats can be used for exposure times, not only ints
|
|
|
|
exposure_time = float(channel_node.attrib["ExposureTimeMs"])
|
|
|
|
return {
|
|
|
|
columns.EXPOSURE_ID: exposure_id,
|
|
|
|
columns.PARAMETERS_CHANNEL: exposure_channel.lower(),
|
|
|
|
columns.PARAMETERS_TIME: exposure_time,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_measurement_params(params_file: PathLike) -> pandas.DataFrame:
|
|
|
|
"""parses the cannel informations from a settings file"""
|
|
|
|
file_path = pathlib.Path(params_file)
|
|
|
|
with file_path.open("r") as file_handle:
|
|
|
|
tree = ElementTree.parse(file_handle)
|
|
|
|
data = [_get_channel_data(child) for child in tree.find("Channels")]
|
|
|
|
return pandas.DataFrame(data)
|
|
|
|
|
|
|
|
|
|
|
|
def get_measurement_params(folder: PathLike) -> Optional[pandas.DataFrame]:
|
|
|
|
"""returns measurement parameters"""
|
|
|
|
params_file = _search_params_file(folder)
|
|
|
|
if params_file is not None:
|
|
|
|
return _parse_measurement_params(params_file)
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def add_measurement_parameters(
|
|
|
|
data_frame: pandas.DataFrame, folder: PathLike
|
|
|
|
) -> pandas.DataFrame:
|
|
|
|
"""adds measurement params to the data frame, if they could be parsed"""
|
|
|
|
params = get_measurement_params(folder)
|
|
|
|
if params is not None:
|
|
|
|
params_exposures = params[columns.EXPOSURE_ID].unique()
|
|
|
|
data_exposures = data_frame[columns.EXPOSURE_ID].unique()
|
|
|
|
if set(data_exposures) == set(params_exposures):
|
|
|
|
return data_frame.merge(params, how="left", on=columns.EXPOSURE_ID)
|
|
|
|
|
|
|
|
# only executing if the parameters were not merged to the data frame
|
|
|
|
data_frame[columns.PARAMETERS_CHANNEL] = numpy.nan
|
|
|
|
data_frame[columns.PARAMETERS_TIME] = numpy.nan
|
|
|
|
return data_frame
|