@@ -776,6 +776,197 @@ def test_palette_from_remote_colortable(self, mock_get):
776776 ),
777777 )
778778
779+ def test_scale_paletted_1band_clips_underflow_values (self ):
780+ """Test that values below the palette min are clipped to lowest color."""
781+ from hybig .browse import scale_paletted_1band
782+
783+ # Create test data with values below the palette minimum
784+ # Palette covers 100-400, but data includes -50 and 50
785+ data_with_underflow = np .array (
786+ [
787+ [- 50 , 50 , 100 , 200 ],
788+ [100 , 200 , 300 , 400 ],
789+ [50 , 100 , 200 , 300 ],
790+ [- 100 , 0 , 150 , 250 ],
791+ ]
792+ ).astype ('float64' )
793+ ds = DataArray (data_with_underflow ).expand_dims ('band' )
794+
795+ # Expected: underflow values (-50, 50, 0, -100) should map to index 0
796+ # which is the lowest color (red at value 100)
797+ expected_raster = np .array (
798+ [
799+ [
800+ [0 , 0 , 0 , 1 ], # -50, 50 -> 0 (red), 100->0, 200->1
801+ [0 , 1 , 2 , 3 ], # 100->0, 200->1, 300->2, 400->3
802+ [0 , 0 , 1 , 2 ], # 50, 100 -> 0, 200->1, 300->2
803+ [0 , 0 , 0 , 1 ], # -100, 0 -> 0, 150->0, 250->1
804+ ],
805+ ],
806+ dtype = 'uint8' ,
807+ )
808+
809+ image_palette = convert_colormap_to_palette (self .colormap )
810+ actual_raster , _ = scale_paletted_1band (ds , image_palette )
811+ assert_array_equal (expected_raster , actual_raster , strict = True )
812+
813+ def test_scale_paletted_1band_clips_overflow_values (self ):
814+ """Test that values above the palette max are clipped to highest color."""
815+ from hybig .browse import scale_paletted_1band
816+
817+ # Create test data with values above the palette maximum
818+ # Palette covers 100-400, but data includes 500 and 1000
819+ data_with_overflow = np .array (
820+ [
821+ [100 , 200 , 300 , 400 ],
822+ [400 , 500 , 600 , 1000 ],
823+ [200 , 300 , 400 , 500 ],
824+ [300 , 350 , 400 , 800 ],
825+ ]
826+ ).astype ('float64' )
827+ ds = DataArray (data_with_overflow ).expand_dims ('band' )
828+
829+ # Expected: overflow values (500, 600, 1000, 800) should map to index 3
830+ # which is the highest color (blue at value 400)
831+ expected_raster = np .array (
832+ [
833+ [
834+ [0 , 1 , 2 , 3 ], # 100->0, 200->1, 300->2, 400->3
835+ [3 , 3 , 3 , 3 ], # 400->3, 500->3, 600->3, 1000->3
836+ [1 , 2 , 3 , 3 ], # 200->1, 300->2, 400->3, 500->3
837+ [2 , 2 , 3 , 3 ], # 300->2, 350->2, 400->3, 800->3
838+ ],
839+ ],
840+ dtype = 'uint8' ,
841+ )
842+
843+ image_palette = convert_colormap_to_palette (self .colormap )
844+ actual_raster , _ = scale_paletted_1band (ds , image_palette )
845+ assert_array_equal (expected_raster , actual_raster , strict = True )
846+
847+ def test_scale_paletted_1band_with_nan_and_clipping (self ):
848+ """Test that NaN values map to nodata while clipping still works."""
849+ from hybig .browse import scale_paletted_1band
850+
851+ # Create test data with NaN, underflow, and overflow values
852+ data_mixed = np .array (
853+ [
854+ [np .nan , - 50 , 100 , 500 ],
855+ [50 , 200 , 300 , 1000 ],
856+ [100 , np .nan , 400 , 600 ],
857+ [- 100 , 250 , np .nan , 800 ],
858+ ]
859+ ).astype ('float64' )
860+ ds = DataArray (data_mixed ).expand_dims ('band' )
861+
862+ # Expected: NaN -> 4 (nodata), underflow -> 0, overflow -> 3
863+ expected_raster = np .array (
864+ [
865+ [
866+ [4 , 0 , 0 , 3 ], # NaN->4, -50->0, 100->0, 500->3
867+ [0 , 1 , 2 , 3 ], # 50->0, 200->1, 300->2, 1000->3
868+ [0 , 4 , 3 , 3 ], # 100->0, NaN->4, 400->3, 600->3
869+ [0 , 1 , 4 , 3 ], # -100->0, 250->1, NaN->4, 800->3
870+ ],
871+ ],
872+ dtype = 'uint8' ,
873+ )
874+
875+ image_palette = convert_colormap_to_palette (self .colormap )
876+ actual_raster , actual_palette = scale_paletted_1band (ds , image_palette )
877+ assert_array_equal (expected_raster , actual_raster , strict = True )
878+
879+ # Verify nodata color is transparent
880+ expected_nodata_color = (0 , 0 , 0 , 0 )
881+ self .assertEqual (actual_palette [np .uint8 (4 )], expected_nodata_color )
882+
883+ def test_scale_paletted_1band_to_rgb_clips_underflow_values (self ):
884+ """Test RGB output clips values below palette min to lowest color."""
885+ from hybig .browse import scale_paletted_1band_to_rgb
886+
887+ # Create test data with values below the palette minimum
888+ data_with_underflow = np .array (
889+ [
890+ [- 50 , 50 , 100 , 200 ],
891+ [100 , 200 , 300 , 400 ],
892+ ]
893+ ).astype ('float64' )
894+ ds = DataArray (data_with_underflow ).expand_dims ('band' )
895+
896+ image_palette = convert_colormap_to_palette (self .colormap )
897+ actual_rgb , _ = scale_paletted_1band_to_rgb (ds , image_palette )
898+
899+ # Values -50 and 50 should get red color (255, 0, 0)
900+ # which is the lowest color in the palette
901+ self .assertEqual (actual_rgb [0 , 0 , 0 ], 255 ) # Red channel for -50
902+ self .assertEqual (actual_rgb [1 , 0 , 0 ], 0 ) # Green channel for -50
903+ self .assertEqual (actual_rgb [2 , 0 , 0 ], 0 ) # Blue channel for -50
904+
905+ self .assertEqual (actual_rgb [0 , 0 , 1 ], 255 ) # Red channel for 50
906+ self .assertEqual (actual_rgb [1 , 0 , 1 ], 0 ) # Green channel for 50
907+ self .assertEqual (actual_rgb [2 , 0 , 1 ], 0 ) # Blue channel for 50
908+
909+ def test_scale_paletted_1band_to_rgb_clips_overflow_values (self ):
910+ """Test RGB output clips values above palette max to highest color."""
911+ from hybig .browse import scale_paletted_1band_to_rgb
912+
913+ # Create test data with values above the palette maximum
914+ data_with_overflow = np .array (
915+ [
916+ [400 , 500 , 600 , 1000 ],
917+ [300 , 400 , 800 , 1500 ],
918+ ]
919+ ).astype ('float64' )
920+ ds = DataArray (data_with_overflow ).expand_dims ('band' )
921+
922+ image_palette = convert_colormap_to_palette (self .colormap )
923+ actual_rgb , _ = scale_paletted_1band_to_rgb (ds , image_palette )
924+
925+ # Values 500, 600, 1000, 800, 1500 should get blue color (0, 0, 255)
926+ # which is the highest color in the palette
927+ for col in [1 , 2 , 3 ]: # columns 1, 2, 3 in row 0
928+ self .assertEqual (actual_rgb [0 , 0 , col ], 0 ) # Red channel
929+ self .assertEqual (actual_rgb [1 , 0 , col ], 0 ) # Green channel
930+ self .assertEqual (actual_rgb [2 , 0 , col ], 255 ) # Blue channel
931+
932+ for col in [2 , 3 ]: # columns 2, 3 in row 1
933+ self .assertEqual (actual_rgb [0 , 1 , col ], 0 ) # Red channel
934+ self .assertEqual (actual_rgb [1 , 1 , col ], 0 ) # Green channel
935+ self .assertEqual (actual_rgb [2 , 1 , col ], 255 ) # Blue channel
936+
937+ def test_scale_paletted_1band_to_rgb_with_nan_and_clipping (self ):
938+ """Test RGB output with NaN mapped to nodata and clipping working."""
939+ from hybig .browse import scale_paletted_1band_to_rgb
940+
941+ # Create test data with NaN, underflow, and overflow values
942+ data_mixed = np .array (
943+ [
944+ [np .nan , - 50 , 100 , 500 ],
945+ [50 , 200 , np .nan , 1000 ],
946+ ]
947+ ).astype ('float64' )
948+ ds = DataArray (data_mixed ).expand_dims ('band' )
949+
950+ image_palette = convert_colormap_to_palette (self .colormap )
951+ actual_rgb , _ = scale_paletted_1band_to_rgb (ds , image_palette )
952+
953+ # NaN should map to nodata color (0, 0, 0)
954+ self .assertEqual (actual_rgb [0 , 0 , 0 ], 0 ) # Red for NaN at (0,0)
955+ self .assertEqual (actual_rgb [1 , 0 , 0 ], 0 ) # Green for NaN at (0,0)
956+ self .assertEqual (actual_rgb [2 , 0 , 0 ], 0 ) # Blue for NaN at (0,0)
957+
958+ self .assertEqual (actual_rgb [0 , 1 , 2 ], 0 ) # Red for NaN at (1,2)
959+ self .assertEqual (actual_rgb [1 , 1 , 2 ], 0 ) # Green for NaN at (1,2)
960+ self .assertEqual (actual_rgb [2 , 1 , 2 ], 0 ) # Blue for NaN at (1,2)
961+
962+ # -50 and 50 should clip to red (255, 0, 0)
963+ self .assertEqual (actual_rgb [0 , 0 , 1 ], 255 ) # Red for -50
964+ self .assertEqual (actual_rgb [0 , 1 , 0 ], 255 ) # Red for 50
965+
966+ # 500 and 1000 should clip to blue (0, 0, 255)
967+ self .assertEqual (actual_rgb [2 , 0 , 3 ], 255 ) # Blue for 500
968+ self .assertEqual (actual_rgb [2 , 1 , 3 ], 255 ) # Blue for 1000
969+
779970
780971class TestCreateBrowse (TestCase ):
781972 """A class testing the create_browse function call.
0 commit comments