Skip to content

Commit 867b2f8

Browse files
committed
GITC-7208: Rasterization changes, wip commit for unit tests, modified docker build process for GDAL
1 parent 5e3f420 commit 867b2f8

File tree

5 files changed

+107
-264
lines changed

5 files changed

+107
-264
lines changed

docker/service.Dockerfile

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,31 @@
1515
# and updates the entrypoint of the new service container
1616
#
1717
###############################################################################
18-
FROM python:3.12
18+
FROM python:3.12-slim
1919

2020
WORKDIR "/home"
2121

22-
RUN apt-get update
23-
RUN apt-get install -y libgdal-dev
22+
RUN apt-get update && apt-get install -y \
23+
build-essential \
24+
libgdal-dev \
25+
gdal-bin \
26+
&& apt-get clean \
27+
&& rm -rf /var/lib/apt/lists/*
28+
29+
RUN export GDAL_VERSION=$(gdal-config --version) && \
30+
echo "GDAL version: $GDAL_VERSION"
31+
32+
ENV CPLUS_INCLUDE_PATH=/usr/include/gdal
33+
ENV C_INCLUDE_PATH=/usr/include/gdal
34+
35+
RUN pip install GDAL==$(gdal-config --version)
2436

2537
# Install Pip dependencies
26-
COPY pip_requirements*.txt /home/
38+
COPY pip_requirements.txt /home/
2739

2840
RUN pip install --no-input --no-cache-dir \
29-
-r pip_requirements.txt \
30-
-r pip_requirements_skip_snyk.txt
41+
-r pip_requirements.txt
42+
# -r pip_requirements_skip_snyk.txt
3143

3244
# Copy service code.
3345
COPY ./harmony_service harmony_service

hybig/browse.py

Lines changed: 50 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
NODATA_RGBA,
2929
OPAQUE,
3030
TRANSPARENT,
31+
ColorMap,
3132
all_black_color_map,
33+
colormap_from_colors,
3234
get_color_palette,
35+
greyscale_colormap,
3336
palette_from_remote_colortable,
34-
remove_alpha,
3537
)
3638
from hybig.exceptions import HyBIGError
3739
from hybig.sizes import (
@@ -155,6 +157,7 @@ def create_browse_imagery(
155157
xml file.
156158
157159
"""
160+
logger.info(message)
158161
output_driver = image_driver(message.format.mime)
159162
out_image_file = output_image_file(Path(input_file_path), driver=output_driver)
160163
out_world_file = output_world_file(Path(input_file_path), driver=output_driver)
@@ -171,18 +174,19 @@ def create_browse_imagery(
171174
color_palette = get_color_palette(
172175
in_dataset, source, item_color_palette
173176
)
174-
raster = convert_singleband_to_raster(rio_in_array, color_palette)
177+
raster, color_map = convert_singleband_to_raster(
178+
rio_in_array, color_palette
179+
)
175180
elif rio_in_array.rio.count in (3, 4):
176181
raster = convert_mulitband_to_raster(rio_in_array)
182+
color_map = None
183+
if output_driver == 'JPEG':
184+
raster = raster[0:3, :, :]
177185
else:
178186
raise HyBIGError(
179187
f'incorrect number of bands for image: {rio_in_array.rio.count}'
180188
)
181189

182-
raster, color_map = standardize_raster_for_writing(
183-
raster, output_driver, rio_in_array.rio.count
184-
)
185-
186190
grid_parameters = get_target_grid_parameters(message, rio_in_array)
187191
grid_parameter_list, tile_locators = create_tiled_output_parameters(
188192
grid_parameters
@@ -283,69 +287,64 @@ def original_dtype(data_array: DataArray) -> str | None:
283287
def convert_singleband_to_raster(
284288
data_array: DataArray,
285289
color_palette: ColorPalette | None = None,
286-
) -> ndarray:
287-
"""Convert input dataset to a 4 band raster image.
290+
) -> tuple[ndarray, ColorMap]:
291+
"""Convert input dataset to a 1-band palettized image with colormap.
288292
289-
Use a palette if provided otherwise return a greyscale image.
293+
Uses a palette if provided otherwise returns a greyscale image.
290294
"""
291295
if color_palette is None:
292-
return convert_gray_1band_to_raster(data_array)
293-
return convert_paletted_1band_to_raster(data_array, color_palette)
296+
return scale_grey_1band(data_array)
297+
return scale_paletted_1band(data_array, color_palette)
294298

295299

296-
def convert_gray_1band_to_raster(data_array: DataArray) -> ndarray:
297-
"""Convert a 1-band raster without a color association."""
300+
def scale_grey_1band(data_array: DataArray) -> tuple[ndarray, ColorMap]:
301+
"""Normalize input array and return scaled data with greyscale ColorMap."""
298302
band = data_array[0, :, :]
299-
cmap = matplotlib.colormaps['Greys_r']
300-
cmap.set_bad(NODATA_RGBA)
301303
norm = Normalize(vmin=np.nanmin(band), vmax=np.nanmax(band))
302-
scalar_map = ScalarMappable(cmap=cmap, norm=norm)
303304

304-
rgba_image = np.zeros((*band.shape, 4), dtype='uint8')
305-
for row_no in range(band.shape[0]):
306-
rgba_image_slice = scalar_map.to_rgba(band[row_no, :], bytes=True)
307-
rgba_image[row_no, :, :] = rgba_image_slice
305+
# Scale input data from 0 to 254
306+
normalized_data = norm(band) * 254.0
308307

309-
return reshape_as_raster(rgba_image)
308+
# Set any missing to missing
309+
normalized_data[np.isnan(normalized_data)] = NODATA_IDX
310310

311+
grey_colormap = greyscale_colormap()
312+
raster_data = np.expand_dims(np.round(normalized_data).data, 0)
313+
return raster_data, grey_colormap
311314

312-
def convert_paletted_1band_to_raster(
315+
316+
def scale_paletted_1band(
313317
data_array: DataArray, palette: ColorPalette
314-
) -> ndarray:
315-
"""Convert a 1 band image with palette into a rgba raster image."""
318+
) -> tuple[ndarray, ColorMap]:
319+
"""Scale a 1-band image with palette into modified image and associated color_map.
320+
321+
Use the palette's levels and values, transform the input data_array into
322+
the correct levels indexed from 0-255 return the scaled array along side of
323+
a colormap corresponding to the new levels.
324+
"""
316325
band = data_array[0, :, :]
317326
levels = list(palette.pal.keys())
318327
colors = [
319328
palette.color_to_color_entry(value, with_alpha=True)
320329
for value in palette.pal.values()
321330
]
322-
scaled_colors = [
323-
(r / 255.0, g / 255.0, b / 255.0, a / 255.0) for r, g, b, a in colors
324-
]
325-
326-
cmap, norm = matplotlib.colors.from_levels_and_colors(
327-
levels, scaled_colors, extend='max'
328-
)
331+
norm = matplotlib.colors.BoundaryNorm(levels, len(levels) - 1)
329332

330333
# handle palette no data value
334+
nodata_color = (0, 0, 0, 0)
331335
if palette.ndv is not None:
332-
nodata_colors = palette.color_to_color_entry(palette.ndv, with_alpha=True)
333-
cmap.set_bad(
334-
(
335-
nodata_colors[0] / 255.0,
336-
nodata_colors[1] / 255.0,
337-
nodata_colors[2] / 255.0,
338-
nodata_colors[3] / 255.0,
339-
)
340-
)
336+
nodata_color = palette.color_to_color_entry(palette.ndv, with_alpha=True)
341337

342-
scalar_map = matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap)
343-
rgba_image = np.zeros((*band.shape, 4), dtype='uint8')
344-
for row_no in range(band.shape[0]):
345-
rgba_image[row_no, :, :] = scalar_map.to_rgba(
346-
np.ma.masked_invalid(band[row_no, :]), bytes=True
347-
)
348-
return reshape_as_raster(rgba_image)
338+
colors = [*colors, nodata_color]
339+
340+
scaled_band = norm(band)
341+
342+
# Set underflow and nan values to nodata
343+
scaled_band[scaled_band == -1] = len(colors) - 1
344+
scaled_band[np.isnan(band)] = len(colors) - 1
345+
346+
color_map = colormap_from_colors(colors)
347+
return np.array(np.expand_dims(scaled_band.data, 0), dtype="uint8"), color_map
349348

350349

351350
def image_driver(mime: str) -> str:
@@ -355,81 +354,6 @@ def image_driver(mime: str) -> str:
355354
return 'PNG'
356355

357356

358-
def standardize_raster_for_writing(
359-
raster: ndarray,
360-
driver: str,
361-
band_count: int,
362-
) -> tuple[ndarray, dict | None]:
363-
"""Standardize raster data for writing to browse image.
364-
365-
Args:
366-
raster: Input raster data array
367-
driver: Output image format ('JPEG' or 'PNG')
368-
band_count: Number of bands in original input data
369-
370-
The function handles two special cases:
371-
- JPEG output with 4-band data -> Drop alpha channel and return 3-band RGB
372-
- PNG output with single-band data -> Convert to paletted format
373-
374-
Returns:
375-
tuple: (prepared_raster, color_map) where:
376-
- prepared_raster is the processed ndarray
377-
- color_map is either None or a dict mapping palette indices to RGBA values
378-
379-
380-
"""
381-
if driver == 'JPEG' and raster.shape[0] == 4:
382-
return raster[0:3, :, :], None
383-
384-
if driver == 'PNG' and band_count == 1:
385-
# Only palettize single band input data that has been converted to an
386-
# RGBA raster.
387-
return palettize_raster(raster)
388-
389-
return raster, None
390-
391-
392-
def palettize_raster(raster: ndarray) -> tuple[ndarray, dict]:
393-
"""Convert an RGB or RGBA image into a 1band image and palette.
394-
395-
Converts a 3 or 4 band np raster into a PIL image.
396-
Quantizes the image into a 1band raster with palette
397-
398-
Transparency is handled by first removing the Alpha layer and creating
399-
quantized raster from just the RGB layers. Next the Alpha layer values are
400-
treated as either transparent or opaque and any transparent values are
401-
written to the final raster as 254 and add the mapped RGBA value to the
402-
color palette.
403-
"""
404-
# reserves index 255 for transparent and off grid fill values
405-
# 0 to 254
406-
max_colors = 255
407-
rgb_raster, alpha = remove_alpha(raster)
408-
409-
multiband_image = Image.fromarray(reshape_as_image(rgb_raster))
410-
quantized_image = multiband_image.quantize(colors=max_colors)
411-
412-
color_map = get_color_map_from_image(quantized_image)
413-
414-
quantized_array, color_map = add_alpha(alpha, np.array(quantized_image), color_map)
415-
416-
one_band_raster = np.expand_dims(quantized_array, 0)
417-
return one_band_raster, color_map
418-
419-
420-
def add_alpha(
421-
alpha: ndarray | None, quantized_array: ndarray, color_map: dict
422-
) -> tuple[ndarray, dict]:
423-
"""If the input data had alpha values, manually set the quantized_image
424-
index to the transparent index in those places.
425-
"""
426-
if alpha is not None and np.any(alpha != OPAQUE):
427-
# Set any alpha to the transparent index value
428-
quantized_array = np.where(alpha != OPAQUE, NODATA_IDX, quantized_array)
429-
color_map[NODATA_IDX] = NODATA_RGBA
430-
return quantized_array, color_map
431-
432-
433357
def get_color_map_from_image(image: Image) -> dict:
434358
"""Get a writable color map
435359
@@ -444,6 +368,10 @@ def get_color_map_from_image(image: Image) -> dict:
444368
return color_map
445369

446370

371+
def get_colormap_from_scaled_colors(scaled_colors) -> dict:
372+
pass
373+
374+
447375
def get_aux_xml_filename(image_filename: Path) -> Path:
448376
"""Get aux.xml filenames."""
449377
return image_filename.with_suffix(image_filename.suffix + '.aux.xml')

hybig/color_utility.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import numpy as np
99
import requests
1010
from harmony_service_lib.message import Source as HarmonySource
11+
from numpy import uint8
1112
from osgeo_utils.auxiliary.color_palette import ColorPalette
1213
from pystac import Item
1314
from rasterio.io import DatasetReader
@@ -17,10 +18,12 @@
1718
HyBIGNoColorInformation,
1819
)
1920

21+
ColorMap = dict[uint8, tuple[uint8, uint8, uint8, uint8]]
22+
2023
# Constants for output PNG images
2124
# Applied to transparent pixels where alpha < 255
22-
TRANSPARENT = np.uint8(0)
23-
OPAQUE = np.uint8(255)
25+
TRANSPARENT = uint8(0)
26+
OPAQUE = uint8(255)
2427
# Applied to off grid areas during reprojection
2528
NODATA_RGBA = (0, 0, 0, 0)
2629
NODATA_IDX = 255
@@ -120,11 +123,28 @@ def get_remote_palette_from_source(source: HarmonySource) -> dict:
120123
raise HyBIGNoColorInformation('No color in source') from exc
121124

122125

123-
def all_black_color_map():
126+
def all_black_color_map() -> ColorMap:
124127
"""Return a full length rgba color map with all black values."""
125128
return {idx: (0, 0, 0, 255) for idx in range(256)}
126129

127130

131+
def colormap_from_colors(
132+
colors: list[tuple[uint8, uint8, uint8, uint8]],
133+
) -> ColorMap:
134+
color_map = {}
135+
for idx, rgba in enumerate(colors):
136+
color_map[idx] = rgba
137+
return color_map
138+
139+
140+
def greyscale_colormap() -> ColorMap:
141+
color_map = {}
142+
for idx in range(255):
143+
color_map[idx] = (idx, idx, idx, 255)
144+
color_map[NODATA_IDX] = NODATA_RGBA
145+
return color_map
146+
147+
128148
def convert_colormap_to_palette(colormap: dict) -> ColorPalette:
129149
"""Convert a GeoTIFF palette to GDAL ColorPalette.
130150

tests/test_service/test_adapter.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
from harmony_service.exceptions import HyBIGServiceError
2020
from hybig.browse import (
2121
convert_mulitband_to_raster,
22-
standardize_raster_for_writing,
2322
)
2423
from tests.utilities import Granule, create_stac
2524

@@ -460,7 +459,6 @@ def move_tif(*args, **kwargs):
460459
'count': 3,
461460
}
462461
raster = convert_mulitband_to_raster(rio_data_array)
463-
raster, color_map = standardize_raster_for_writing(raster, 'PNG', 3)
464462

465463
dest = np.full(
466464
(expected_params['height'], expected_params['width']),

0 commit comments

Comments
 (0)