scanbase.py 10.8 KB
Newer Older
payno's avatar
payno committed
1
2
# coding: utf-8
#/*##########################################################################
payno's avatar
payno committed
3
# Copyright (C) 2016- 2020 European Synchrotron Radiation Facility
payno's avatar
payno committed
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#############################################################################*/
"""This module contains base class for TomoScanBase"""

__authors__ = ["H.Payno"]
__license__ = "MIT"
__date__ = "09/10/2019"


import os
import logging
payno's avatar
payno committed
33
from typing import Union
34
from collections import OrderedDict
payno's avatar
payno committed
35
from .unitsystem.metricsystem import MetricSystem
payno's avatar
payno committed
36
37
38
39
40
41
42
43
44
45

logger = logging.getLogger(__name__)


class TomoScanBase:
    """
    Base Class representing a scan.
    It is used to obtain core information regarding an aquisition like
    projections, dark and flat field...

payno's avatar
payno committed
46
47
    :param scan: path to the root folder containing the scan.
    :type: Union[str,None]
payno's avatar
payno committed
48
    """
49
    DICT_TYPE_KEY = 'type'
payno's avatar
payno committed
50

51
    DICT_PATH_KEY = 'path'
payno's avatar
payno committed
52
53
54
55

    _SCHEME = None
    """scheme to read data url for this type of acquisition"""

payno's avatar
payno committed
56
    def __init__(self, scan: Union[None, str], type_: str):
payno's avatar
payno committed
57
58
59
        self.path = scan
        self._type = type_

60
61
62
63
64
    def clear_caches(self):
        """clear caches. Might be call if some data changed after
        first read of data or metadata"""
        raise NotImplementedError('Base class')

payno's avatar
payno committed
65
    @property
payno's avatar
payno committed
66
    def path(self) -> Union[None, str]:
payno's avatar
payno committed
67
68
69
70
71
72
73
74
        """

        :return: path of the scan root folder.
        :rtype: Union[str,None]
        """
        return self._path

    @path.setter
payno's avatar
payno committed
75
    def path(self, path: Union[str, None]) -> None:
payno's avatar
payno committed
76
77
78
79
80
81
82
        if path is None:
            self._path = path
        else:
            assert type(path) is str
            self._path = os.path.abspath(path)

    @property
payno's avatar
payno committed
83
    def type(self) -> str:
payno's avatar
payno committed
84
85
86
87
88
89
90
91
        """

        :return: type of the scanBase (can be 'edf' or 'hdf5' for now).
        :rtype: str
        """
        return self._type

    @staticmethod
payno's avatar
payno committed
92
    def is_tomoscan_dir(directory: str, **kwargs) -> bool:
payno's avatar
payno committed
93
94
95
96
97
98
99
100
101
        """
        Check if the given directory is holding an acquisition

        :param str directory:
        :return: does the given directory contains any acquisition
        :rtype: bool
        """
        raise NotImplementedError("Base class")

payno's avatar
payno committed
102
    def is_abort(self, **kwargs) -> bool:
payno's avatar
payno committed
103
104
105
106
107
108
109
110
        """

        :return: True if the acquisition has been abort
        :rtype: bool
        """
        raise NotImplementedError("Base class")

    @property
payno's avatar
payno committed
111
    def flats(self) -> Union[None, dict]:
payno's avatar
payno committed
112
113
114
115
        """list of flats files"""
        return self._flats

    @flats.setter
payno's avatar
payno committed
116
    def flats(self, flats: Union[None,dict]) -> None:
payno's avatar
payno committed
117
118
119
        self._flats = flats

    @property
120
    def darks(self) -> Union[None, dict]:
payno's avatar
payno committed
121
122
123
124
        """list of darks files"""
        return self._darks

    @darks.setter
payno's avatar
payno committed
125
    def darks(self, darks: Union[None,dict]) -> None:
payno's avatar
payno committed
126
127
128
        self._darks = darks

    @property
payno's avatar
payno committed
129
    def projections(self) -> Union[None,dict]:
payno's avatar
payno committed
130
131
132
133
        """list of projections files"""
        return self._projections

    @projections.setter
