Skip to content

Commit

Permalink
Improve test coverage and fix issues in ImageFile.convert
Browse files Browse the repository at this point in the history
  • Loading branch information
gb119 committed Jan 10, 2021
1 parent 2d8c55f commit 3300391
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 100 deletions.
159 changes: 79 additions & 80 deletions Stoner/Image/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ def __add_core__(result, other):
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
Expand Down Expand Up @@ -894,19 +905,6 @@ def __delattr__(self, name):
if name in self._public_attrs_real:
del self._public_attrs_real[name]

# def __getattr__(self, n):
# """Handle attriobute access."""
# obj, attr = self._where_attr(n)
# if obj is None:
# raise AttributeError(f"{n} is not an attribute of {type(self)}")
# if obj is self:
# return attr
# raise AttributeError(f"{n} is not an attribute of {type(self)}")

# if callable(attr): # we have a method
# attr = self._func_generator(attr) # modiy so that we can change image in place
# return attr

def __setattr__(self, n, v):
"""Handle setting attributes."""
obj, attr = self._where_attr(n)
Expand Down Expand Up @@ -976,8 +974,9 @@ def __invert__(self):

def __floordiv__(self, other):
"""Calculate and XMCD ratio on the images."""
if not isinstance(other, ImageFile):
return NotImplemented
if not isinstance(other, type(self)): # Only do XMCD type operations on ImageFiles of the same type
result = self
return __floor_div_core__(result, other)
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."
Expand Down Expand Up @@ -1044,71 +1043,71 @@ def _init_from_datafile(self, *args, **kargs):
self["x_vector"] = np.unique(X)
self["y_vector"] = np.unique(Y)

def _func_generator(self, workingfunc):
"""Make wrappers for ImageFile functions.
Notes:
The wrapped functions take additional keyword arguments that are stripped off from the call.
Keyword Arguments:
_box(:py:meth:`Stoner.ImageArray.crop` arguments):
Crops the image first before calling the parent method.
_(bool, None):
Controls whether a :py:class:`ImageArray` return will be substituted for the current
:py:class:`ImageArray`.
* True: - all ImageArray return types are substituted.
* False (default) - Imagearray return types are substituted if they are the same size as the original
* None - A copy of the current object is taken and the returned ImageArray provides the data.
"""
# Avoid PEP257/black issue

@wraps(workingfunc)
def gen_func(*args, **kargs):
"""Wrap a called method to capture the result back into the calling object."""
box = kargs.pop("_box", False)
if len(args) > 0:
args = list(args)
for ix, a in enumerate(args):
if isinstance(a, ImageFile):
if isinstance(box, bool) and not box:
args[ix] = a.image
else:
args[ix] = a.image[self.image._box(box)]
if getattr(workingfunc, "changes_size", False) and "_" not in kargs:
# special case for common function crop which will change the array shape
force = True
else:
force = kargs.pop("_", False)
r = workingfunc(*args, **kargs)
if (
force is not None
and isinstance(r, ImageArray)
and (force or r.shape == self.image[self.image._box(box)].shape)
):
# Enure that we've captured any metadata added inside the working function
self.metadata.update(r.metadata)
# Now swap the iamge in, but keep the metadata
r.metadata = self.metadata
filename = self.filename
self.image = r
self.filename = filename

return self
if (force is None or not force) and isinstance(r, np.ndarray) and r.ndim == 2:
ret = self.clone
if hasattr(r, "metadata"):
ret.metadata.update(r.metadata)
# Now swap the iamge in, but keep the metadata
metadata = ret.metadata
filename = ret.filename
ret.image = np.atleast_2d(r).view(type(self.image))
ret.filename = filename
ret.metadata = metadata
return ret
return r

return fix_signature(gen_func, workingfunc)
# def _func_generator(self, workingfunc):
# """Make wrappers for ImageFile functions.

# Notes:
# The wrapped functions take additional keyword arguments that are stripped off from the call.

# Keyword Arguments:
# _box(:py:meth:`Stoner.ImageArray.crop` arguments):
# Crops the image first before calling the parent method.
# _(bool, None):
# Controls whether a :py:class:`ImageArray` return will be substituted for the current
# :py:class:`ImageArray`.

# * True: - all ImageArray return types are substituted.
# * False (default) - Imagearray return types are substituted if they are the same size as the original
# * None - A copy of the current object is taken and the returned ImageArray provides the data.
# """
# # Avoid PEP257/black issue

