22
33from typing import TYPE_CHECKING , Literal , Self
44
5- from async_tiff import TIFF , ImageFileDirectory , ObspecInput
6- from async_tiff . store import ObjectStore
5+ from affine import Affine
6+ from async_tiff import TIFF
77
88from async_geotiff .enums import Compression , Interleaving , PhotometricInterp
99
1010if TYPE_CHECKING :
1111 import pyproj
12- from affine import Affine
12+ from async_tiff import GeoKeyDirectory , ImageFileDirectory , ObspecInput
13+ from async_tiff .store import ObjectStore
1314
1415
1516class GeoTIFF :
@@ -19,13 +20,32 @@ class GeoTIFF:
1920 """The underlying async-tiff TIFF instance that we wrap.
2021 """
2122
23+ _primary_ifd : ImageFileDirectory
24+ """The primary (first) IFD of the GeoTIFF.
25+
26+ Some tags, like most geo tags, only exist on the primary IFD.
27+ """
28+
29+ _gkd : GeoKeyDirectory
30+ """The GeoKeyDirectory of the primary IFD.
31+ """
32+
2233 def __init__ (self , tiff : TIFF ) -> None :
2334 """Create a GeoTIFF from an existing TIFF instance."""
35+
36+ first_ifd = tiff .ifds [0 ]
37+ gkd = first_ifd .geo_key_directory
38+
2439 # Validate that this is indeed a GeoTIFF
25- if not has_geokeys ( tiff . ifds [ 0 ]) :
40+ if gkd is None :
2641 raise ValueError ("TIFF does not contain GeoTIFF keys" )
2742
43+ if len (tiff .ifds ) == 0 :
44+ raise ValueError ("TIFF does not contain any IFDs" )
45+
2846 self ._tiff = tiff
47+ self ._primary_ifd = first_ifd
48+ self ._gkd = gkd
2949
3050 @classmethod
3151 async def open (
@@ -36,7 +56,7 @@ async def open(
3656 prefetch : int = 32768 ,
3757 multiplier : int | float = 2.0 ,
3858 ) -> Self :
39- """Open a new TIFF .
59+ """Open a new GeoTIFF .
4060
4161 Args:
4262 path: The path within the store to read from.
@@ -121,12 +141,12 @@ def compression(self) -> Compression:
121141
122142 @property
123143 def count (self ) -> int :
124- """The number of raster bands in the dataset ."""
144+ """The number of raster bands in the full image ."""
125145 raise NotImplementedError ()
126146
127147 @property
128148 def crs (self ) -> pyproj .CRS :
129- """The dataset’ s coordinate reference system."""
149+ """The dataset' s coordinate reference system."""
130150 raise NotImplementedError ()
131151
132152 @property
@@ -138,8 +158,8 @@ def dtypes(self) -> list[str]:
138158
139159 @property
140160 def height (self ) -> int :
141- """The height (number of rows) of the dataset ."""
142- raise NotImplementedError ()
161+ """The height (number of rows) of the full image ."""
162+ return self . _primary_ifd . image_height
143163
144164 def index (
145165 self ,
@@ -208,12 +228,48 @@ def transform(self) -> Affine:
208228
209229 This transform maps pixel row/column coordinates to coordinates in the dataset's coordinate reference system.
210230 """
211- raise NotImplementedError ()
231+ if (tie_points := self ._primary_ifd .model_tiepoint ) and (
232+ model_scale := self ._primary_ifd .model_pixel_scale
233+ ):
234+ x_origin = tie_points [3 ]
235+ y_origin = tie_points [4 ]
236+ x_resolution = model_scale [0 ]
237+ y_resolution = - model_scale [1 ]
238+
239+ return Affine (x_resolution , 0 , x_origin , 0 , y_resolution , y_origin )
240+
241+ if model_transformation := self ._primary_ifd .model_transformation :
242+ # ModelTransformation is a 4x4 matrix in row-major order
243+ # [0 1 2 3 ] [a b 0 c]
244+ # [4 5 6 7 ] = [d e 0 f]
245+ # [8 9 10 11] [0 0 1 0]
246+ # [12 13 14 15] [0 0 0 1]
247+ x_origin = model_transformation [3 ]
248+ y_origin = model_transformation [7 ]
249+ row_rotation = model_transformation [1 ]
250+ col_rotation = model_transformation [4 ]
251+
252+ # TODO: confirm these are correct
253+ # Why does geotiff.js square and then square-root them?
254+ # https://github.com/developmentseed/async-geotiff/issues/7
255+ x_resolution = model_transformation [0 ]
256+ y_resolution = - model_transformation [5 ]
257+
258+ return Affine (
259+ model_transformation [0 ],
260+ row_rotation ,
261+ x_origin ,
262+ col_rotation ,
263+ model_transformation [5 ],
264+ y_origin ,
265+ )
266+
267+ raise ValueError ("The image does not have an affine transformation." )
212268
213269 @property
214270 def width (self ) -> int :
215- """The width (number of columns) of the dataset ."""
216- raise NotImplementedError ()
271+ """The width (number of columns) of the full image ."""
272+ return self . _primary_ifd . image_width
217273
218274 def xy (
219275 self ,
0 commit comments