payno's avatar
payno committed
134
    def projections(self, projections: dict) -> None:
payno's avatar
payno committed
135
136
137
        self._projections = projections

    @property
payno's avatar
payno committed
138
    def dark_n(self) -> Union[None, int]:
payno's avatar
payno committed
139
140
141
        raise NotImplementedError('Base class')

    @property
payno's avatar
payno committed
142
    def tomo_n(self) -> Union[None, int]:
143
        """number of projection WITHOUT the return projections"""
payno's avatar
payno committed
144
145
146
        raise NotImplementedError('Base class')

    @property
payno's avatar
payno committed
147
    def ref_n(self) -> Union[None, int]:
payno's avatar
payno committed
148
149
150
        raise NotImplementedError('Base class')

    @property
payno's avatar
payno committed
151
    def pixel_size(self) -> Union[None, float]:
payno's avatar
payno committed
152
153
        raise NotImplementedError('Base class')

payno's avatar
payno committed
154
155
    def get_pixel_size(self, unit='m') -> Union[None, float]:
        if self.pixel_size:
payno's avatar
payno committed
156
            return self.pixel_size / MetricSystem.from_value(unit).value
payno's avatar
payno committed
157
158
159
        else:
            return None

payno's avatar
payno committed
160
    @property
payno's avatar
payno committed
161
    def dim_1(self) -> Union[None, int]:
payno's avatar
payno committed
162
163
164
        raise NotImplementedError('Base class')

    @property
payno's avatar
payno committed
165
    def dim_2(self) -> Union[None, int]:
payno's avatar
payno committed
166
167
168
        raise NotImplementedError('Base class')

    @property
payno's avatar
payno committed
169
    def ff_interval(self) -> Union[None, int]:
payno's avatar
payno committed
170
171
172
        raise NotImplementedError('Base class')

    @property
payno's avatar
payno committed
173
    def scan_range(self) -> Union[None, int]:
payno's avatar
payno committed
174
175
        raise NotImplementedError('Base class')

payno's avatar
payno committed
176
177
178
179
180
181
182
183
    @property
    def energy(self) -> Union[None, float]:
        """

        :return: incident beam energy in keV
        """
        raise NotImplementedError('Base class')

payno's avatar
payno committed
184
185
186
187
188
189
190
191
    @property
    def distance(self) -> Union[None, float]:
        """

        :return: sample / detector distance in meter
        """
        raise NotImplementedError('Base class')

payno's avatar
payno committed
192
193
194
195
196
197
198
199
200
201
202
    def get_distance(self, unit='m') -> Union[None, float]:
        """

        :param Union[MetricSystem, str] unit: unit requested for the distance
        :return: sample / detector distance with the requested unit
        """
        if self.distance:
            return self.distance / MetricSystem.from_value(unit).value
        else:
            return None

payno's avatar
payno committed
203
    def update(self) -> None:
payno's avatar
payno committed
204
205
206
        """Parse the root folder and files to update informations"""
        raise NotImplementedError("Base class")

payno's avatar
payno committed
207
    def to_dict(self) -> dict:
payno's avatar
payno committed
208
209
210
211
212
213
214
        """

        :return: convert the TomoScanBase object to a dictionary.
                 Used to serialize the object for example.
        :rtype: dict
        """
        res = dict()
215
216
        res[self.DICT_TYPE_KEY] = self.type
        res[self.DICT_PATH_KEY] = self.path
payno's avatar
payno committed
217
218
        return res

payno's avatar
payno committed
219
    def load_from_dict(self, _dict: dict):
payno's avatar
payno committed
220
221
222
223
224
225
226
227
228
229
        """
        Load properties contained in the dictionnary.

        :param _dict: dictionary to load
        :type: dict
        :return: self
        :raises: ValueError if dict is invalid
        """
        raise NotImplementedError("Base class")

payno's avatar
payno committed
230
    def equal(self, other) -> bool:
payno's avatar
payno committed
231
232
233
234
235
236
237
238
239
240
241
242
243
        """

        :param :class:`.ScanBase` other: instance to compare with 
        :return: True if instance are equivalent
        :note: we cannot use the __eq__ function because this object need to be
               pickable
        """
        return (
            isinstance(other, self.__class__) or isinstance(self, other.__class__) and
            self.type == other.type and
            self.path == other.path
        )

