Skip to content

Commit 701fdb9

Browse files
authored
Merge pull request #52 from nasa/GITC-7383
GITC-7383: Fixed JPEG driver issue, updated maintainer information
2 parents edbbd25 + 5193f70 commit 701fdb9

File tree

6 files changed

+121
-12
lines changed

6 files changed

+121
-12
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ HyBIG follows semantic versioning. All notable changes to this project will be
44
documented in this file. The format is based on [Keep a
55
Changelog](http://keepachangelog.com/en/1.0.0/).
66

7+
## [2.4.1] - 2025-06-23
8+
9+
### Changed
10+
11+
* Fix bug with JPEG driver on single-banded input granules. Since we are now palettizing where possible since 2.4.0, this creates an issue when trying to output in JPEG since color palettes (and transparency) are not supported.
12+
13+
714
## [v2.4.0] - 2025-04-28
815

916
### Changed

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,5 @@ Guide](https://github.com/nasa/harmony/blob/main/docs/guides/managing-existing-s
478478

479479
You can reach out to the maintainers of this repository via email:
480480

481-
482-
483-
481+
482+

docker/service_version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.4.0
1+
2.4.1

hybig/browse.py

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,17 @@ def create_browse_imagery(
171171
color_palette = get_color_palette(
172172
in_dataset, source, item_color_palette
173173
)
174-
raster, color_map = convert_singleband_to_raster(
175-
rio_in_array, color_palette
176-
)
174+
if output_driver == 'JPEG':
175+
# For JPEG output, convert to RGB
176+
# color_map will be None
177+
raster, color_map = convert_singleband_to_rgb(
178+
rio_in_array, color_palette
179+
)
180+
else:
181+
# For PNG output, use palettized approach
182+
raster, color_map = convert_singleband_to_raster(
183+
rio_in_array, color_palette
184+
)
177185
elif rio_in_array.rio.count in (3, 4):
178186
raster = convert_mulitband_to_raster(rio_in_array)
179187
color_map = None
@@ -310,6 +318,78 @@ def scale_grey_1band(data_array: DataArray) -> tuple[ndarray, ColorMap]:
310318
return np.array(raster_data, dtype='uint8'), grey_colormap
311319

312320

321+
def convert_singleband_to_rgb(
322+
data_array: DataArray,
323+
color_palette: ColorPalette | None = None,
324+
) -> tuple[ndarray, None]:
325+
"""Convert input 1-band dataset to RGB image for JPEG output.
326+
327+
Uses a palette if provided, otherwise returns a greyscale RGB image.
328+
Returns a 3-band RGB array and None for colormap (since RGB doesn't need colormap).
329+
"""
330+
if color_palette is None:
331+
return scale_grey_1band_to_rgb(data_array)
332+
return scale_paletted_1band_to_rgb(data_array, color_palette)
333+
334+
335+
def scale_grey_1band_to_rgb(data_array: DataArray) -> tuple[ndarray, None]:
336+
"""Normalize input array and return as 3-band RGB grayscale image."""
337+
band = data_array[0, :, :]
338+
norm = Normalize(vmin=np.nanmin(band), vmax=np.nanmax(band))
339+
340+
# Scale input data from 0 to 254 (palettized data is 254-level quantized)
341+
normalized_data = norm(band) * 254.0
342+
343+
# Set any missing to 0 (black), no transparency
344+
normalized_data[np.isnan(normalized_data)] = 0
345+
346+
grey_data = np.round(normalized_data).astype('uint8')
347+
rgb_data = np.stack([grey_data, grey_data, grey_data], axis=0)
348+
349+
return rgb_data, None
350+
351+
352+
def scale_paletted_1band_to_rgb(
353+
data_array: DataArray, palette: ColorPalette
354+
) -> tuple[ndarray, None]:
355+
"""Scale a 1-band image with palette into RGB image for JPEG output."""
356+
band = data_array[0, :, :]
357+
levels = list(palette.pal.keys())
358+
colors = [
359+
palette.color_to_color_entry(value, with_alpha=True)
360+
for value in palette.pal.values()
361+
]
362+
norm = matplotlib.colors.BoundaryNorm(levels, len(levels) - 1)
363+
364+
# handle palette no data value
365+
nodata_color = (0, 0, 0, 0)
366+
if palette.ndv is not None:
367+
nodata_color = palette.color_to_color_entry(palette.ndv, with_alpha=True)
368+
369+
# Apply normalization to get palette indices
370+
indexed_band = norm(band)
371+
372+
# Create RGB output array
373+
height, width = band.shape
374+
rgb_array = np.zeros((3, height, width), dtype='uint8')
375+
376+
# Apply colors based on palette indices
377+
for i, color in enumerate(colors):
378+
mask = indexed_band == i
379+
rgb_array[0, mask] = color[0] # Red
380+
rgb_array[1, mask] = color[1] # Green
381+
rgb_array[2, mask] = color[2] # Blue
382+
383+
# Handle NaN/nodata values
384+
nan_mask = np.isnan(band)
385+
if nan_mask.any():
386+
rgb_array[0, nan_mask] = nodata_color[0]
387+
rgb_array[1, nan_mask] = nodata_color[1]
388+
rgb_array[2, nan_mask] = nodata_color[2]
389+
390+
return rgb_array, None
391+
392+
313393
def scale_paletted_1band(
314394
data_array: DataArray, palette: ColorPalette
315395
) -> tuple[ndarray, ColorMap]:

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ dynamic = ["dependencies", "version"]
55
authors = [
66
{name="Matt Savoie", email="[email protected]"},
77
{name="Owen Littlejohns", email="[email protected]"},
8+
{name="Jacqueline Ryan", email="[email protected]"},
9+
{name="Mauricio Hess-Flores", email="[email protected]"},
810
]
911

1012
maintainers = [
11-
{name="Matt Savoie", email="[email protected]"},
12-
{name="Owen Littlejohns", email="owen.m.littlejohns@nasa.gov"},
13+
{name="Jacqueline Ryan", email="[email protected]"},
14+
{name="Mauricio Hess-Flores", email="Mauricio.A.Hess.Flores@jpl.nasa.gov"},
1315
]
1416

1517
description = "Python package designed to produce browse imagery compatible with NASA's Global Image Browse Services (GIBS)."

tests/unit/test_browse.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,10 @@ def test_create_browse_imagery_with_mocks(
209209
target_transform = Affine(90.0, 0.0, -180.0, 0.0, -45.0, 90.0)
210210
dest = np.zeros((da_mock.rio.height, da_mock.rio.width), dtype='uint8')
211211

212-
# since we are no longer de-palettizing, we only have to reproject a single band
213-
self.assertEqual(reproject_mock.call_count, 1)
212+
# For JPEG output with 1-band input, we convert to RGB, so we reproject 3 bands
213+
self.assertEqual(reproject_mock.call_count, 3)
214214

215+
# For RGB output, we expect 3 calls (one per band) with TRANSPARENT nodata
215216
expected_calls = [
216217
call(
217218
source=expected_raster[0, :, :],
@@ -220,7 +221,27 @@ def test_create_browse_imagery_with_mocks(
220221
src_crs=da_mock.rio.crs,
221222
dst_transform=target_transform,
222223
dst_crs=CRS.from_string('EPSG:4326'),
223-
dst_nodata=255, # NODATA_IDX
224+
dst_nodata=0, # TRANSPARENT for RGB data
225+
resampling=Resampling.nearest,
226+
),
227+
call(
228+
source=expected_raster[0, :, :],
229+
destination=dest,
230+
src_transform=file_transform,
231+
src_crs=da_mock.rio.crs,
232+
dst_transform=target_transform,
233+
dst_crs=CRS.from_string('EPSG:4326'),
234+
dst_nodata=0, # TRANSPARENT for RGB data
235+
resampling=Resampling.nearest,
236+
),
237+
call(
238+
source=expected_raster[0, :, :],
239+
destination=dest,
240+
src_transform=file_transform,
241+
src_crs=da_mock.rio.crs,
242+
dst_transform=target_transform,
243+
dst_crs=CRS.from_string('EPSG:4326'),
244+
dst_nodata=0, # TRANSPARENT for RGB data
224245
resampling=Resampling.nearest,
225246
),
226247
]

0 commit comments

Comments
 (0)