Skip to content

Commit 2d3f8bb

Browse files
authored
Merge pull request #625 from dcs4cop/toniof-xxx-flip_fix
Fixed flipping for unevenly tiled images
2 parents 9b61e11 + 40788b4 commit 2d3f8bb

File tree

4 files changed

+135
-68
lines changed

4 files changed

+135
-68
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
### Fixes
66

7+
* Images with ascending y-values are tiled correctly. This fixes an issue where
8+
some datasets seemed to be shifted in the y-(latitude-) direction and were
9+
misplaced on maps. (#626)
10+
711
### Other
812

913
* Replace the dependency on the rfc3339-validator PyPI package with a

test/util/test_tiledimage.py

Lines changed: 82 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import numpy as np
44
import PIL.Image
55

6-
from xcube.util.tiledimage import ArrayImage
6+
from xcube.util.tiledimage import SourceArrayImage
77
from xcube.util.tiledimage import ColorMappedRgbaImage
88
from xcube.util.tiledimage import DirectRgbaImage
99
from xcube.util.tiledimage import OpImage
10-
from xcube.util.tiledimage import TransformArrayImage
10+
from xcube.util.tiledimage import NormalizeArrayImage
1111
from xcube.util.tiledimage import trim_tile
1212

1313

@@ -26,7 +26,7 @@ def compute_tile(self, tile_x, tile_y, rectangle):
2626
class ColorMappedRgbaImageTest(TestCase):
2727
def test_default(self):
2828
a = np.linspace(0, 255, 24, dtype=np.int32).reshape((4, 6))
29-
source_image = ArrayImage(a, (2, 2))
29+
source_image = SourceArrayImage(a, (2, 2))
3030
cm_rgb_image = ColorMappedRgbaImage(source_image, format='PNG')
3131
self.assertEqual(cm_rgb_image.size, (6, 4))
3232
self.assertEqual(cm_rgb_image.tile_size, (2, 2))
@@ -41,9 +41,9 @@ def test_default(self):
4141
b = np.linspace(255, 0, 24, dtype=np.int32).reshape((4, 6))
4242
c = np.linspace(50, 200, 24, dtype=np.int32).reshape((4, 6))
4343
source_images = [
44-
ArrayImage(a, (2, 2)),
45-
ArrayImage(b, (2, 2)),
46-
ArrayImage(c, (2, 2)),
44+
SourceArrayImage(a, (2, 2)),
45+
SourceArrayImage(b, (2, 2)),
46+
SourceArrayImage(c, (2, 2)),
4747
]
4848
cm_rgb_image = DirectRgbaImage(source_images, format='PNG')
4949
self.assertEqual(cm_rgb_image.size, (6, 4))
@@ -53,11 +53,11 @@ def test_default(self):
5353
self.assertIsInstance(tile, PIL.Image.Image)
5454

5555

56-
class TransformArrayImageTest(TestCase):
56+
class NormalizeArrayImageTest(TestCase):
5757
def test_default(self):
5858
a = np.arange(0, 24, dtype=np.int32).reshape((4, 6))
59-
source_image = ArrayImage(a, (2, 2))
60-
target_image = TransformArrayImage(source_image)
59+
source_image = SourceArrayImage(a, (2, 2))
60+
target_image = NormalizeArrayImage(source_image)
6161

6262
self.assertEqual(target_image.size, (6, 4))
6363
self.assertEqual(target_image.tile_size, (2, 2))
@@ -77,37 +77,86 @@ def test_default(self):
7777
self.assertEqual(target_image.get_tile(2, 1).tolist(), [[16, 17],
7878
[22, 23]])
7979

80-
def test_flip_y(self):
81-
a = np.arange(0, 24, dtype=np.int32).reshape((4, 6))
82-
source_image = ArrayImage(a, (2, 2))
83-
target_image = TransformArrayImage(source_image, flip_y=True)
80+
def test_force_2d(self):
81+
a = np.arange(0, 48, dtype=np.int32).reshape((2, 4, 6))
82+
source_image = SourceArrayImage(a[0], (2, 2))
83+
target_image = NormalizeArrayImage(source_image, force_2d=True)
8484

8585
self.assertEqual(target_image.size, (6, 4))
8686
self.assertEqual(target_image.tile_size, (2, 2))
8787
self.assertEqual(target_image.num_tiles, (3, 2))
8888

89-
self.assertEqual(target_image.get_tile(0, 0).tolist(), [[18, 19],
90-
[12, 13]])
91-
self.assertEqual(target_image.get_tile(1, 0).tolist(), [[20, 21],
92-
[14, 15]])
93-
self.assertEqual(target_image.get_tile(2, 0).tolist(), [[22, 23],
94-
[16, 17]])
9589

96-
self.assertEqual(target_image.get_tile(0, 1).tolist(), [[6, 7],
97-
[0, 1]])
98-
self.assertEqual(target_image.get_tile(1, 1).tolist(), [[8, 9],
99-
[2, 3]])
100-
self.assertEqual(target_image.get_tile(2, 1).tolist(), [[10, 11],
101-
[4, 5]])
102-
103-
def test_force_2d(self):
104-
a = np.arange(0, 48, dtype=np.int32).reshape((2, 4, 6))
105-
source_image = ArrayImage(a[0], (2, 2))
106-
target_image = TransformArrayImage(source_image, force_2d=True)
90+
class SourceArrayImageTest(TestCase):
10791

108-
self.assertEqual(target_image.size, (6, 4))
109-
self.assertEqual(target_image.tile_size, (2, 2))
110-
self.assertEqual(target_image.num_tiles, (3, 2))
92+
def test_flip_y_tiled_evenly(self):
93+
a = np.arange(0, 24, dtype=np.int32).reshape((4, 6))
94+
source_array_image = SourceArrayImage(a, (2, 2), flip_y=True)
95+
96+
self.assertEqual(source_array_image.size, (6, 4))
97+
self.assertEqual(source_array_image.tile_size, (2, 2))
98+
self.assertEqual(source_array_image.num_tiles, (3, 2))
99+
100+
self.assertEqual(source_array_image.get_tile(0, 0).tolist(),
101+
[[18, 19],
102+
[12, 13]])
103+
self.assertEqual(source_array_image.get_tile(1, 0).tolist(),
104+
[[20, 21],
105+
[14, 15]])
106+
self.assertEqual(source_array_image.get_tile(2, 0).tolist(),
107+
[[22, 23],
108+
[16, 17]])
109+
110+
self.assertEqual(source_array_image.get_tile(0, 1).tolist(),
111+
[[6, 7],
112+
[0, 1]])
113+
self.assertEqual(source_array_image.get_tile(1, 1).tolist(),
114+
[[8, 9],
115+
[2, 3]])
116+
self.assertEqual(source_array_image.get_tile(2, 1).tolist(),
117+
[[10, 11],
118+
[4, 5]])
119+
120+
def test_flip_y_not_tiled_evenly(self):
121+
a = np.arange(0, 30, dtype=np.int32).reshape((5, 6))
122+
source_array_image = SourceArrayImage(a, (2, 2), flip_y=True)
123+
124+
self.assertEqual(source_array_image.size, (6, 5))
125+
self.assertEqual(source_array_image.tile_size, (2, 2))
126+
self.assertEqual(source_array_image.num_tiles, (3, 3))
127+
128+
self.assertEqual(source_array_image.get_tile(0, 0).tolist(),
129+
[[24, 25],
130+
[18, 19]])
131+
self.assertEqual(source_array_image.get_tile(1, 0).tolist(),
132+
[[26, 27],
133+
[20, 21]])
134+
self.assertEqual(source_array_image.get_tile(2, 0).tolist(),
135+
[[28, 29],
136+
[22, 23]])
137+
138+
self.assertEqual(source_array_image.get_tile(0, 1).tolist(),
139+
[[12, 13],
140+
[6, 7]])
141+
self.assertEqual(source_array_image.get_tile(1, 1).tolist(),
142+
[[14, 15],
143+
[8, 9]])
144+
self.assertEqual(source_array_image.get_tile(2, 1).tolist(),
145+
[[16, 17],
146+
[10, 11]])
147+
148+
tile_0_2 = source_array_image.get_tile(0, 2).tolist()
149+
self.assertEqual(tile_0_2[0], [0, 1])
150+
self.assertTrue(np.isnan(tile_0_2[1][0]))
151+
self.assertTrue(np.isnan(tile_0_2[1][1]))
152+
tile_1_2 = source_array_image.get_tile(1, 2).tolist()
153+
self.assertEqual(tile_1_2[0], [2, 3])
154+
self.assertTrue(tile_1_2[1][0])
155+
self.assertTrue(tile_1_2[1][1])
156+
tile_2_2 = source_array_image.get_tile(2, 2).tolist()
157+
self.assertEqual(tile_2_2[0], [4, 5])
158+
self.assertTrue(tile_2_2[1][0])
159+
self.assertTrue(tile_2_2[1][1])
111160

112161

113162
class TrimTileTest(TestCase):

xcube/core/tile.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@
3030
from xcube.core.schema import get_dataset_xy_var_names
3131
from xcube.util.cache import Cache
3232
from xcube.util.perf import measure_time_cm
33-
from xcube.util.tiledimage import ArrayImage
33+
from xcube.util.tiledimage import SourceArrayImage
3434
from xcube.util.tiledimage import ColorMappedRgbaImage
3535
from xcube.util.tiledimage import DEFAULT_COLOR_MAP_NAME
3636
from xcube.util.tiledimage import DEFAULT_COLOR_MAP_VALUE_RANGE
3737
from xcube.util.tiledimage import DirectRgbaImage
3838
from xcube.util.tiledimage import Tile
3939
from xcube.util.tiledimage import TiledImage
40-
from xcube.util.tiledimage import TransformArrayImage
40+
from xcube.util.tiledimage import NormalizeArrayImage
4141

4242

4343
def get_ml_dataset_tile(
@@ -148,13 +148,13 @@ def new_rgb_image(ml_dataset: MultiLevelDataset,
148148
exception_type
149149
)
150150

151-
image = ArrayImage(array,
152-
image_id=f'ai-{image_id}',
153-
tile_size=tile_grid.tile_size,
154-
trace_perf=trace_perf)
155-
image = TransformArrayImage(image,
151+
image = SourceArrayImage(array,
152+
image_id=f'ai-{image_id}',
153+
tile_size=tile_grid.tile_size,
154+
flip_y=tile_grid.is_j_axis_up,
155+
trace_perf=trace_perf)
156+
image = NormalizeArrayImage(image,
156157
image_id=f'tai-{image_id}',
157-
flip_y=tile_grid.is_j_axis_up,
158158
norm_range=norm_ranges[i],
159159
trace_perf=trace_perf)
160160
images.append(image)
@@ -189,13 +189,13 @@ def new_color_mapped_image(ml_dataset: MultiLevelDataset,
189189
cmap_range,
190190
valid_range)
191191
tile_grid = ml_dataset.tile_grid
192-
image = ArrayImage(array,
193-
image_id=f'ai-{image_id}',
194-
tile_size=tile_grid.tile_size,
195-
trace_perf=trace_perf)
196-
image = TransformArrayImage(image,
192+
image = SourceArrayImage(array,
193+
image_id=f'ai-{image_id}',
194+
tile_size=tile_grid.tile_size,
195+
flip_y=tile_grid.is_j_axis_up,
196+
trace_perf=trace_perf)
197+
image = NormalizeArrayImage(image,
197198
image_id=f'tai-{image_id}',
198-
flip_y=tile_grid.is_j_axis_up,
199199
norm_range=cmap_range,
200200
trace_perf=trace_perf)
201201
if cmap_name is None and cmap_range is None:

xcube/util/tiledimage.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -338,38 +338,30 @@ def compute_tile_from_source_tile(self,
338338
"""
339339

340340

341-
class TransformArrayImage(DecoratorImage):
341+
class NormalizeArrayImage(DecoratorImage):
342342
"""
343-
Performs basic (numpy) array tile transformations. Currently available: force_masked, flip_y.
343+
Performs basic (numpy) array tile transformations.
344+
Currently available: norm_range.
344345
Expects the source image to provide (numpy) arrays.
345346
346347
:param source_image: The source image
347348
:param image_id: Optional unique image identifier
348-
:param flip_y: Whether to flip pixels in y-direction
349349
:param tile_cache: Optional tile cache
350350
:param trace_perf: Whether to log runtime performance information
351351
"""
352352

353353
def __init__(self,
354354
source_image: TiledImage,
355355
image_id: str = None,
356-
flip_y: bool = False,
357356
force_2d: bool = False,
358357
norm_range: NormRange = None,
359358
tile_cache: Cache = None,
360359
trace_perf: bool = False):
361360
super().__init__(source_image, image_id=image_id, tile_cache=tile_cache, trace_perf=trace_perf)
362361
self._force_2d = force_2d
363-
self._flip_y = flip_y
364362
self._norm_range = norm_range
365363

366364
def compute_tile(self, tile_x: int, tile_y: int, rectangle: Rectangle2D) -> Tile:
367-
if self._flip_y:
368-
num_tiles_y = self.num_tiles[1]
369-
tile_size_y = self.tile_size[1]
370-
tile_y = num_tiles_y - 1 - tile_y
371-
x, y, w, h = rectangle
372-
rectangle = x, tile_y * tile_size_y, w, h
373365
source_tile = self._source_image.get_tile(tile_x, tile_y)
374366
target_tile = None
375367
if source_tile is not None:
@@ -381,11 +373,6 @@ def compute_tile_from_source_tile(self, tile_x: int, tile_y: int, rectangle: Rec
381373
measure_time = self.measure_time
382374
tile_tag = self._get_tile_tag(tile_x, tile_y)
383375

384-
if self._flip_y:
385-
with measure_time(tile_tag + "flip y"):
386-
# Flip tile using fancy indexing
387-
tile = tile[..., ::-1, :]
388-
389376
if self._norm_range is not None:
390377
norm_min, norm_max = self._norm_range
391378
with measure_time(tile_tag + "normalize_min_max"):
@@ -522,20 +509,22 @@ def compute_tile_from_source_tile(self,
522509
return image
523510

524511

525-
class ArrayImage(OpImage):
512+
class SourceArrayImage(OpImage):
526513
"""
527-
A tiled image created an numpy ndarray-like data array.
514+
A tiled image created from a numpy ndarray-like data array.
528515
529516
:param array: a numpy-ndarray-like data array
530517
:param tile_size: the tile size
531518
:param image_id: optional unique image identifier
519+
:param flip_y: Whether to flip pixels in y-direction
532520
:param tile_cache: an optional tile cache
533521
"""
534522

535523
def __init__(self,
536524
array: Union[NDArrayLike, Any],
537525
tile_size: Size2D,
538526
image_id: str = None,
527+
flip_y: bool = False,
539528
tile_cache: Cache = None,
540529
trace_perf: bool = False):
541530
if len(array.shape) != 2:
@@ -556,19 +545,44 @@ def __init__(self,
556545
trace_perf=trace_perf)
557546
is_xarray_like = hasattr(array, 'data') and hasattr(array, 'dims') and hasattr(array, 'attrs')
558547
self._array = array.data if is_xarray_like else array
548+
self._flip_y = flip_y
549+
self._tile_offset_y = self.size[1] % self.tile_size[1]
559550

560-
def compute_tile(self, tile_x: int, tile_y: int, rectangle: Rectangle2D) -> NDArrayLike:
551+
def compute_tile(self, tile_x: int, tile_y: int, rectangle: Rectangle2D) \
552+
-> NDArrayLike:
553+
measure_time = self.measure_time
554+
tile_tag = self._get_tile_tag(tile_x, tile_y)
561555
x, y, w, h = rectangle
556+
557+
if self._flip_y:
558+
num_tiles_y = self.num_tiles[1]
559+
tile_size_y = self.tile_size[1]
560+
tile_y = num_tiles_y - 1 - tile_y
561+
if self._tile_offset_y > 0:
562+
if tile_y == 0:
563+
y = 0
564+
h = self._tile_offset_y
565+
else:
566+
y = self._tile_offset_y + (tile_y - 1) * tile_size_y
567+
else:
568+
y = tile_y * tile_size_y
569+
562570
tile = self._array[y:y + h, x:x + w]
571+
if self._flip_y:
572+
with measure_time(tile_tag + "flip y"):
573+
# Flip tile using fancy indexing
574+
tile = tile[..., ::-1, :]
563575
# ensure that our tile size is w x h
564576
return trim_tile(tile, self.tile_size)
565577

566578

567-
def trim_tile(tile: NDArrayLike, expected_tile_size: Size2D, fill_value: float = np.nan) -> NDArrayLike:
579+
def trim_tile(tile: NDArrayLike,
580+
expected_tile_size: Size2D,
581+
fill_value: float = np.nan) -> NDArrayLike:
568582
"""
569583
Trim a tile.
570584
571-
If to small, expand and pad with background value. If to large, crop.
585+
If too small, expand and pad with background value. If too large, crop.
572586
573587
:param tile: The tile
574588
:param expected_tile_size: expected tile size

0 commit comments

Comments
 (0)