diff --git a/Stoner/Image/core.py b/Stoner/Image/core.py index 7ea9267fa..b40bd4f87 100755 --- a/Stoner/Image/core.py +++ b/Stoner/Image/core.py @@ -409,9 +409,7 @@ def _load_tiff(cls, filename, **kargs): # pylint: disable=unused-argument 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]) + image = io.imread(filename).view(np.uint32)[:, :, 0] # Workaround for issues with more recent pillow tags = img.tag_v2 if 270 in tags: from json import loads diff --git a/Stoner/Image/imagefuncs.py b/Stoner/Image/imagefuncs.py index 3e8ca2a35..664404175 100755 --- a/Stoner/Image/imagefuncs.py +++ b/Stoner/Image/imagefuncs.py @@ -348,47 +348,44 @@ def convert(image, dtype, force_copy=False, uniform=False, normalise=True): if not (dtype_in in _supported_types and dtype in _supported_types): raise ValueError(f"can not convert {dtype_in} to {dtype}.") - kind = dtypeobj.kind - kind_in = dtypeobj_in.kind - itemsize = dtypeobj.itemsize - itemsize_in = dtypeobj_in.itemsize - - if kind == "b": + if dtypeobj.kind == "b": # to binary image - if kind_in in "fi": + if dtypeobj_in.kind in "fi": sign_loss(dtype_in, dtypeobj) prec_loss(dtypeobj_in, dtypeobj) return image > dtype_in(dtype_range[dtype_in][1] / 2) - if kind_in == "b": + if dtypeobj_in.kind == "b": # from binary image, to float and to integer result = np.where(~image, *dtype_range[dtype]) return result - if kind in "ui": + if dtypeobj.kind in "ui": imin = np.iinfo(dtype).min imax = np.iinfo(dtype).max - if kind_in in "ui": + if dtypeobj_in.kind in "ui": imin_in = np.iinfo(dtype_in).min imax_in = np.iinfo(dtype_in).max - if kind_in == "f": + if dtypeobj_in.kind == "f": if np.min(image) < -1.0 or np.max(image) > 1.0: raise ValueError("Images of type float must be between -1 and 1.") - if kind == "f": + if dtypeobj.kind == "f": # floating point -> floating point - if itemsize_in > itemsize: + if dtypeobj_in.itemsize > dtypeobj.itemsize: prec_loss(dtypeobj_in, dtypeobj) return image.astype(dtype) # floating point -> integer prec_loss(dtypeobj_in, dtypeobj) # use float type that can represent output integer type - image = np.array(image, _dtype(itemsize, dtype_in, np.float32, np.float64)) + image = np.array(image, _dtype(dtypeobj.itemsize, dtype_in, np.float32, np.float64)) if not uniform: - if kind == "u": + if dtypeobj.kind == "u": image *= imax - elif itemsize_in <= itemsize and itemsize == 8: # f64->int64 needs care to avoid overruns! + elif ( + dtypeobj_in.itemsize <= dtypeobj.itemsize and dtypeobj.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) @@ -400,7 +397,7 @@ def convert(image, dtype, force_copy=False, uniform=False, normalise=True): image /= 2.0 np.rint(image, out=image) np.clip(image, imin, imax, out=image) - elif kind == "u": + elif dtypeobj.kind == "u": image *= imax + 1 np.clip(image, 0, imax, out=image) else: @@ -409,14 +406,14 @@ def convert(image, dtype, force_copy=False, uniform=False, normalise=True): np.clip(image, imin, imax, out=image) return image.astype(dtype) - if kind == "f": + if dtypeobj.kind == "f": # integer -> floating point - if itemsize_in >= itemsize: + if dtypeobj_in.itemsize >= dtypeobj.itemsize: prec_loss(dtypeobj_in, dtypeobj) # use float type that can exactly represent input integers - image = np.array(image, _dtype(itemsize_in, dtype, np.float32, np.float64)) + image = np.array(image, _dtype(dtypeobj_in.itemsize, dtype, np.float32, np.float64)) if normalise: # normalise floats by maximum value of int type - if kind_in == "u": + if dtypeobj_in.kind == "u": image /= imax_in # DirectX uses this conversion also for signed ints # if imin_in: @@ -427,28 +424,28 @@ def convert(image, dtype, force_copy=False, uniform=False, normalise=True): image /= imax_in - imin_in return image.astype(dtype) - if kind_in == "u": - if kind == "i": + if dtypeobj_in.kind == "u": + if dtypeobj.kind == "i": # unsigned integer -> signed integer - image = im_scale(image, 8 * itemsize_in, 8 * itemsize - 1, dtypeobj_in, dtypeobj) + image = im_scale(image, 8 * dtypeobj_in.itemsize, 8 * dtypeobj.itemsize - 1, dtypeobj_in, dtypeobj) return image.view(dtype) # unsigned integer -> unsigned integer - return im_scale(image, 8 * itemsize_in, 8 * itemsize, dtypeobj_in, dtypeobj) + return im_scale(image, 8 * dtypeobj_in.itemsize, 8 * dtypeobj.itemsize, dtypeobj_in, dtypeobj) - if kind == "u": + if dtypeobj.kind == "u": # signed integer -> unsigned integer sign_loss(dtype_in, dtypeobj) - image = im_scale(image, 8 * itemsize_in - 1, 8 * itemsize, dtypeobj_in, dtypeobj) + image = im_scale(image, 8 * dtypeobj_in.itemsize - 1, 8 * dtypeobj.itemsize, dtypeobj_in, dtypeobj) result = np.empty(image.shape, dtype) np.maximum(image, 0, out=result, dtype=image.dtype, casting="unsafe") return result # signed integer -> signed integer - if itemsize_in > itemsize: - return im_scale(image, 8 * itemsize_in - 1, 8 * itemsize - 1, dtypeobj_in, dtypeobj) - image = image.astype(_dtype2("i", itemsize * 8)) + if dtypeobj_in.itemsize > dtypeobj.itemsize: + return im_scale(image, 8 * dtypeobj_in.itemsize - 1, 8 * dtypeobj.itemsize - 1, dtypeobj_in, dtypeobj) + image = image.astype(_dtype2("i", dtypeobj.itemsize * 8)) image -= imin_in - image = im_scale(image, 8 * itemsize_in, 8 * itemsize, dtypeobj_in, dtypeobj, copy=False) + image = im_scale(image, 8 * dtypeobj_in.itemsize, 8 * dtypeobj.itemsize, dtypeobj_in, dtypeobj, copy=False) image += imin return image.astype(dtype) diff --git a/Stoner/Image/util.py b/Stoner/Image/util.py index 55ebb212b..c6857cc79 100755 --- a/Stoner/Image/util.py +++ b/Stoner/Image/util.py @@ -78,58 +78,63 @@ def _dtype2(kind, bits, itemsize=1): return np.dtype(kind + str(s)) -def _scale(a, n, m, dtypeobj_in, dtypeobj, copy=True): - """Scaleunsigned/positive integers from n to m bits. - - 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: - mnew = int(np.ceil(m / 2) * 2) - if mnew > m: - dtype = "int%s" % mnew - else: - dtype = "uint%s" % mnew - n = int(np.ceil(n / 2) * 2) - msg = "Downcasting %s to %s without scaling because max " "value %s fits in %s" % ( - a.dtype, - dtype, - a.max(), - dtype, - ) +def _scale(image, src_bits, dest_bits, dtypeobj_in, dtypeobj, copy=True): + """Scale unsigned/positive integers from src_bitsto dest_bits_bits. + + Numbers can be represented exactly only if dest_bits_is a multiple of n + Output array is of same kind as input. + """ + if src_bits == dest_bits: # Trivial case no scale necessary + return image.copy() if copy else image + + if src_bits > dest_bits: + return _down_scale(image, src_bits, dest_bits, dtypeobj_in, dtypeobj) + + return _up_scale(image, src_bits, dest_bits, dtypeobj_in, dtypeobj) + + +def _down_scale(image, src_bits, dest_bits, dtypeobj_in, dtypeobj, copy=True): + """Downscale and image from src_bits to dest_bits.""" + kind = image.dtype.kind + if image.max() <= 2**dest_bits: + prefix = ["uint", "int"][dest_bits % 2] + dest_bits += dest_bits % 2 + dtype = f"{prefix}{dest_bits}" + src_bits += src_bits % 2 if Options().warnings: - warn(msg) - return a.astype(_dtype2(kind, m)) - if n == m: - return a.copy() if copy else a - if n > m: - # downscale with precision loss - prec_loss(dtypeobj_in, dtypeobj) - if copy: - b = np.empty(a.shape, _dtype2(kind, m)) - np.floor_divide(a, 2 ** (n - m), out=b, dtype=a.dtype, casting="unsafe") - return b - a //= 2 ** (n - m) - return a - if m % n == 0: - # exact upscale to a multiple of n bits + warn( + f"Downcasting {image.dtype} to {dtype} without scaling" + + f"because max value {image.max()} fits in {dtype}." + ) + return image.astype(_dtype2(kind, dest_bits)) + prec_loss(dtypeobj_in, dtypeobj) + if copy: + image2 = np.empty(image.shape, _dtype2(kind, dest_bits)) + else: + image2 = image + np.floor_divide(image, 2 ** (src_bits - dest_bits), out=image2, dtype=image.dtype, casting="unsafe") + return image2 + + +def _up_scale(image, src_bits, dest_bits, dtypeobj_in, dtypeobj, copy=True): + """Upscale an image from src_bits to dest_bits.""" + kind = image.dtype.kind + if dest_bits % src_bits == 0: + # exact upscale to a multiple of src_bitsbits if copy: - b = np.empty(a.shape, _dtype2(kind, m)) - np.multiply(a, (2**m - 1) // (2**n - 1), out=b, dtype=b.dtype) - return b - a = np.array(a, _dtype2(kind, m, a.dtype.itemsize), copy=False) - a *= (2**m - 1) // (2**n - 1) - return a - # upscale to a multiple of n bits, + image2 = np.empty(image.shape, _dtype2(kind, dest_bits)) + else: + image2 = np.array(image, _dtype2(kind, dest_bits, image.dtype.itemsize), copy=False) + np.multiply(image, (2**dest_bits - 1) // (2**src_bits - 1), out=image2, dtype=image2.dtype) + return image2 + # upscale to a multiple of src_bitsbits, # then downscale with precision loss prec_loss(dtypeobj_in, dtypeobj) - o = (m // n + 1) * n + upscale_factor = (dest_bits // src_bits + 1) * src_bits if copy: - b = np.empty(a.shape, _dtype2(kind, o)) - np.multiply(a, (2**o - 1) // (2**n - 1), out=b, dtype=b.dtype) - b //= 2 ** (o - m) - return b - a = np.array(a, _dtype2(kind, o, a.dtype.itemsize), copy=False) - a *= (2**o - 1) // (2**n - 1) - a //= 2 ** (o - m) - return a + image2 = np.empty(image.shape, _dtype2(kind, upscale_factor)) + else: + image2 = image + np.multiply(image, (2**upscale_factor - 1) // (2**src_bits - 1), out=image2, dtype=image2.dtype) + image2 //= 2 ** (upscale_factor - dest_bits) + return image2 diff --git a/tests/Stoner/Image/test_core.py b/tests/Stoner/Image/test_core.py index ef040258b..d9ac11f50 100755 --- a/tests/Stoner/Image/test_core.py +++ b/tests/Stoner/Image/test_core.py @@ -107,12 +107,12 @@ def test_load_save_all(): iml = ImageFile(path.join(tmpdir, "kermit-{}.tiff".format(fmt))) del iml["Loaded from"] assert ims[fmt] == iml, f"{ims[fmt].metadata} {iml.metadata}" - iml = ImageFile(path.join(tmpdir, "kermit-{}.npy".format(fmt))) + iml = ImageFile(path.join(tmpdir, f"kermit-{fmt}.npy")) del iml["Loaded from"] - assert np.all(ims[fmt].data == iml.data), "Round tripping npy with format {} failed".format(fmt) + assert np.all(ims[fmt].data == iml.data), f"Round tripping npy with format {fmt} failed" if fmt != "uint16": - im = ImageFile(path.join(tmpdir, "kermit-nometadata-{}.tiff".format(fmt))) - assert np.all(im.data == ims[fmt].data), "Loading from tif without metadata failed for {}".format(fmt) + im = ImageFile(path.join(tmpdir, f"kermit-nometadata-{fmt}.tiff")) + assert np.all(im.data == ims[fmt].data), f"Loading from tif without metadata failed for {fmt}" shutil.rmtree(tmpdir) _ = image.convert("uint8")