|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import math |
3 | 4 | from dataclasses import dataclass, field |
4 | 5 | from functools import cached_property |
5 | | -from typing import TYPE_CHECKING, Self, cast |
| 6 | +from typing import TYPE_CHECKING, Self |
6 | 7 |
|
7 | 8 | import numpy as np |
8 | 9 | from affine import Affine |
@@ -161,17 +162,25 @@ def bounds(self) -> tuple[float, float, float, float]: |
161 | 162 | lower left x, lower left y, upper right x, upper right y |
162 | 163 |
|
163 | 164 | """ |
164 | | - transform = self.transform |
165 | | - |
166 | | - # TODO: Remove type casts once affine supports typing overloads for matmul |
167 | | - # https://github.com/rasterio/affine/pull/137 |
168 | | - (left, top) = cast("tuple[float, float]", transform * (0, 0)) |
169 | | - (right, bottom) = cast( |
170 | | - "tuple[float, float]", |
171 | | - transform * (self.width, self.height), |
| 165 | + # Transform all four corners to handle rotated images correctly |
| 166 | + # Pixel coordinates of corners: (x, y, 1) for affine transform |
| 167 | + corners_pixel = np.array( |
| 168 | + [ |
| 169 | + [0, 0, 1], |
| 170 | + [self.width, 0, 1], |
| 171 | + [0, self.height, 1], |
| 172 | + [self.width, self.height, 1], |
| 173 | + ], |
172 | 174 | ) |
173 | 175 |
|
174 | | - return (left, bottom, right, top) |
| 176 | + # Apply affine transform: transform @ corners_pixel.T |
| 177 | + transform_matrix = np.array(self.transform).reshape(3, 3) |
| 178 | + corners_geo = (transform_matrix @ corners_pixel.T)[:2].T |
| 179 | + |
| 180 | + min_x, min_y = corners_geo.min(axis=0) |
| 181 | + max_x, max_y = corners_geo.max(axis=0) |
| 182 | + |
| 183 | + return (float(min_x), float(min_y), float(max_x), float(max_y)) |
175 | 184 |
|
176 | 185 | # @property |
177 | 186 | # def colorinterp(self) -> list[str]: |
@@ -346,7 +355,11 @@ def photometric(self) -> PhotometricInterpretation | None: # noqa: PLR0911 |
346 | 355 | def res(self) -> tuple[float, float]: |
347 | 356 | """Return the (width, height) of pixels in the units of its CRS.""" |
348 | 357 | transform = self.transform |
349 | | - return (transform.a, -transform.e) |
| 358 | + # For rotated images, resolution is the magnitude of the pixel size |
| 359 | + # calculated from the transform matrix components |
| 360 | + res_x = math.sqrt(transform.a**2 + transform.d**2) |
| 361 | + res_y = math.sqrt(transform.b**2 + transform.e**2) |
| 362 | + return (res_x, res_y) |
350 | 363 |
|
351 | 364 | @property |
352 | 365 | def shape(self) -> tuple[int, int]: |
|
0 commit comments