Skip to content

Instantly share code, notes, and snippets.

@thejohnhoffer
Created July 26, 2018 18:42
Show Gist options
  • Save thejohnhoffer/8ada13099c3a36ced0ed2dfbae65bea7 to your computer and use it in GitHub Desktop.
Save thejohnhoffer/8ada13099c3a36ced0ed2dfbae65bea7 to your computer and use it in GitHub Desktop.
from warnings import warn
from multiprocessing.pool import ThreadPool
from io import BytesIO
import numpy as np
import boto3
import cv2
import os
######
# Copyright (C) 2011, the scikit-image team
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
# 3. Neither the name of skimage nor the names of its contributors may be
# used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# From skikit-image: https://github.com/scikit-image/scikit-image/tree/cc7b116cdbb9f9981d4c7b9cd01a201489e4dc6e # noqa: E501
# skimage.util.dtype._integer_types|_integer_ranges|dtype_range
_integer_types = (np.byte, np.ubyte, # 8 bits
np.short, np.ushort, # 16 bits
np.intc, np.uintc, # 16 or 32 or 64 bits
np.int_, np.uint, # 32 or 64 bits
np.longlong, np.ulonglong) # 64 bits
_integer_ranges = {t: (np.iinfo(t).min, np.iinfo(t).max)
for t in _integer_types}
dtype_range = {np.bool_: (False, True),
np.bool8: (False, True),
np.float16: (-1, 1),
np.float32: (-1, 1),
np.float64: (-1, 1)}
dtype_range.update(_integer_ranges)
# skimage.exposure.DTYPE_RANGE
DTYPE_RANGE = dtype_range.copy()
DTYPE_RANGE.update((d.__name__, limits) for d, limits in dtype_range.items())
DTYPE_RANGE.update({'uint10': (0, 2 ** 10 - 1),
'uint12': (0, 2 ** 12 - 1),
'uint14': (0, 2 ** 14 - 1),
'bool': dtype_range[np.bool_],
'float': dtype_range[np.float64]})
# skimage.util.dtype._supported_types
_supported_types = list(dtype_range.keys())
# skimage.util.dtype.dtype_limits
def dtype_limits(image, clip_negative=False):
"""Return intensity limits, i.e. (min, max) tuple, of the image's dtype.
Parameters
----------
image : ndarray
Input image.
clip_negative : bool, optional
If True, clip the negative range (i.e. return 0 for min intensity)
even if the image dtype allows negative values.
Returns
-------
imin, imax : tuple
Lower and upper intensity limits.
"""
imin, imax = dtype_range[image.dtype.type]
if clip_negative:
imin = 0
return imin, imax
# skimage.exposure.exposure._assert_non_negative
def _assert_non_negative(image):
if np.any(image < 0):
raise ValueError('Image Correction methods work correctly only on '
'images with non-negative values. Use '
'skimage.exposure.rescale_intensity.')
# skimage.exposure.exposure.intensity_range
def intensity_range(image, range_values='image', clip_negative=False):
"""Return image intensity range (min, max) based on desired value type.
Parameters
----------
image : array
Input image.
range_values : str or 2-tuple
The image intensity range is configured by this parameter.
The possible values for this parameter are enumerated below.
'image'
Return image min/max as the range.
'dtype'
Return min/max of the image's dtype as the range.
dtype-name
Return intensity range based on desired `dtype`. Must be valid key
in `DTYPE_RANGE`. Note: `image` is ignored for this range type.
2-tuple
Return `range_values` as min/max intensities. Note that there's no
reason to use this function if you just want to specify the
intensity range explicitly. This option is included for functions
that use `intensity_range` to support all desired range types.
clip_negative : bool
If True, clip the negative range (i.e. return 0 for min intensity)
even if the image dtype allows negative values.
"""
if range_values == 'dtype':
range_values = image.dtype.type
if range_values == 'image':
i_min = np.min(image)
i_max = np.max(image)
elif range_values in DTYPE_RANGE:
i_min, i_max = DTYPE_RANGE[range_values]
if clip_negative:
i_min = 0
else:
i_min, i_max = range_values
return i_min, i_max
# skimage.util.dtype.convert
def convert(image, dtype, force_copy=False, uniform=False):
"""
Convert an image to the requested data-type.
Warnings are issued in case of precision loss, or when negative values
are clipped during conversion to unsigned integer types (sign loss).
Floating point values are expected to be normalized and will be clipped
to the range [0.0, 1.0] or [-1.0, 1.0] when converting to unsigned or
signed integers respectively.
Numbers are not shifted to the negative side when converting from
unsigned to signed integer types. Negative values will be clipped when
converting to unsigned integers.
Parameters
----------
image : ndarray
Input image.
dtype : dtype
Target data-type.
force_copy : bool, optional
Force a copy of the data, irrespective of its current dtype.
uniform : bool, optional
Uniformly quantize the floating point range to the integer range.
By default (uniform=False) floating point values are scaled and
rounded to the nearest integers, which minimizes back and forth
conversion errors.
References
----------
.. [1] DirectX data conversion rules.
http://msdn.microsoft.com/en-us/library/windows/desktop/dd607323%28v=vs.85%29.aspx
.. [2] Data Conversions. In "OpenGL ES 2.0 Specification v2.0.25",
pp 7-8. Khronos Group, 2010.
.. [3] Proper treatment of pixels as integers. A.W. Paeth.
In "Graphics Gems I", pp 249-256. Morgan Kaufmann, 1990.
.. [4] Dirty Pixels. J. Blinn. In "Jim Blinn's corner: Dirty Pixels",
pp 47-57. Morgan Kaufmann, 1998.
"""
image = np.asarray(image)
dtypeobj_in = image.dtype
dtypeobj_out = np.dtype(dtype)
dtype_in = dtypeobj_in.type
dtype_out = dtypeobj_out.type
kind_in = dtypeobj_in.kind
kind_out = dtypeobj_out.kind
itemsize_in = dtypeobj_in.itemsize
itemsize_out = dtypeobj_out.itemsize
# Below, we do an `issubdtype` check. Its purpose is to find out
# whether we can get away without doing any image conversion. This happens
# when:
#
# - the output and input dtypes are the same or
# - when the output is specified as a type, and the input dtype
# is a subclass of that type (e.g. `np.floating` will allow
# `float32` and `float64` arrays through)
type_out = dtype if isinstance(dtype, type) else dtypeobj_out
if np.issubdtype(dtypeobj_in, type_out):
if force_copy:
image = image.copy()
return image
if not (dtype_in in _supported_types and dtype_out in _supported_types):
raise ValueError("Can not convert from {} to {}."
.format(dtypeobj_in, dtypeobj_out))
def sign_loss():
warn("Possible sign loss when converting negative image of type "
"{} to positive image of type {}."
.format(dtypeobj_in, dtypeobj_out))
def prec_loss():
warn("Possible precision loss when converting from {} to {}"
.format(dtypeobj_in, dtypeobj_out))
def _dtype_itemsize(itemsize, *dtypes):
# Return first of `dtypes` with itemsize greater than `itemsize`
return next(dt for dt in dtypes if np.dtype(dt).itemsize >= itemsize)
def _dtype_bits(kind, bits, itemsize=1):
# Return dtype of `kind` that can store a `bits` wide unsigned int
def compare(x, y, kind='u'):
if kind == 'u':
return x <= y
else:
return x < y
s = next(i for i in (itemsize, ) + (2, 4, 8) if compare(bits, i * 8,
kind=kind))
return np.dtype(kind + str(s))
def _scale(a, n, m, copy=True):
"""Scale an array of unsigned/positive integers from `n` to `m` bits.
Numbers can be represented exactly only if `m` is a multiple of `n`.
Parameters
----------
a : ndarray
Input image array.
n : int
Number of bits currently used to encode the values in `a`.
m : int
Desired number of bits to encode the values in `out`.
copy : bool, optional
If True, allocates and returns new array. Otherwise, modifies
`a` in place.
Returns
-------
out : array
Output image array. Has the same kind as `a`.
"""
kind = a.dtype.kind
if n > m and a.max() < 2 ** m:
mnew = int(np.ceil(m / 2) * 2)
if mnew > m:
dtype = "int{}".format(mnew)
else:
dtype = "uint{}".format(mnew)
n = int(np.ceil(n / 2) * 2)
warn("Downcasting {} to {} without scaling because max "
"value {} fits in {}".format(a.dtype, dtype, a.max(), dtype))
return a.astype(_dtype_bits(kind, m))
elif n == m:
return a.copy() if copy else a
elif n > m:
# downscale with precision loss
prec_loss()
if copy:
b = np.empty(a.shape, _dtype_bits(kind, m))
np.floor_divide(a, 2**(n - m), out=b, dtype=a.dtype,
casting='unsafe')
return b
else:
a //= 2**(n - m)
return a
elif m % n == 0:
# exact upscale to a multiple of `n` bits
if copy:
b = np.empty(a.shape, _dtype_bits(kind, m))
np.multiply(a, (2**m - 1) // (2**n - 1), out=b, dtype=b.dtype)
return b
else:
a = a.astype(_dtype_bits(kind, m, a.dtype.itemsize),
copy=False)
a *= (2**m - 1) // (2**n - 1)
return a
else:
# upscale to a multiple of `n` bits,
# then downscale with precision loss
prec_loss()
o = (m // n + 1) * n
if copy:
b = np.empty(a.shape, _dtype_bits(kind, o))
np.multiply(a, (2**o - 1) // (2**n - 1), out=b, dtype=b.dtype)
b //= 2**(o - m)
return b
else:
a = a.astype(_dtype_bits(kind, o, a.dtype.itemsize),
copy=False)
a *= (2**o - 1) // (2**n - 1)
a //= 2**(o - m)
return a
if kind_in in 'ui':
imin_in = np.iinfo(dtype_in).min
imax_in = np.iinfo(dtype_in).max
if kind_out in 'ui':
imin_out = np.iinfo(dtype_out).min
imax_out = np.iinfo(dtype_out).max
# any -> binary
if kind_out == 'b':
if kind_in in "fi":
sign_loss()
prec_loss()
return image > dtype_in(dtype_range[dtype_in][1] / 2)
# binary -> any
if kind_in == 'b':
result = image.astype(dtype_out)
if kind_out != 'f':
result *= dtype_out(dtype_range[dtype_out][1])
return result
# float -> any
if kind_in == '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_out == 'f':
# float -> float
if itemsize_in > itemsize_out:
prec_loss()
return image.astype(dtype_out)
# floating point -> integer
prec_loss()
# use float type that can represent output integer type
computation_type = _dtype_itemsize(itemsize_out, dtype_in,
np.float32, np.float64)
if not uniform:
if kind_out == 'u':
image_out = np.multiply(image, imax_out,
dtype=computation_type)
else:
image_out = np.multiply(image, (imax_out - imin_out) / 2,
dtype=computation_type)
image_out -= 1.0 / 2.
np.rint(image_out, out=image_out)
np.clip(image_out, imin_out, imax_out, out=image_out)
elif kind_out == 'u':
image_out = np.multiply(image, imax_out + 1,
dtype=computation_type)
np.clip(image_out, 0, imax_out, out=image_out)
else:
image_out = np.multiply(image, (imax_out - imin_out + 1.0) / 2.0,
dtype=computation_type)
np.floor(image_out, out=image_out)
np.clip(image_out, imin_out, imax_out, out=image_out)
return image_out.astype(dtype_out)
# signed/unsigned int -> float
if kind_out == 'f':
if itemsize_in >= itemsize_out:
prec_loss()
# use float type that can exactly represent input integers
computation_type = _dtype_itemsize(itemsize_in, dtype_out,
np.float32, np.float64)
if kind_in == 'u':
# using np.divide or np.multiply doesn't copy the data
# until the computation time
image = np.multiply(image, 1. / imax_in,
dtype=computation_type)
# DirectX uses this conversion also for signed ints
# if imin_in:
# np.maximum(image, -1.0, out=image)
else:
image = np.add(image, 0.5, dtype=computation_type)
image *= 2 / (imax_in - imin_in)
return np.asarray(image, dtype_out)
# unsigned int -> signed/unsigned int
if kind_in == 'u':
if kind_out == 'i':
# unsigned int -> signed int
image = _scale(image, 8 * itemsize_in, 8 * itemsize_out - 1)
return image.view(dtype_out)
else:
# unsigned int -> unsigned int
return _scale(image, 8 * itemsize_in, 8 * itemsize_out)
# signed int -> unsigned int
if kind_out == 'u':
sign_loss()
image = _scale(image, 8 * itemsize_in - 1, 8 * itemsize_out)
result = np.empty(image.shape, dtype_out)
np.maximum(image, 0, out=result, dtype=image.dtype, casting='unsafe')
return result
# signed int -> signed int
if itemsize_in > itemsize_out:
return _scale(image, 8 * itemsize_in - 1, 8 * itemsize_out - 1)
image = image.astype(_dtype_bits('i', itemsize_out * 8))
image -= imin_in
image = _scale(image, 8 * itemsize_in, 8 * itemsize_out, copy=False)
image += imin_out
return image.astype(dtype_out)
# skimage.util.dtype.img_as_float
def img_as_float(image, force_copy=False):
"""Convert an image to floating point format.
This function is similar to `img_as_float64`, but will not convert
lower-precision floating point arrays to `float64`.
Parameters
----------
image : ndarray
Input image.
force_copy : bool, optional
Force a copy of the data, irrespective of its current dtype.
Returns
-------
out : ndarray of float
Output image.
Notes
-----
The range of a floating point image is [0.0, 1.0] or [-1.0, 1.0] when
converting from unsigned or signed datatypes, respectively.
If the input image has a float type, intensity values are not modified
and can be outside the ranges [0.0, 1.0] or [-1.0, 1.0].
"""
return convert(image, np.floating, force_copy)
# skimage.exposure.exposure.rescale_intensity
def rescale_intensity(image, in_range='image', out_range='dtype'):
"""Return image after stretching or shrinking its intensity levels.
The desired intensity range of the input and output, `in_range` and
`out_range` respectively, are used to stretch or shrink the intensity range
of the input image. See examples below.
Parameters
----------
image : array
Image array.
in_range, out_range : str or 2-tuple
Min and max intensity values of input and output image.
The possible values for this parameter are enumerated below.
'image'
Use image min/max as the intensity range.
'dtype'
Use min/max of the image's dtype as the intensity range.
dtype-name
Use intensity range based on desired `dtype`. Must be valid key
in `DTYPE_RANGE`.
2-tuple
Use `range_values` as explicit min/max intensities.
Returns
-------
out : array
Image array after rescaling its intensity. This image is the same dtype
as the input image.
See Also
--------
equalize_hist
Examples
--------
By default, the min/max intensities of the input image are stretched to
the limits allowed by the image's dtype, since `in_range` defaults to
'image' and `out_range` defaults to 'dtype':
>>> image = np.array([51, 102, 153], dtype=np.uint8)
>>> rescale_intensity(image)
array([ 0, 127, 255], dtype=uint8)
It's easy to accidentally convert an image dtype from uint8 to float:
>>> 1.0 * image
array([ 51., 102., 153.])
Use `rescale_intensity` to rescale to the proper range for float dtypes:
>>> image_float = 1.0 * image
>>> rescale_intensity(image_float)
array([ 0. , 0.5, 1. ])
To maintain the low contrast of the original, use the `in_range` parameter:
>>> rescale_intensity(image_float, in_range=(0, 255))
array([ 0.2, 0.4, 0.6])
If the min/max value of `in_range` is more/less than the min/max image
intensity, then the intensity levels are clipped:
>>> rescale_intensity(image_float, in_range=(0, 102))
array([ 0.5, 1. , 1. ])
If you have an image with signed integers but want to rescale the image to
just the positive range, use the `out_range` parameter:
>>> image = np.array([-10, 0, 10], dtype=np.int8)
>>> rescale_intensity(image, out_range=(0, 127))
array([ 0, 63, 127], dtype=int8)
"""
dtype = image.dtype.type
imin, imax = intensity_range(image, in_range)
omin, omax = intensity_range(image, out_range, clip_negative=(imin >= 0))
image = np.clip(image, imin, imax)
image = (image - imin) / float(imax - imin)
return np.array(image * (omax - omin) + omin, dtype=dtype)
# skimage.exposure.exposure.adjust_gamma
def adjust_gamma(image, gamma=1, gain=1):
"""Performs Gamma Correction on the input image.
Also known as Power Law Transform.
This function transforms the input image pixelwise according to the
equation ``O = I**gamma`` after scaling each pixel to the range 0 to 1.
Parameters
----------
image : ndarray
Input image.
gamma : float
Non negative real number. Default value is 1.
gain : float
The constant multiplier. Default value is 1.
Returns
-------
out : ndarray
Gamma corrected output image.
See Also
--------
adjust_log
Notes
-----
For gamma greater than 1, the histogram will shift towards left and
the output image will be darker than the input image.
For gamma less than 1, the histogram will shift towards right and
the output image will be brighter than the input image.
References
----------
.. [1] http://en.wikipedia.org/wiki/Gamma_correction
Examples
--------
>>> from skimage import data, exposure, img_as_float
>>> image = img_as_float(data.moon())
>>> gamma_corrected = exposure.adjust_gamma(image, 2)
>>> # Output is darker for gamma > 1
>>> image.mean() > gamma_corrected.mean()
True
"""
_assert_non_negative(image)
dtype = image.dtype.type
if gamma < 0:
raise ValueError("Gamma should be a non-negative real number.")
scale = float(dtype_limits(image, True)[1] - dtype_limits(image, True)[0])
out = ((image / scale) ** gamma) * scale * gain
return dtype(out)
# blend.py
#
# from Minerva Library version 0.0.1
# https://github.com/sorgerlab/minerva-lib-python/
###
def composite_channel(target, image, color, range_min, range_max, out=None):
''' Render _image_ in pseudocolor and composite into _target_
By default, a new output array will be allocated to hold
the result of the composition operation. To update _target_
in place instead, specify the same array for _target_ and _out_.
Args:
target: Numpy array containing composition target image
image: Numpy array of image to render and composite
color: Color as r, g, b float array within 0, 1
range_min: Threshhold range minimum, float within 0, 1
range_max: Threshhold range maximum, float within 0, 1
out: Optional output numpy array in which to place the result.
Returns:
A numpy array with the same shape as the composited image.
If an output array is specified, a reference to _out_ is returned.
'''
if out is None:
out = target.copy()
# Rescale the new channel to a float64 between 0 and 1
f64_range = (range_min, range_max)
f64_image = img_as_float(image)
f64_image = rescale_intensity(f64_image, f64_range)
# Colorize and add the new channel to composite image
for i, component in enumerate(color):
out[:, :, i] += f64_image * component
return out
def composite_channels(channels):
'''Render each image in _channels_ additively into a composited image
Args:
channels: List of dicts for channels to blend. Each dict in the
list must have the following rendering settings:
{
image: Numpy 2D image data of any type
color: Color as r, g, b float array within 0, 1
min: Threshhold range minimum, float within 0, 1
max: Threshhold range maximum, float within 0, 1
}
Returns:
For input images with shape `(n,m)`,
returns a float32 RGB color image with shape
`(n,m,3)` and values in the range 0 to 1
'''
num_channels = len(channels)
# Must be at least one channel
if num_channels < 1:
raise ValueError('At least one channel must be specified')
# Ensure that dimensions of all channels are equal
shape = channels[0]['image'].shape
for channel in channels:
if channel['image'].shape != shape:
raise ValueError('All channel images must have equal dimensions')
# Shape of 3 color image
shape_color = shape + (3,)
# Final buffer for blending
out_buffer = np.zeros(shape_color, dtype=np.float32)
# rescaled images and normalized colors
for channel in channels:
# Add all three channels to output buffer
args = map(channel.get, ['image', 'color', 'min', 'max'])
composite_channel(out_buffer, *args, out=out_buffer)
# Return gamma correct image within 0, 1
np.clip(out_buffer, 0, 1, out=out_buffer)
return adjust_gamma(out_buffer, 1 / 2.2)
######
# Entrypoint
###
def _s3_get(bucket, key):
'''Fetch a specific PNG from S3 and decode'''
obj = boto3.resource('s3').Object(bucket, key)
body = obj.get()['Body']
data = body.read()
stream = BytesIO(data)
image = cv2.imdecode(np.fromstring(stream.getvalue(), dtype=np.uint8), 0)
return image
def format_input(args):
''' Combine all parameters
'''
image_, color_, range_ = args
return {
'image': image_,
'color': color_,
'min': range_[0],
'max': range_[1],
}
def main():
""" Crop a region
"""
out = './output'
os.makedirs(out)
out_path_format = out + '/T{0:}-Z{2:}-L{1:}-Y{3:}-X{4:}.png'
# Important parameters
channels = [0]
ranges = np.array([
[0.01068132, 0.0717174]
])
colors = np.array([
[0, 1, 0]
])
x = 0
y = 0
z = 0
t = 0
level = 0
uuid = 'a7db6f10-8a27-4c40-9cab-bc30c6abfb59'
bucket = 'minerva-test-cf-common-tilebucket-yhuku9umej1s'
key_base = (
f'{uuid}/'
'C{}'
f'-T{t}-Z{z}-L{level}-Y{y}-X{x}.png'
)
# Prepare for blending
args = [(bucket, key_base.format(c)) for c in channels]
# Fetch raw tiles in parallel
pool = ThreadPool(processes=len(channels))
images = pool.starmap(_s3_get, args)
pool.close()
all_in = zip(images, colors, ranges)
inputs = list(map(format_input, all_in))
img_buffer = 255*composite_channels(inputs)
# Write the image buffer to a file
out_file = out_path_format.format(t, level, z, y, x)
try:
cv2.imwrite(out_file, img_buffer)
except OSError as o_e:
print(o_e)
if __name__ == "__main__":
main()
@thejohnhoffer
Copy link
Author

usage python composite_s3.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment