def test_parse_bands(arr): fa = parse_operations("gamma 1,2 0.95")[0] fb = parse_operations("gamma Rg 0.95")[0] assert np.array_equal(fa(arr), fb(arr)) with pytest.raises(ValueError): parse_operations("gamma 7,8,9 1.05")
def _postprocess( tile: numpy.ndarray, mask: numpy.ndarray, rescale: str = None, color_formula: str = None, ) -> Tuple[numpy.ndarray, numpy.ndarray]: """Post-process tile data.""" if rescale: rescale_arr = list(map(float, rescale.split(","))) rescale_arr = list(_chunks(rescale_arr, 2)) if len(rescale_arr) != tile.shape[0]: rescale_arr = ((rescale_arr[0]),) * tile.shape[0] for bdx in range(tile.shape[0]): tile[bdx] = numpy.where( mask, linear_rescale( tile[bdx], in_range=rescale_arr[bdx], out_range=[0, 255] ), 0, ) tile = tile.astype(numpy.uint8) if color_formula: # make sure one last time we don't have # negative value before applying color formula tile[tile < 0] = 0 for ops in parse_operations(color_formula): tile = scale_dtype(ops(to_math_type(tile)), numpy.uint8) return tile, mask
def _postprocess( tile: numpy.ndarray, mask: numpy.ndarray, tilesize: int, rescale: str = None, color_formula: str = None, ) -> Tuple[numpy.ndarray, numpy.ndarray]: if tile is None: # Return empty tile tile = numpy.zeros((1, tilesize, tilesize), dtype=numpy.uint8) mask = numpy.zeros((tilesize, tilesize), dtype=numpy.uint8) else: if rescale: rescale_arr = (tuple(map(float, rescale.split(","))), ) * tile.shape[0] for bdx in range(tile.shape[0]): tile[bdx] = numpy.where( mask, linear_rescale(tile[bdx], in_range=rescale_arr[bdx], out_range=[0, 255]), 0, ) tile = tile.astype(numpy.uint8) if color_formula: # make sure one last time we don't have # negative value before applying color formula tile[tile < 0] = 0 for ops in parse_operations(color_formula): tile = scale_dtype(ops(to_math_type(tile)), numpy.uint8) return tile, mask
def post_process_tile( tile: numpy.ndarray, mask: numpy.ndarray, rescale: str = None, color_formula: str = None, ) -> Tuple[numpy.ndarray, numpy.ndarray]: """Tile data post processing.""" if rescale: rescale_arr = (tuple(map(float, rescale.split(","))), ) * tile.shape[0] for bdx in range(tile.shape[0]): tile[bdx] = numpy.where( mask, linear_rescale(tile[bdx], in_range=rescale_arr[bdx], out_range=[0, 255]), 0, ) tile = tile.astype(numpy.uint8) if color_formula: if issubclass(tile.dtype.type, numpy.floating): tile = tile.astype(numpy.int16) # make sure one last time we don't have # negative value before applying color formula tile[tile < 0] = 0 for ops in parse_operations(color_formula): tile = scale_dtype(ops(to_math_type(tile)), numpy.uint8) return tile
def color(jobs, out_dtype, src_path, dst_path, operations, creation_options): with rasterio.open(src_path) as src: opts = src.profile.copy() windows = [(window, ij) for ij, window in src.block_windows()] opts.update(**creation_options) opts["transform"] = guard_transform(opts["transform"]) out_dtype = out_dtype if out_dtype else opts["dtype"] opts["dtype"] = out_dtype args = {"ops_string": " ".join(operations), "out_dtype": out_dtype} # Just run this for validation this time # parsing will be run again within the worker # where its returned value will be used try: parse_operations(args["ops_string"]) except ValueError as e: import sys sys.exit(1) print(e) jobs = check_jobs(jobs) if jobs > 1: with riomucho.RioMucho( [src_path], dst_path, color_worker, windows=windows, options=opts, global_args=args, mode="manual_read", ) as mucho: mucho.run(jobs) else: with rasterio.open(dst_path, "w", **opts) as dest: with rasterio.open(src_path) as src: rasters = [src] for window, ij in windows: arr = color_worker(rasters, window, ij, args) dest.write(arr, window=window) dest.colorinterp = src.colorinterp
def post_process( self, in_range: Optional[Tuple[NumType, NumType]] = None, out_dtype: Union[str, numpy.number] = "uint8", color_formula: Optional[str] = None, **kwargs: Any, ) -> "ImageData": """Post-process image data. Args: in_range (tuple): input min/max bounds value to rescale from. out_dtype (str, optional): output datatype after rescaling. Defaults to `uint8`. color_formula (str, optional): rio-color formula (see: https://github.com/mapbox/rio-color). kwargs (optional): keyword arguments to forward to `rio_tiler.utils.linear_rescale`. Returns: ImageData: new ImageData object with the updated data. Examples: >>> img.post_process(in_range=(0, 16000)) >>> img.post_process(color_formula="Gamma RGB 4.1") """ data = self.data.copy() mask = self.mask.copy() if in_range: rescale_arr = tuple(_chunks(in_range, 2)) if len(rescale_arr) != self.count: rescale_arr = ((rescale_arr[0]), ) * self.count for bdx in range(self.count): data[bdx] = numpy.where( self.mask, linear_rescale( data[bdx], in_range=rescale_arr[bdx], **kwargs, ), 0, ) data = data.astype(out_dtype) if color_formula: data[data < 0] = 0 for ops in parse_operations(color_formula): data = scale_dtype(ops(to_math_type(data)), numpy.uint8) return ImageData( data, mask, crs=self.crs, bounds=self.bounds, assets=self.assets, metadata=self.metadata, )
def test_parse_rgba(arr, arr_rgba): f = parse_operations("gamma rg 0.95")[0] rgb = f(arr) assert rgb.shape[0] == 3 rgba = f(arr_rgba) assert rgba.shape[0] == 4 # rgb bands are same assert np.allclose(rgba[0:3], rgb[0:3]) # alpha unaltered assert np.array_equal(rgba[3], arr_rgba[3])
def test_saturation_rgba(arr, arr_rgba): f = parse_operations("saturation 1.25")[0] satrgb = f(arr) assert satrgb.shape[0] == 3 satrgba = f(arr_rgba) # Still rgba assert satrgba.shape[0] == 4 # alpha is unaltered assert np.array_equal(satrgba[3], arr_rgba[3]) # first 3 bands are same b/t rgb and rgba assert np.allclose(satrgba[0:3], satrgb[0:3])
def read_tile(id, tile, scale=1, **kwargs): meta = get_metadata(id, **kwargs) maxzoom = int(meta['maxzoom']) minzoom = int(meta['minzoom']) if not minzoom <= tile.z <= maxzoom: raise InvalidTileRequest('Invalid zoom: {} outside [{}, {}]'.format(tile.z, minzoom, maxzoom)) sw = mercantile.tile(*meta['bounds'][0:2], zoom=tile.z) ne = mercantile.tile(*meta['bounds'][2:4], zoom=tile.z) if not sw.x <= tile.x <= ne.x: raise InvalidTileRequest('Invalid x coordinate: {} outside [{}, {}]'.format(tile.x, sw.x, ne.x)) if not ne.y <= tile.y <= sw.y: raise InvalidTileRequest('Invalid y coordinate: {} outside [{}, {}]'.format(tile.y, sw.y, ne.y)) data = render_tile(meta, tile, scale=scale) # 8-bit per pixel target_dtype = np.uint8 # default values from rio color atmo ops = meta['meta'].get('operations') if ops: # scale to (0..1) floats = (data * 1.0 / np.iinfo(data.dtype).max).astype(np.float32) for func in operations.parse_operations(ops): floats = func(floats) # scale back to uint8 data = (floats * np.iinfo(target_dtype).max).astype(target_dtype) if data.dtype != target_dtype: # rescale try: data = (data * (np.iinfo(target_dtype).max / np.iinfo(data.dtype).max)).astype(target_dtype) except: raise Exception('Not enough information to rescale; source is "{}""'.format(data.dtype)) imgarr = np.ma.transpose(data, [1, 2, 0]) out = StringIO() im = Image.fromarray(imgarr, 'RGBA') im.save(out, 'png') return out.getvalue()
def _postprocess_tile(tile, mask, rescale=None, color_ops=None): """Tile data post-processing.""" if rescale: rescale = (tuple(map(float, rescale.split(","))), ) * tile.shape[0] for bdx in range(tile.shape[0]): tile[bdx] = numpy.where( mask, linear_rescale(tile[bdx], in_range=rescale[bdx], out_range=[0, 255]), 0, ) tile = tile.astype(numpy.uint8) if color_ops: # make sure one last time we don't have # negative value before applying color formula tile[tile < 0] = 0 for ops in parse_operations(color_ops): tile = scale_dtype(ops(to_math_type(tile)), numpy.uint8) return tile, mask
def _postprocess(tile, mask, tilesize, rescale=None, color_formula=None): if tile is None: # Return empty tile tile = numpy.zeros((1, tilesize, tilesize), dtype=numpy.uint8) mask = numpy.zeros((tilesize, tilesize), dtype=numpy.uint8) else: if rescale: rescale = (tuple(map(float, rescale.split(","))), ) * tile.shape[0] for bdx in range(tile.shape[0]): tile[bdx] = numpy.where( mask, linear_rescale(tile[bdx], in_range=rescale[bdx], out_range=[0, 255]), 0, ) tile = tile.astype(numpy.uint8) if color_formula: for ops in parse_operations(color_formula): tile = scale_dtype(ops(to_math_type(tile)), numpy.uint8) return tile, mask
def test_parse_multi_saturation_first(arr): f1, f2 = parse_operations("saturation 1.25 gamma rgb 0.95") assert np.array_equal(f2(f1(arr)), gamma(saturation(arr, 1.25), g=0.95))
def test_parse_sigmoidal(arr): f = parse_operations("sigmoidal rgb 5 0.53")[0] assert np.array_equal(f(arr), sigmoidal(arr, contrast=5, bias=0.53))
def test_parse_gamma(arr): f = parse_operations("gamma rgb 0.95")[0] assert np.array_equal(f(arr), gamma(arr, 0.95))
def test_parse_comma(arr): # Commas are optional whitespace, treated like empty string f1, f2 = parse_operations("gamma r,g,b 0.95, sigmoidal r,g,b 35 0.13") assert np.array_equal( f2(f1(arr)), sigmoidal(gamma(arr, g=0.95), contrast=35, bias=0.13))
def test_parse_multi(arr): f1, f2 = parse_operations("gamma rgb 0.95 sigmoidal rgb 35 0.13") assert np.array_equal( f2(f1(arr)), sigmoidal(gamma(arr, g=0.95), contrast=35, bias=0.13))
def apply_color(self, arr, state): """Apply color formula to an array.""" ops = self.cmd(state) for func in parse_operations(ops): arr = func(arr) return arr
def color(ctx, jobs, out_dtype, src_path, dst_path, operations, creation_options): """Color correction Operations will be applied to the src image in the specified order. Available OPERATIONS include: \b "gamma BANDS VALUE" Applies a gamma curve, brightening or darkening midtones. VALUE > 1 brightens the image. \b "sigmoidal BANDS CONTRAST BIAS" Adjusts the contrast and brightness of midtones. BIAS > 0.5 darkens the image. \b "saturation PROPORTION" Controls the saturation in LCH color space. PROPORTION = 0 results in a grayscale image PROPORTION = 1 results in an identical image PROPORTION = 2 is likely way too saturated BANDS are specified as a single arg, no delimiters \b `123` or `RGB` or `rgb` are all equivalent Example: \b rio color -d uint8 -j 4 input.tif output.tif \\ gamma 3 0.95, sigmoidal rgb 35 0.13 """ with rasterio.open(src_path) as src: opts = src.profile.copy() windows = [(window, ij) for ij, window in src.block_windows()] opts.update(**creation_options) opts['transform'] = guard_transform(opts['transform']) out_dtype = out_dtype if out_dtype else opts['dtype'] opts['dtype'] = out_dtype args = { 'ops_string': ' '.join(operations), 'out_dtype': out_dtype } # Just run this for validation this time # parsing will be run again within the worker # where its returned value will be used try: ops = parse_operations(args['ops_string']) except ValueError as e: raise click.UsageError(str(e)) jobs = check_jobs(jobs) if jobs > 1: with riomucho.RioMucho( [src_path], dst_path, color_worker, windows=windows, options=opts, global_args=args, mode="manual_read" ) as mucho: mucho.run(jobs) else: with rasterio.open(dst_path, 'w', **opts) as dest: with rasterio.open(src_path) as src: rasters = [src] for window, ij in windows: arr = color_worker(rasters, window, ij, args) dest.write(arr, window=window)
def me(in_tif, out_tif, bands, s_expressions, ops, lut=None): bands = [band for band in bands if band] logging.info(len(bands)) # read the input tif, it's the VRT ds = gdal.Open(in_tif) width = ds.RasterXSize height = ds.RasterYSize input_geotransform = ds.GetGeoTransform() input_georef = ds.GetProjectionRef() # the lenght of bands (same as ds.RasterCount) tells us how many inputs we have ctx = dict() ctx['v1'] = ds.GetRasterBand(1).ReadAsArray() if len(bands) > 1: ctx['v2'] = ds.GetRasterBand(2).ReadAsArray() if len(bands) == 3: ctx['v3'] = ds.GetRasterBand(3).ReadAsArray() # apply the expressions to the bands arr = np.stack( [snuggs.eval(s_expression, **ctx) for s_expression in s_expressions]) # apply the color operations using rio color if ops is not None: assert arr.shape[0] == 3 assert arr.min() >= 0 assert arr.max() <= 1 logging.info('Applying color operations: {}'.format(ops)) for func in parse_operations(ops): arr = func(arr) if lut is not None: logging.info('Applying look-up table') temp_arr = cm.get_cmap(lut)(arr) arr = np.array( [temp_arr[0][:, :, 0], temp_arr[0][:, :, 1], temp_arr[0][:, :, 2]]) # save driver = gdal.GetDriverByName('GTiff') output = driver.Create(out_tif, width, height, arr.shape[0], gdal.GDT_Byte) output.SetGeoTransform(input_geotransform) output.SetProjection(input_georef) for index in range(1, arr.shape[0] + 1): logging.info('Adding band {} of {}'.format(index, arr.shape[0])) band = output.GetRasterBand(index) band.WriteArray((arr[index - 1] * 255).astype(np.int)) output.FlushCache() band = None output = None sleep(5) del (output) ds = None del (ds) return True
def test_parse_saturation_rgb(arr): f = parse_operations("saturation 1.25")[0] assert np.allclose(f(arr), saturation(arr, 1.25))
def test_simple_atmos_opstring(arr): x = simple_atmo(arr, 0.03, 10, 0.15) ops = simple_atmo_opstring(0.03, 10, 0.15) for op in parse_operations(ops): arr = op(arr) assert np.allclose(x, arr)
def color(ctx, jobs, out_dtype, src_path, dst_path, operations, creation_options): """Color correction Operations will be applied to the src image in the specified order. Available OPERATIONS include: \b "gamma BANDS VALUE" Applies a gamma curve, brightening or darkening midtones. VALUE > 1 brightens the image. \b "sigmoidal BANDS CONTRAST BIAS" Adjusts the contrast and brightness of midtones. BIAS > 0.5 darkens the image. \b "saturation PROPORTION" Controls the saturation in LCH color space. PROPORTION = 0 results in a grayscale image PROPORTION = 1 results in an identical image PROPORTION = 2 is likely way too saturated BANDS are specified as a single arg, no delimiters \b `123` or `RGB` or `rgb` are all equivalent Example: \b rio color -d uint8 -j 4 input.tif output.tif \\ gamma 3 0.95, sigmoidal rgb 35 0.13 """ with rasterio.open(src_path) as src: opts = src.profile.copy() windows = [(window, ij) for ij, window in src.block_windows()] opts.update(**creation_options) opts["transform"] = guard_transform(opts["transform"]) out_dtype = out_dtype if out_dtype else opts["dtype"] opts["dtype"] = out_dtype args = {"ops_string": " ".join(operations), "out_dtype": out_dtype} # Just run this for validation this time # parsing will be run again within the worker # where its returned value will be used try: parse_operations(args["ops_string"]) except ValueError as e: raise click.UsageError(str(e)) jobs = check_jobs(jobs) if jobs > 1: with riomucho.RioMucho( [src_path], dst_path, color_worker, windows=windows, options=opts, global_args=args, mode="manual_read", ) as mucho: mucho.run(jobs) else: with rasterio.open(dst_path, "w", **opts) as dest: with rasterio.open(src_path) as src: rasters = [src] for window, ij in windows: arr = color_worker(rasters, window, ij, args) dest.write(arr, window=window) dest.colorinterp = src.colorinterp
def test_parse_bad_op(): with pytest.raises(ValueError): parse_operations("foob 123")
def _apply_color_operations(self, img, color_ops): for ops in parse_operations(color_ops): img = scale_dtype(ops(to_math_type(img)), numpy.uint8) return img
def test_parse_multi_name(arr): f1, f2 = parse_operations("saturation 1.25 gamma rgb 0.95") assert f1.__name__ == "saturation" assert f2.__name__ == "gamma"