/
core.py
executable file
·1226 lines (1060 loc) · 48.5 KB
/
core.py
1
2
3
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
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
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
"""Implements core image handling classes for the :mod:`Stoner.Image` package."""
__all__ = ["ImageArray", "ImageFile"]
import os
from copy import copy, deepcopy
import inspect
from importlib import import_module
from io import BytesIO as StreamIO
from warnings import warn
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
from scipy import ndimage as ndi
from skimage import (
color,
exposure,
feature,
io,
measure,
filters,
graph,
util,
restoration,
morphology,
segmentation,
transform,
)
from ..core.base import typeHintedDict, metadataObject
from ..core.exceptions import StonerLoadError, StonerUnrecognisedFormat
from ..Core import DataFile
from ..tools import isTuple, isLikeList, make_Data
from ..tools.file import file_dialog, get_file_name_type, auto_load_classes
from ..tools.decorators import class_modifier, image_file_adaptor, class_wrapper, clones
from ..compat import (
string_types,
get_filedialog,
int_types,
path_types,
) # Some things to help with Python2 and Python3 compatibility
from .attrs import DrawProxy, MaskProxy
from .widgets import RegionSelect
from . import imagefuncs
from ..tools.classes import Options
IMAGE_FILES = [("Tiff File", "*.tif;*.tiff"), ("PNG files", "*.png", "Numpy Files", "*.npy")]
dtype_range = {
np.bool_: (False, True),
np.bool8: (False, True),
np.uint8: (0, 255),
np.uint16: (0, 65535),
np.int8: (-128, 127),
np.int16: (-32768, 32767),
np.int64: (-(2**63), 2**63 - 1),
np.uint64: (0, 2**64 - 1),
np.int32: (-(2**31), 2**31 - 1),
np.uint32: (0, 2**32 - 1),
np.float16: (-1, 1),
np.float32: (-1, 1),
np.float64: (-1, 1),
}
def _add_core_(result, other):
"""Actually do result=result-other."""
if isinstance(other, type(result)) and result.shape == other.shape:
result.image += other.image
elif isinstance(other, np.ndarray) and other.shape == result.shape:
result.image += other
elif isinstance(other, (int, float)):
result.image += other
else:
return NotImplemented
return result
def _floor_div_core_(result, other):
"""Actually do result=result/other."""
# Cheat and pass through to ImageArray
if isinstance(other, ImageFile):
other = other.image
result.image = result.image // other
return result
def _div_core_(result, other):
"""Actually do result=result/other."""
# Cheat and pass through to ImageArray
if isinstance(other, ImageFile):
other = other.image
result.image = result.image / other
return result
def _sub_core_(result, other):
"""Actually do result=result-other."""
if isinstance(other, type(result)) and result.shape == other.shape:
result.image -= other.image
elif isinstance(other, np.ndarray) and other.shape == result.shape:
result.image -= other
elif isinstance(other, (int, float)):
result.image -= other
else:
return NotImplemented
return result
def copy_into(source: "ImageFile", dest: "ImageFile") -> "ImageFile":
"""Copy the data associated with source to dest.
Args:
source(ImageFile): The ImageFile object to be copied from
dest (ImageFile): The ImageFile objrct to be changed by receiving the copiued data.
Returns:
The modified *dest* ImageFile.
Unlike copying or deepcopying a ImageFile, this function preserves the class of the destination and just
overwrites the attributes that represent the data in the ImageFile.
"""
dest.image = source.image.clone
for k in source._public_attrs:
if hasattr(source, k):
setattr(dest, k, deepcopy(getattr(source, k)))
return dest
@class_modifier(
[
color,
exposure,
feature,
io,
measure,
filters,
filters.rank,
graph,
util,
restoration,
morphology,
segmentation,
transform,
]
)
@class_modifier([ndi], transpose=True)
@class_modifier(imagefuncs, overload=True)
class ImageArray(np.ma.MaskedArray, metadataObject):
"""A numpy array like class with a metadata parameter and pass through to skimage methods.
ImageArray is for manipulating images stored as a 2d numpy array.
It is built to be almost identical to a numpy array except for one extra
parameter which is the metadata. This stores information about the image
in a dictionary object for later retrieval.
All standard numpy functions should work as normal and casting two types
together should yield a ImageArray type (ie. ImageArray+np.ndarray=ImageArray)
In addition any function from skimage should work and return a ImageArray.
They can be called as eg. im=im.gaussian(sigma=2). Don't include the module
name, just the function name (ie not filters.gaussian). Also omit the first
image argument required by skimage.
Attributes:
metadata (:py:class:`Stoner.core.regexpDict`):
A dictionary of metadata items associated with this image.
filename (str):
The name of the file from which this image was loaded.
title (str):
The title of the image (defaults to the filename).
mask (:py:class:`numpy.ndarray of bool`):
The underlying mask data of the image. Masked elements (i.e. where mask=True) are ignored for many
image operations. Indexing them will return the mask fill value (typically NaN, ot -1 or -MAXINT)
draw (:py:class:`Stoner.Image.attrs.DrawProxy`):
A special object that allows the user to manipulate the image data by making use of
:py:mod:`skimage.draw` functions as well as some additional drawing functions.
clone (:py:class:`Stoner.ImageArry`):
Return a duplicate copy of the current image - this allows subsequent methods to
modify the cloned version rather than the original version.
centre (tuple of (float,float)):
The coordinates of the centre of the image.
aspect (float):
The aspect ratio (width/height) of the image.
max_box (tuple (0,x-size,0-y-size)):
The extent of the image size in a form suitable for use in defining a box.
flip_h (:py:class:`ImageArray`):
Clone the current image and then flip it horizontally (left-right).
flip_v (:py:class:`ImageArray`):
Clone the current image and then flip it vertically (top-bottom).
CW (:py:class:`ImageArray`):
Clone the current image and then rotate it 90 degrees clockwise.
CCW (:py:class:`ImageArray`):
Clone the current image and then rotate it 90 degrees counter-clockwise.
T (:py:class:`ImageArray`):
Transpose the current image
shape (tuple (int,int)):
Return the current shape of the image (rows, columns)
dtype (:py:class:`numpy.dtype`):
The current dtype of the elements of the image data.
For clarity it should be noted that any function will not alter the current
instance, it will clone it first then return the clone after performing the
function on it.
Note:
For arrays the indexing is (row, column). However the normal way to index
an image would be to do (horizontal, vert), which is the opposite.
In ImageArray the coordinate system is chosen similar to skimage. y points
down x points right and the origin is in the top left corner of the image.
When indexing the array therefore you need to give it (y,x) coordinates
for (row, column).::
----> x (column)
|
|
v
y (row)
eg I want the 4th pixel in the horizontal direction and the 10th pixel down
from the top I would ask for ImageArray[10,4]
but if I want to translate the image 4 in the x direction and 10 in the y
I would call im=im.translate((4,10))
"""
# Proxy attributes for storing imported functions. Only do the import when needed
_func_proxy = None
# extra attributes for class beyond standard numpy ones
# Default values for when we can't find the attribute already
_defaults = {"debug": False, "_hardmask": False}
fmts = ["png", "npy", "tiff", "tif"]
# These will be overridden with instance attributes, but setting here allows ImageFile properties to be defined.
debug = False
filename = ""
# now initialise class
def __new__(cls, *args, **kargs):
"""Construct an ImageArray object.
We're using __new__ rather than __init__ to imitate a numpy array as
close as possible.
"""
array_arg_keys = ["dtype", "copy", "order", "subok", "ndmin", "mask"] # kwargs for array setup
array_args = {k: kargs.pop(k) for k in array_arg_keys if k in kargs.keys()}
user_metadata = kargs.get("metadata", {})
if len(args) not in [0, 1]:
raise ValueError(f"ImageArray expects 0 or 1 arguments, {len(args)} given")
# 0 args initialisation
if len(args) == 0:
ret = np.empty((0, 0), dtype=float).view(cls)
# merge the results of __new__ from emtadataObject
else:
# 1 args initialisation
arg = args[0]
loadfromfile = False
if isinstance(arg, cls):
ret = arg
elif isinstance(arg, np.ndarray):
# numpy array or ImageArray)
if arg.ndim < 2:
ret = np.atleast_2d(arg).view(ImageArray)
else:
ret = arg.view(ImageArray)
kargs["metadata"] = getattr(arg, "metadata", typeHintedDict())
kargs["metadata"].update(user_metadata)
elif isinstance(arg, bool) and not arg:
patterns = (("png", "*.png"), ("npy", "*.npy"))
arg = get_filedialog(what="r", filetypes=patterns)
if len(arg) == 0:
raise ValueError("No file given")
loadfromfile = True
elif isinstance(arg, path_types) or loadfromfile:
# Filename- load datafile
if not os.path.exists(arg):
raise ValueError(f"File path does not exist {arg}")
ret = np.empty((0, 0), dtype=float).view(cls)
ret = ret._load(arg, **array_args) # pylint: disable=no-member
kargs["metadata"] = getattr(ret, "metadata", typeHintedDict())
kargs["metadata"].update(user_metadata)
elif isinstance(arg, ImageFile):
# extract the image
ret = arg.image
kargs["metadata"] = getattr(ret, "metadata", typeHintedDict())
kargs["metadata"].update(user_metadata)
else:
try: # try converting to a numpy array (eg a list type)
ret = np.asarray(arg, **array_args).view(cls)
if ret.dtype == "O": # object dtype - can't deal with this
raise ValueError
except ValueError as err: # ok couldn't load from iterable, we're done
raise ValueError(f"No constructor for {arg}") from err
asfloat = kargs.pop("asfloat", False) or kargs.pop(
"convert_float", False
) # convert_float for back compatibility
if asfloat and ret.dtype.kind != "f": # convert to float type in place
ret = ret.convert(np.float64)
ret.__dict__["kargs"] = kargs
return ret
def __array_finalize__(self, obj):
"""__array_finalize__ is a necessary functions when subclassing numpy.ndarray to fix some behaviours.
See http://docs.scipy.org/doc/numpy-1.10.1/user/basics.subclassing.html for
more info and examples
Defaults below are only set when constructing an array using view
eg np.arange(10).view(ImageArray). Otherwise filename and metadata
attributes are just copied over (plus any other attributes set in
_optinfo).
"""
if not hasattr(self, "_optinfo"):
setattr(self, "_optinfo", {"metadata": typeHintedDict({}), "filename": ""})
kargs = self.__dict__.pop("kargs", {}) # pylint: disable=access-member-before-definition
tmp = metadataObject.__new__(metadataObject)
tmp.__dict__.update(self.__dict__) # pylint: disable=access-member-before-definition
self.__dict__ = tmp.__dict__
# Deal with kwargs
user_metadata = kargs.pop("metadata", {})
_debug = kargs.pop("debug", False)
_title = kargs.pop("title", None)
self.metadata.update(user_metadata)
# all constructors call array_finalise so metadata is now initialised
self.filename = self.metadata.setdefault("Loaded from", "")
self.debug = _debug
self._title = _title
self._public_attrs = {"title": str, "filename": str}
self._mask_color = "red"
self._mask_alpha = 0.5
# merge the results of __new__ from emtadataObject
if getattr(self, "debug", False):
curframe = inspect.currentframe()
calframe = inspect.getouterframes(curframe, 2)
print(curframe, calframe)
if obj is not None:
self._optinfo.update(getattr(obj, "_optinfo", {}))
super().__array_finalize__(obj=obj)
def _load(self, filename, *args, **kargs):
"""Load an image from a file and return as a ImageArray."""
cls = type(self)
fmt = kargs.pop("fmt", os.path.splitext(filename)[1][1:])
handlers = {"npy": cls._load_npy, "png": cls._load_png, "tiff": cls._load_tiff, "tif": cls._load_tiff}
if fmt not in handlers:
raise StonerLoadError(f"{fmt} is not a recognised format for loading.")
ret = handlers[fmt](filename, **kargs)
return ret
@classmethod
def _load_npy(cls, filename, **kargs):
"""Load image data from a numpy file."""
image = np.load(filename)
image = np.array(image, **kargs).view(cls)
image.metadata["Loaded from"] = os.path.realpath(filename) # pylint: disable=no-member
image.filename = os.path.realpath(filename)
return image
@classmethod
def _load_png(cls, filename, **kargs): # pylint: disable=unused-argument
"""Create a new ImageArray from a png file."""
with Image.open(filename, "r") as img:
image = np.asarray(img).view(cls)
# Since skimage.img_as_float() looks at the dtype of the array when mapping ranges, it's important to make
# sure that we're not using too many bits to store the image in. This is a bit of a hack to reduce the
# bit-depth...
if np.issubdtype(image.dtype, np.integer):
bits = np.ceil(np.log2(image.max()))
if bits <= 8:
image = image.astype("uint8")
elif bits <= 16:
image = image.astype("uint16")
elif bits <= 32:
image = image.astype("uint32")
for k in img.info:
v = img.info[k]
if v.startswith("b'"):
v = v.strip(" b'")
v = bytes(v)
k = k.split("{")[0]
image.metadata[k] = v
image.metadata["Loaded from"] = os.path.realpath(filename)
image.filename = os.path.realpath(filename)
return image
@classmethod
def _load_tiff(cls, filename, **kargs): # pylint: disable=unused-argument
"""Create a new ImageArray from a tiff file."""
metadict = typeHintedDict({})
with Image.open(filename, "r") as img:
image = np.asarray(img)
if image.ndim == 3:
if image.shape[2] < 4: # Need to add a dummy alpha channel
image = np.append(np.zeros_like(image[:, :, 0]), axis=2)
image = image.view(dtype=np.uint32).reshape(image.shape[:-1])
tags = img.tag_v2
if 270 in tags:
from json import loads
try:
userdata = loads(tags[270])
typ = userdata.get("type", cls.__name__)
mod = userdata.get("module", cls.__module__)
mod = import_module(mod)
typ = getattr(mod, typ)
if not issubclass(typ, ImageArray):
raise TypeError(f"Bad type in Tiff file {typ.__name__} is not a subclass of Stoner.ImageArray")
metadata = userdata.get("metadata", [])
except (ValueError, TypeError, IOError):
metadata = []
else:
metadata = []
metadict.import_all(metadata)
# OK now try and sort out the datatype before loading
dtype = metadict.get(
"ImageArray.dtype", None
) # if tif was previously saved by Stoner then dtype should have been added to the metadata
# If we convert to float, it's important to make
# sure that we're not using too many bits to store the image in.
# This is a bit of a hack to reduce the bit-depth...
if dtype is None:
if np.issubdtype(image.dtype, np.integer):
bits = np.ceil(np.log2(image.max()))
if bits <= 8:
dtype = "uint8"
elif bits <= 16:
dtype = "uint16"
elif bits <= 32:
dtype = "uint32"
else:
dtype = np.dtype(image.dtype).name # retain the loaded datatype
try:
image = image.astype(dtype)
except TypeError: # Python 2.7 can throw up a bad type error here
pass
image = image.view(cls)
image.update(metadict)
image.metadata["Loaded from"] = os.path.realpath(filename)
image.filename = os.path.realpath(filename)
return image
def _box(self, *args, **kargs):
"""Construct and indexing tuple for selecting areas for cropping and boxing.
The box can be specified as:
- (int): a fixed number of pxiels is removed from all sides
- (float): the central region of the image is selected
- None: the whole image is selected
- False: The user can select a region of interest
- (iterable of length 4) - assumed to give 4 integers to describe a specific box
"""
if len(args) == 0 and "box" in kargs.keys():
args = [kargs["box"]] # back compatibility
elif len(args) not in (0, 1, 4):
raise ValueError("box accepts 1 or 4 arguments, {len(args)} given.")
if len(args) == 0 or (len(args) == 1 and args[0] is None):
args = RegionSelect()(self)
if len(args) == 1:
box = args[0]
if isinstance(box, bool) and not box: # box=False is the same as all values
return slice(None, None, None), slice(None, None, None)
if isLikeList(box) and len(box) == 4: # Full box as a list
box = [x for x in box]
elif isinstance(box, int): # Take a border of n pixels out
box = [box, self.shape[1] - box, box, self.shape[0] - box]
elif isinstance(box, string_types):
box = self.metadata[box]
return self._box(*box)
elif isinstance(box, float): # Keep the central fraction of the image
box = [
round(self.shape[1] * box / 2),
round(self.shape[1] * (1 - box / 2)),
round(self.shape[1] * box / 2),
round(self.shape[1] * (1 - box / 2)),
]
box = list([int(x) for x in box])
else:
raise ValueError(f"crop accepts tuple of length 4, {len(box)} given.")
else:
box = list(args)
for i, item in enumerate(box): # replace None with max extent
if isinstance(item, float) and 0 <= item <= 1:
if i < 2:
box[i] = int(round(self.shape[1] * item))
else:
box[i] = int(round(self.shape[0] * item))
elif isinstance(item, float):
box[i] = int(round(item))
elif isinstance(item, int_types):
pass
elif item is None:
box[i] = self.max_box[i]
else:
raise TypeError(f"Arguments for box should be floats, integers or None, not {type(item)}")
return slice(box[2], box[3]), slice(box[0], box[1])
#################################################################################################
################################################ Properties #####################################
@property
def aspect(self):
"""Return the aspect ratio (width/height) of the image."""
return float(self.shape[1]) / self.shape[0]
@property
def centre(self):
"""Return the coordinates of the centre of the image."""
return tuple(np.array(self.shape) / 2.0)
@property
def clone(self):
"""Duplicate the ImageFile and return the copy.
Using .clone allows further methods to modify the clone, allowing the original immage to be unmodified.
"""
ret = self.copy().view(type(self))
self._optinfo["mask"] = self.mask # Make sure we've updated our mask record
self._optinfo["metadata"] = self.metadata # Update metadata record
for k, v in self._optinfo.items():
try:
setattr(ret, k, deepcopy(v))
except (TypeError, ValueError, RecursionError):
if isinstance(v, np.ndarray):
setattr(self, k, np.copy(v).view(v.__class__))
else:
setattr(ret, k, copy(v))
return ret
@property
def flat(self):
"""Return the numpy.ndarray.flat rather than a MaskedIterator."""
return self.asarray().flat
@property
def max_box(self):
"""Return the maximum coordinate extent (xmin,xmax,ymin,ymax)."""
box = (0, self.shape[1], 0, self.shape[0])
return box
@property
def title(self):
"""Get a title for this image."""
if self._title is None:
return self.filename
return self._title
@title.setter
def title(self, title):
"""Set the title of the current image."""
if not isinstance(title, str):
title = repr(title)
self._title = title
@property
@clones
def flip_h(self):
"""Clone the image and then mirror the image horizontally."""
ret = self.clone[:, ::-1]
return ret
@property
@clones
def flip_v(self):
"""Clone the image and then mirror the image vertically."""
ret = self.clone[::-1, :]
return ret
@property
@clones
def CW(self):
"""Clone the image and then rotate the imaage 90 degrees clockwise."""
return self.clone.T[:, ::-1]
@property
@clones
def CCW(self):
"""Clone the image and then rotate the imaage 90 degrees counter clockwise."""
return self.clone.T[::-1, :]
@property
def draw(self):
"""Access the DrawProxy object for accessing the skimage draw sub module."""
return DrawProxy(self, self)
# ==============================================================================
# OTHER SPECIAL METHODS
# ==============================================================================
def __getstate__(self):
"""Help with pickling ImageArrays."""
ret = super().__getstate__()
return {"numpy": ret, "ImageArray": {"metadata": self.metadata}}
def __setstate__(self, state):
"""Help with pickling ImageArrays."""
original = state.pop("numpy", tuple())
local = state.pop("ImageArray", {})
metadata = local.pop("metadata", {})
super().__setstate__(original)
self.metadata.update(metadata)
def __delattr__(self, name):
"""Handle deleting attributes."""
super().__delattr__(name)
if name in self._optinfo:
del self._optinfo[name]
def __setattr__(self, name, value):
"""Set an attribute on the object."""
super().__setattr__(name, value)
# add attribute to those for copying in array_finalize. use value as
# default.
circ = ["_optinfo", "mask", "__dict__"] # circular references
proxy = ["_funcs"] # can be reloaded for cloned arrays
if name in circ + proxy:
# Ignore these in clone
pass
else:
self._optinfo.update({name: value})
def __getitem__(self, index):
"""Patch indexing of strings to metadata."""
if getattr(self, "debug", False):
curframe = inspect.currentframe()
calframe = inspect.getouterframes(curframe, 2)
print(curframe, calframe)
if isinstance(index, ImageFile) and index.image.dtype == bool:
index = index.image
if isinstance(index, string_types):
return self.metadata[index]
return super().__getitem__(index)
def __setitem__(self, index, value):
"""Patch string index through to metadata."""
if isinstance(index, ImageFile) and index.dtype == bool:
index = index.image
if isinstance(index, string_types):
self.metadata[index] = value
else:
super().__setitem__(index, value)
def __delitem__(self, index):
"""Patch indexing of strings to metadata."""
if isinstance(index, string_types):
del self.metadata[index]
else:
super().__delitem__(index)
def save(self, filename=None, **kargs):
"""Stub method for a save function."""
raise NotImplementedError(f"Save is not implemented in {self.__class__}")
@class_modifier(
[
color,
exposure,
feature,
io,
measure,
filters,
filters.rank,
graph,
util,
restoration,
morphology,
segmentation,
transform,
],
adaptor=image_file_adaptor,
)
@class_modifier(
[ndi],
transpose=True,
adaptor=image_file_adaptor,
)
@class_modifier(imagefuncs, overload=True, adaptor=image_file_adaptor)
@class_wrapper(target=ImageArray, exclude_below=metadataObject)
class ImageFile(metadataObject):
"""An Image file type that is analogous to :py:class:`Stoner.Data`.
This contains metadata and an image attribute which
is an :py:class:`Stoner.Image.ImageArray` type which subclasses numpy ndarray and
adds lots of extra image specific processing functions.
Attributes:
image (:py:class:`Stoner.Image.ImageArray`):
A :py:class:`numpy.ndarray` subclass that stores the actual image data.
metadata (:py:class:`Stoner.core.regexpDict`):
A dictionary of metadata items associated with this image.
filename (str):
The name of the file from which this image was loaded.
title (str):
The title of the image (defaults to the filename).
mask (:py:class:`Stoner.Image.attrs.MaskProxy`):
A special object that allows manipulation of the image's mask - thius allows the
user to selectively disable regions of the image from rpocessing functions.
draw (:py:class:`Stoner.Image.attrs.DrawProxy`):
A special object that allows the user to manipulate the image data by making use of
:py:mod:`skimage.draw` functions as well as some additional drawing functions.
clone (:py:class:`Stoner.ImageFile`):
Return a duplicate copy of the current image - this allows subsequent methods to
modify the cloned version rather than the original version.
centre (tuple of (int,int)):
The coordinates of the centre of the image.
aspect (float):
The aspect ratio (width/height) of the image.
max_box (tuple (0,x-size,0-y-size)):
The extent of the image size in a form suitable for use in defining a box.
flip_h (ImageFile):
Clone the current image and then flip it horizontally (left-right).
flip_v (ImageFile):
Clone the current image and then flip it vertically (top-bottom).
CW (ImageFile):
Clone the current image and then rotate it 90 degrees clockwise.
CCW (ImageFile):
Clone the current image and then rotate it 90 degrees counter-clockwise.
T (ImageFile):
Transpose the current image
shape (tuple (int,int)):
Return the current shape of the image (rows, columns)
dtype (:py:class:`numpy.dtype`):
The current dtype of the elements of the image data.
The ImageFile owned attribute is image. All other calls including metadata
are passed through to ImageArray (so no need to inherit from metadataObject).
Almost all calls to ImageFile are passed through to the underlying ImageArray
logic and ImageArray can be used as a standalone class.
However because ImageArray subclasses an ndarray it is not possible to enter
it in place. All attributes return an array instance which needs to be reassigned.
ImageFile owns image and so can change in place.
The penalty is that numpy ufuncs don't return ImageFile type
so can do::
imfile.asfloat() #imagefile.image is updated to float type however need to do:
imfile.image = np.abs(imfile.image)
whereas for imarray need to do::
imarray = imagearray.asfloat()
but::
np.abs(imarray) #returns ImageArray type
"""
# pylint: disable=no-member
_protected_attrs = ["_fromstack"] # these won't be passed through to self.image attrs
def __init__(self, *args, **kargs):
"""Mostly a pass through to ImageArray constructor.
Local attribute is image. All other attributes and calls are passed
through to image attribute.
There is one special case of creating an ImageFile from a :py:class:`Stoner.Core.DataFile`. In this case the
the DataFile is assumed to contain (x,y,z) data that should be converted to a map of
z on a regular grid of x,y. The columns for the x,y,z data can be taken from the DataFile's
:py:attr:`Stoner.Core.DataFile.setas` attribute or overridden by providing xcol, ycol and zcol keyword
arguments. A further *shape* keyword can spewcify the shape as a tuple or "unique" to use the unique values of
x and y or if omitted asquare grid will be interpolated.
"""
self._image = ImageArray() # Ensire we have the image data in place
super().__init__(*args, **kargs)
args = list(args)
if len(args) == 0:
pass
elif len(args) > 0 and isinstance(args[0], path_types):
try:
copy_into(self.__class__.load(args[0], **kargs), self)
self._public_attrs = {"title": str, "filename": str}
self._fromstack = kargs.pop("_fromstack", False) # for use by ImageStack
return
except StonerLoadError:
args[0] = ImageArray(*args, **kargs)
if len(args) > 0 and isinstance(args[0], ImageFile): # Fixing type
self._image = args[0].image
for k in args[0]._public_attrs:
setattr(self, k, getattr(args[0], k, None))
elif len(args) > 0 and isinstance(args[0], np.ndarray): # Fixing type
self._image = ImageArray(*args, **kargs)
if isinstance(args[0], ImageArray):
for k in args[0]._public_attrs:
setattr(self, k, getattr(args[0], k, None))
elif len(args) > 0 and isinstance(
args[0], DataFile
): # Support initing from a DataFile that defines x,y,z coordinates
self._init_from_datafile(*args, **kargs)
self._public_attrs = {"title": str, "filename": str}
self._fromstack = kargs.pop("_fromstack", False) # for use by ImageStack
###################################################################################################################
############################# Properties #### #####################################################################
@property
def _repr_png_(self):
return self._repr_png_private_
@property
def clone(self):
"""Make a copy of this ImageFile."""
new = type(self)(self.image.clone)
for attr in self.__dict__:
if callable(getattr(self, attr)) or attr in ["image", "metadata"] or attr.startswith("_"):
continue
try:
setattr(new, attr, deepcopy(getattr(self, attr)))
except NotImplementedError: # Deepcopying failed, so just copy a reference instead
setattr(new, attr, getattr(self, attr))
return new
@property
def data(self):
"""Alias for image[:]. Equivalence to Stoner.data behaviour."""
return self.image
@data.setter
def data(self, value):
"""Access the image data by data attribute."""
self.image = value
@property
def draw(self):
"""Access the DrawProxy object for accessing the skimage draw sub module."""
return DrawProxy(self.image, self)
@property
def image(self):
"""Access the image data."""
return self._image
@image.setter
def image(self, v):
"""Ensure stored image is always an ImageArray."""
filename = self._image.filename
metadata = self._image.metadata
# ensure setting image goes into the same memory block if from stack
if (
hasattr(self, "_fromstack")
and self._fromstack
and self._image.shape == v.shape
and self._image.dtype == v.dtype
):
self._image[:] = np.copy(v)
elif isinstance(v, np.ndarray):
self._image = np.copy(v).view(ImageArray)
else:
self._image = ImageArray(v)
self.filename = filename
self._image.metadata.update(metadata)
self._image.metadata.update(getattr(v, "metadata", {}))
@property
def mask(self):
"""Get the mask of the underlying IamgeArray."""
return MaskProxy(self)
@mask.setter
def mask(self, value):
"""Set the underlying ImageArray's mask."""
if isinstance(value, ImageFile):
value = value.image
if isinstance(value, MaskProxy):
value = value._mask
self.image.mask = value
###################################################################################################################
############################# Special methods #####################################################################
def __getitem__(self, n):
"""Pass through to ImageArray."""
try:
ret = self.image.__getitem__(n)
if isinstance(ret, ImageArray) and ret.ndim == 2:
retval = self.clone
retval.image = ret
return retval
return ret
except KeyError:
if n not in self.metadata and n in self._image.metadata:
self.metadata[n] = self._image.metadata[n]
return self.metadata.__getitem__(n)
def __setitem__(self, n, v):
"""Pass through to ImageArray."""
if isinstance(n, string_types):
self.metadata.__setitem__(n, v)
else:
self.image.__setitem__(n, v)
def __getstate__(self):
"""Record state for pickling ImageFiles."""
ret = copy(self.__dict__)
ret.update({"metadata": self.metadata})
return ret
def __setstate__(self, state):
"""Write state for unpickling ImageFiles."""
metadata = state.pop("metadata", {})
self.__dict__.update(state)
self.metadata.update(metadata)
def __delitem__(self, n):
"""Pass through to ImageArray."""
try:
self.image.__delitem__(n)
except KeyError:
self.metadata.__delitem__(n)
def __delattr__(self, name):
"""Handle the delete attribute code."""
super().__delattr__(name)
if name in self._public_attrs_real:
del self._public_attrs_real[name]
def __setattr__(self, n, v):
"""Handle setting attributes."""
obj, _ = self._where_attr(n)
if obj is None: # This is a new attribute so note it for preserving
obj = self
if self._where_attr("_public_attrs_real")[0] is self:
self._public_attrs = {n: type(v)}
if obj is self:
super().__setattr__(n, v)
else:
setattr(obj, n, v)
def __add__(self, other):
"""Implement the subtract operator."""
result = self.clone
result = _add_core_(result, other)
return result
def __iadd__(self, other):
"""Implement the inplace subtract operator."""
result = self
result = _add_core_(result, other)
return result
def __floordiv__(self, other):
"""Implement a // operator to do XMCD calculations on a whole image."""
if isinstance(other, ImageFile):
if (
hasattr(other, "polarization")
and hasattr(self, "polarization")
and getattr(self, "polarization") == getattr(other, "polarization")
):
raise ValueError("Can only calculate and XMCD ratio from images of opposite polarization")
if not (hasattr(other, "polarization") and hasattr(self, "polarization")) and Options().warnings:
warn("Calculating XMCD ratio even though one or both image polarizations cannoty be determined.")
if self.image.dtype != other.image.dtype:
raise ValueError(
"Only ImageFiles with the same type of underlying image data can be used to calculate an"
+ "XMCD ratio.Mismatch is {self.image.dtype} vs {other.image.dtype}"
)
if self.image.dtype.kind != "f":
ret = self.clone.convert(float)
other = other.clone.convert(float)
else:
ret = self.clone
plus, minus = self, other
polarization = getattr(self, "polarization", 1)
ret.image = polarization * (plus.image - minus.image) / (plus.image + minus.image)
return ret
result = self
return _floor_div_core_(result, other)
def __truediv__(self, other):
"""Implement the divide operator."""
result = self.clone
result = _div_core_(result, other)