payno's avatar
payno committed
244
    def get_proj_angle_url(self) -> dict:
payno's avatar
payno committed
245
        """
246
247
248
249
250
        return a dictionary of all the projection. key is the angle of the
        projection and value is the url.

        Keys are int for 'standard' projections and strings for return
        projections.
payno's avatar
payno committed
251
252
253
254
255
256

        :return dict: angles as keys, radios as value.
        """
        raise NotImplementedError('Base class')

    @staticmethod
payno's avatar
payno committed
257
    def map_urls_on_scan_range(urls, n_projection, scan_range) -> dict:
payno's avatar
payno committed
258
259
260
261
262
263
264
265
266
267
268
269
270
        """
        map given urls to an angle regarding scan_range and number of projection.
        We take the hypothesis that 'extra projection' are taken regarding the
        'id19' policy:
         * If the acquisition has a scan range of 360 then:
            * if 4 extra projection, the angles are (270, 180, 90, 0)
            * if 5 extra projection, the angles are (360, 270, 180, 90, 0)
         * If the acquisition has a scan range of 180 then:
            * if 2 extra projections: the angles are (90, 0)
            * if 3 extra projections: the angles are (180, 90, 0)

        :warning: each url should contain only one radio.

271
        :param urls: dict with all the urls. First url should be
payno's avatar
payno committed
272
273
                     the first radio acquire, last url should match the last
                     radio acquire.
274
        :type: dict
payno's avatar
payno committed
275
276
277
278
279
280
        :param n_projection: number of projection for the sample.
        :type: int
        :param scan_range: acquisition range (usually 180 or 360)
        :type: float
        :return: angle in degree as key and url as value
        :rtype: dict
281
282
283

        :raises: ValueError if the number of extra images found and scan_range
                 are incoherent
payno's avatar
payno committed
284
285
        """
        assert n_projection is not None
286
        ordered_url = OrderedDict(sorted(urls.items(), key=lambda x: x))
payno's avatar
payno committed
287

288
        res = {}
payno's avatar
payno committed
289
290
        # deal with the 'standard' acquisitions
        for proj_i in range(n_projection):
291
            url = list(ordered_url.values())[proj_i]
292
293
294
295
            if n_projection == 1:
                angle = 0.0
            else:
                angle = proj_i * scan_range / (n_projection - 1)
payno's avatar
payno committed
296
            if proj_i < len(urls):
297
                res[angle] = url
payno's avatar
payno committed
298
299
300
301

        if len(urls) > n_projection:
            # deal with extra images (used to check if the sampled as moved for
            # example)
302
            extraImgs = list(ordered_url.keys())[n_projection:]
payno's avatar
payno committed
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
            if len(extraImgs) in (4, 5):
                if scan_range < 360:
                    logger.warning('incoherent data information to retrieve'
                                   'scan extra images angle')
                elif len(extraImgs) == 4:
                    res['270(1)'] = extraImgs[0]
                    res['180(1)'] = extraImgs[1]
                    res['90(1)'] = extraImgs[2]
                    res['0(1)'] = extraImgs[3]
                else:
                    res['360(1)'] = extraImgs[0]
                    res['270(1)'] = extraImgs[1]
                    res['180(1)'] = extraImgs[2]
                    res['90(1)'] = extraImgs[3]
                    res['0(1)'] = extraImgs[4]
            elif len(extraImgs) in (2, 3):
                if scan_range > 180:
                    logger.warning('incoherent data information to retrieve'
                                   'scan extra images angle')
                elif len(extraImgs) is 3:
                    res['180(1)'] = extraImgs[0]
                    res['90(1)'] = extraImgs[1]
                    res['0(1)'] = extraImgs[2]
                else:
                    res['90(1)'] = extraImgs[0]
                    res['0(1)'] = extraImgs[1]
            else:
330
331
                raise ValueError('incoherent data information to retrieve scan'
                                 'extra images angle')
payno's avatar
payno committed
332
        return res