# @wraps(workingfunc)
# def gen_func(*args, **kargs):
# """Wrap a called method to capture the result back into the calling object."""
# box = kargs.pop("_box", False)
# if len(args) > 0:
# args = list(args)
# for ix, a in enumerate(args):
# if isinstance(a, ImageFile):
# if isinstance(box, bool) and not box:
# args[ix] = a.image
# else:
# args[ix] = a.image[self.image._box(box)]
# if getattr(workingfunc, "changes_size", False) and "_" not in kargs:
# # special case for common function crop which will change the array shape
# force = True
# else:
# force = kargs.pop("_", False)
# r = workingfunc(*args, **kargs)
# if (
# force is not None
# and isinstance(r, ImageArray)
# and (force or r.shape == self.image[self.image._box(box)].shape)
# ):
# # Enure that we've captured any metadata added inside the working function
# self.metadata.update(r.metadata)
# # Now swap the iamge in, but keep the metadata
# r.metadata = self.metadata
# filename = self.filename
# self.image = r
# self.filename = filename

# return self
# if (force is None or not force) and isinstance(r, np.ndarray) and r.ndim == 2:
# ret = self.clone
# if hasattr(r, "metadata"):
# ret.metadata.update(r.metadata)
# # Now swap the iamge in, but keep the metadata
# metadata = ret.metadata
# filename = ret.filename
# ret.image = np.atleast_2d(r).view(type(self.image))
# ret.filename = filename
# ret.metadata = metadata
# return ret
# return r

# return fix_signature(gen_func, workingfunc)

def _load(self, filename, *args, **kargs):
"""Load an ImageFile by calling the ImageArray method instead."""
Expand Down
10 changes: 8 additions & 2 deletions Stoner/Image/imagefuncs.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,12 +390,18 @@ def convert(image, dtype, force_copy=False, uniform=False, normalise=True):
if not uniform:
if kind == "u":
image *= imax
elif itemsize_in <= itemsize and itemsize == 8: # f64->int64 needs care to avoid overruns!
image *= 2 ** 54 # float64 has 52bits of mantissa, so this will avoid precission loss for a +/-1 range
np.rint(image, out=image)
image = image.astype(dtype)
np.clip(image, -(2 ** 54), 2 ** 54 - 1, out=image)
image *= 512
else:
image *= imax - imin
image -= 1.0
image /= 2.0
np.rint(image, out=image)
np.clip(image, imin, imax, out=image)
np.rint(image, out=image)
np.clip(image, imin, imax, out=image)
elif kind == "u":
image *= imax + 1
np.clip(image, 0, imax, out=image)
Expand Down
2 changes: 1 addition & 1 deletion Stoner/Image/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def _scale(a, n, m, dtypeobj_in, dtypeobj, copy=True):
Numbers can be represented exactly only if m is a multiple of n
Output array is of same kind as input."""
kind = a.dtype.kind
if n > m and a.max() > 2 ** m:
if n > m and a.max() <= 2 ** m:
mnew = int(np.ceil(m / 2) * 2)
if mnew > m:
dtype = "int%s" % mnew
Expand Down
2 changes: 0 additions & 2 deletions Stoner/Image/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,5 @@ def get_mask(self):
vertices = np.array(self.vertices)
meth = getattr(self, f"draw_{self.obj}", lambda x: ([], []))
rr, cc = meth(vertices)
if len(rr) == 0:
breakpoint()
mask[rr, cc] = ~self.invert
return mask
2 changes: 1 addition & 1 deletion Stoner/Util.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ def hysteresis_correct(data, **kargs):
the background suceptibility, offset in moment, co-ercive fields and saturation magnetisation.
"""
if isinstance(data, DataFile):
cls = data.__class__
cls = type(data)
else:
cls = make_Data(None)
data = cls(data)
Expand Down
37 changes: 35 additions & 2 deletions tests/Stoner/Image/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@ def test_load_save_all():
shutil.rmtree(tmpdir)
i8=image.convert("uint8")

def test_conversion_bitness():
pth=path.join(__home__,"..")
datadir=path.join(pth,"sample-data")
image=ImageFile(path.join(datadir,"kermit.png"))
img=image.clone
img2=img.clone//256
img.convert("uint8") #Down conversion with prevision loss
assert np.all(img.image==img2.image)
img.convert("int16") #Upconversion with unequal bit lengths
assert img.max()==32767 and img.min()==0
img3=img2.clone.convert("uint8") # Downconversion without precision loss
assert np.all(img2.image==img3.image)


def test_load_from_ImageFile():
#uses the ImageFile.im attribute to set up ImageArray. Memory overlaps
imfi = ImageFile(selfarr)
Expand Down Expand Up @@ -164,6 +178,14 @@ def test_clone():
assert c.userxyz == 123
c.userxyz = 234
assert c.userxyz != selfimarr.userxyz
img=ImageFile(np.zeros((100,100)))
img.abcd="Hello"
img2=img.clone
assert img2.abcd=="Hello"
del img["Loaded from"]
assert len(img.metadata)==0
del img.abcd
assert not hasattr(img,"abcd")

def test_metadata():
assert isinstance(selfimarr.metadata,typeHintedDict)
Expand Down Expand Up @@ -240,8 +262,8 @@ def test_crop():
def test_asint():
ui = selfimarr.asint()
assert ui.dtype==np.uint16
intarr = np.array([[27312, 43319, 60132, 22149],
[ 3944, 9439, 22571, 64980],
intarr = np.array([[27311, 43318, 60131, 22148],
[ 3943, 9439, 22571, 64979],
[19556, 60082, 48378, 50169]], dtype=np.uint16)
assert np.array_equal(ui,intarr)

Expand Down Expand Up @@ -395,6 +417,17 @@ def test_draw():
attrs=[x for x in dir(i.draw) if not x.startswith("_")]
expected = 21
assert len(attrs)==expected,"Directory of DrawProxy failed"
i2=i.clone
i2.draw.circle(10,100,10,value=1)
assert i2.image.sum()==305
i2=i.clone.draw.circle_perimeter_aa(100,100,10,value=1)
assert np.isclose(i2.image.sum(),56.5657137141714)
i2=i.clone.draw.random_shapes(i.shape,1,1,30,100)
assert i2.label()[1]==1
i2=i.clone.draw.rectangle_perimeter(100,100,20,10,value=1)
assert i2.image.sum()==60.0
i2.mask.threshold(0.5)
assert i2.mask.sum()==60

def test_operators():
i=ImageFile(np.zeros((10,10)))
Expand Down
37 changes: 25 additions & 12 deletions tests/Stoner/Image/test_wdigets.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,20 @@
import time
import numpy as np

def _event(image,name,**kargs):
def _event(image,names,**kargs):
"""Make a fake event."""
select=image._image._select
event=Event("fake",select.fig.canvas)
for k,v in kargs.items():
setattr(event,k,v)
try:
getattr(select,name)(event)
except Exception as err:
breakpoint()
pass
if not isinstance(names,list):
names=[names]
for name in names:
for k,v in kargs.items():
setattr(event,k,v)
try:
getattr(select,name)(event)
except Exception as err:
breakpoint()
pass


def _trigger(image):
Expand Down Expand Up @@ -57,15 +60,20 @@ def _trigger4(image):

def _trigger5(image,mode):
time.sleep(1)
_event(image,"on_click",xdata=50,ydata=25,button=1,dblclick=False)
_event(image,"on_click",xdata=75,ydata=50,button=1,dblclick=False)
_event(image,["draw","on_click"],xdata=50,ydata=25,button=1,dblclick=False)
_event(image,["draw","on_click"],xdata=75,ydata=50,button=1,dblclick=False)
_event(image,"keypress",xdata=50,ydata=75,key=mode.lower()[0])
if mode=="c": # add some extra points:
_event(image,"on_click",xdata=30,ydata=40,button=1,dblclick=False)
_event(image,"on_click",xdata=30,ydata=30,button=1,dblclick=False)
_event(image,["draw","on_click"],xdata=30,ydata=40,button=1,dblclick=False)
_event(image,["draw","on_click"],xdata=30,ydata=30,button=1,dblclick=False)
_event(image,"keypress",xdata=50,ydata=75,key="i")
_event(image,"keypress",xdata=50,ydata=75,key="enter")

def _trigger6(image,mode):
time.sleep(1)
_event(image,["draw","on_click"],xdata=50,ydata=25,button=1,dblclick=False)
_event(image,["draw","on_click"],xdata=75,ydata=50,button=1,dblclick=False)
_event(image,"keypress",xdata=50,ydata=75,key="escape")


def test_profile_line():
Expand Down Expand Up @@ -122,6 +130,11 @@ def test_masking_select():
result = img.mean()
assert np.isclose(result,15745.5853061)
img.mask=False
thread=threading.Thread(target=_trigger6,args=(img,"c"))
thread.start()
img.mask.select()
result = img.mean()
assert np.isclose(result,27715.3245)


if __name__=="__main__": # Run some tests manually to allow debugging
Expand Down

0 comments on commit 3300391

Please sign in to comment.