import calendar
import copy
from os import fspath
import time
import xml.etree.ElementTree as ET
import numpy as np
import qpimage
import tifffile
from .dataset import SingleData
# baseline clamp intensity normalization for phasics tif files
INTENSITY_BASELINE_CLAMP = 150
# https://www.loc.gov/preservation/digital/formats/content/tiff_tags.shtml
TIFF_TAGS = {
"MaxSampleValue": 281,
}
class LoadTifPhasicsError(BaseException):
pass
[docs]class SingleTifPhasics(SingleData):
"""Phasics image ("SID PHA*.tif")
Notes
-----
- Only the processed phase data files are supported, i.e. TIFF
file names starting with "SID PHA" exported by the commercial
Phasics software.
- If the "wavelength" key in `meta_data` is not set (units: [m]),
then the wavelength is extracted from the xml data stored in
tag "61238" of the tif file.
"""
storage_type = "phase,intensity"
def __init__(self, path, meta_data={}, *args, **kwargs):
if "wavelength" not in meta_data:
# get wavelength if not given
wl = self._get_wavelength(path)
if not np.isnan(wl):
meta_data["wavelength"] = wl
else:
# We need the wavelength to convert OPD to phase
msg = "'wavelength' must be specified in `meta_data`!"
raise LoadTifPhasicsError(msg)
super(SingleTifPhasics, self).__init__(path=path,
meta_data=meta_data,
*args, **kwargs)
@staticmethod
def _get_meta_data(path, section, name):
with SingleTifPhasics._get_tif(path) as tf:
meta = tf.pages[0].tags[61238].value
meta = meta.strip("'b")
meta = meta.replace("\\n", "\n")
meta = meta.replace("\\r", "")
root = ET.fromstring("<root>\n" + meta + "</root>")
for phadata in root:
for cluster in phadata:
sec = cluster[0].text
for child in cluster:
if len(child) == 2:
nm, val = child
# print(sec, nm.text, val.text)
if (sec.lower() == section and
nm.text.lower() == name):
return val.text
else:
return None
@staticmethod
def _get_tif(path):
if hasattr(path, "seek"): # opened file
# Seek open file zero to avoid error in tifffile:
# "ValueError: invalid TIFF file"
path.seek(0)
else: # path
path = fspath(path)
return tifffile.TiffFile(path)
def _get_wavelength(self, path):
for section in ["analyse data", "analyse data v1"]:
wl_str = SingleTifPhasics._get_meta_data(path=path,
section=section,
name="lambda(nm)")
if wl_str:
wavelength = float(wl_str) * 1e-9
break
else:
wavelength = np.nan
return wavelength
def get_time(self, idx=0):
"""Return the time of the tif data since the epoch
The time is stored in the "61238" tag.
"""
timestr = SingleTifPhasics._get_meta_data(path=self.path,
section="acquisition info",
name="date & heure")
if timestr is not None:
timestr = timestr.replace(",", ".")
timestrf, timeus = timestr.split(".")
# '2016-04-29_17h31m35s.00827'
structtime = time.strptime(timestrf,
"%Y-%m-%d_%Hh%Mm%Ss")
fracsec = float(timeus) * 1e-5
# use calendar, because we need UTC
thetime = calendar.timegm(structtime) + fracsec
else:
thetime = np.nan
return thetime
def get_qpimage_raw(self, idx=0):
"""Return QPImage without background correction"""
# Load experimental data
with SingleTifPhasics._get_tif(self.path) as tf:
# page 0 contains intensity
# page 1 contains phase in nm
# page 2 contains phase in wavelengths
# Intensity:
inttags = tf.pages[0].tags
imin = inttags["61243"].value
imax = inttags["61242"].value
isamp = inttags[TIFF_TAGS["MaxSampleValue"]].value
blc = INTENSITY_BASELINE_CLAMP
inten = tf.pages[0].asarray() * (imax - imin) / isamp + imin - blc
inten[inten < 0] = 0
# Phase
# The SID4Bio records two phase images, one in wavelengths and
# one in nanometers. Surprisingly, these two phase images are
# not derived from the same recording. The first image (pages[1])
# (in wavelengths) matches the intensity image (pages[0]). The
# second image (pages[2]) is recorded at a different time point.
# Initially, I thought it would be best to compute the phase
# directly from the measured value in nanometers (pages[2]) using
# the known wavelength given by the qpformat user. However, since
# phase and amplitude won't match up in that case, the wavelength
# phase image (pages[1]) has to be used. Since phasics uses its own
# wavelength (set by the user in the acquisition/extraction
# software) which might be wrong, I decided to first compute
# the phase in nanometers from tf.pages[1] using the phasics
# wavelength and then proceed as before, computing the phase
# in radians using the correct, user-given wavelength.
wl_phasics = self._get_wavelength(self.path)
if not np.isnan(wl_phasics):
# proceed with phase in wavelengths
phaid = 1
else:
# proceed with phase in nanometers
phaid = 2
phatags = tf.pages[phaid].tags
pmin = phatags["61243"].value
pmax = phatags["61242"].value
psamp = phatags[TIFF_TAGS["MaxSampleValue"]].value
if psamp == 0 or pmin == pmax:
# no phase data
pha = np.zeros_like(inten)
else:
# optical path difference
opd = tf.pages[phaid].asarray() * (pmax - pmin) / psamp + pmin
if phaid == 1: # convert [wavelengths] to [nm]
assert not np.isnan(wl_phasics)
opd *= wl_phasics * 1e9
# convert from [nm] to [rad]
pha = opd / (self.meta_data["wavelength"] * 1e9) * 2 * np.pi
meta_data = copy.copy(self.meta_data)
if "time" not in meta_data:
meta_data["time"] = self.get_time()
qpi = qpimage.QPImage(data=(pha, inten),
which_data="phase,intensity",
meta_data=meta_data,
h5dtype=self.as_type)
# set identifier
qpi["identifier"] = self.get_identifier()
return qpi
@staticmethod
def verify(path):
"""Verify that `path` is a phasics phase/intensity TIFF file"""
valid = False
try:
tf = SingleTifPhasics._get_tif(path)
except (ValueError, IsADirectoryError, KeyError,
tifffile.tifffile.TiffFileError):
pass
else:
if (len(tf.pages) == 3 and
61243 in tf.pages[0].tags and
61242 in tf.pages[0].tags and
61238 in tf.pages[0].tags and
61243 in tf.pages[1].tags and
61242 in tf.pages[1].tags and
TIFF_TAGS["MaxSampleValue"] in tf.pages[0].tags and
(tf.pages[0].tags[61242].value !=
tf.pages[1].tags[61242].value)):
valid = True
return valid