@@ -465,7 +465,10 @@ def to_geotiff(data: xr.DataArray | np.ndarray, path: str, *,
465465 compression = compression ,
466466 compression_level = compression_level ,
467467 tile_size = tile_size ,
468- predictor = predictor )
468+ predictor = predictor ,
469+ cog = cog ,
470+ overview_levels = overview_levels ,
471+ overview_resampling = overview_resampling )
469472 return
470473 except (ImportError , Exception ):
471474 pass # fall through to CPU path
@@ -1154,14 +1157,21 @@ def write_geotiff_gpu(data, path: str, *,
11541157 compression : str = 'zstd' ,
11551158 compression_level : int | None = None ,
11561159 tile_size : int = 256 ,
1157- predictor : bool = False ) -> None :
1160+ predictor : bool = False ,
1161+ cog : bool = False ,
1162+ overview_levels : list [int ] | None = None ,
1163+ overview_resampling : str = 'mean' ) -> None :
11581164 """Write a CuPy-backed DataArray as a GeoTIFF with GPU compression.
11591165
11601166 Tiles are extracted and compressed on the GPU via nvCOMP, then
11611167 assembled into a TIFF file on CPU. The CuPy array stays on device
11621168 throughout compression -- only the compressed bytes transfer to CPU
11631169 for file writing.
11641170
1171+ When ``cog=True``, generates overview pyramids on GPU and writes a
1172+ Cloud Optimized GeoTIFF with all IFDs at the file start for
1173+ efficient range-request access.
1174+
11651175 Falls back to CPU compression if nvCOMP is not available.
11661176
11671177 Parameters
@@ -1184,13 +1194,22 @@ def write_geotiff_gpu(data, path: str, *,
11841194 Tile size in pixels (default 256).
11851195 predictor : bool
11861196 Apply horizontal differencing predictor.
1197+ cog : bool
1198+ Write as Cloud Optimized GeoTIFF with overviews.
1199+ overview_levels : list[int] or None
1200+ Overview decimation factors (e.g. [2, 4, 8]). Only used when
1201+ cog=True. If None and cog=True, auto-generates levels by
1202+ halving until the smallest overview fits in a single tile.
1203+ overview_resampling : str
1204+ Resampling method for overviews: 'mean' (default), 'nearest',
1205+ 'min', 'max', 'median', or 'mode'.
11871206 """
11881207 try :
11891208 import cupy
11901209 except ImportError :
11911210 raise ImportError ("cupy is required for GPU writes" )
11921211
1193- from ._gpu_decode import gpu_compress_tiles
1212+ from ._gpu_decode import gpu_compress_tiles , make_overview_gpu
11941213 from ._writer import (
11951214 _compression_tag , _assemble_tiff , _write_bytes ,
11961215 GeoTransform as _GT ,
@@ -1245,28 +1264,45 @@ def write_geotiff_gpu(data, path: str, *,
12451264 comp_tag = _compression_tag (compression )
12461265 pred_val = 2 if predictor else 1
12471266
1248- # GPU compress
1249- compressed_tiles = gpu_compress_tiles (
1250- arr , tile_size , tile_size , width , height ,
1251- comp_tag , pred_val , np_dtype , samples )
1252-
1253- # Build offset/bytecount lists
1254- rel_offsets = []
1255- byte_counts = []
1256- offset = 0
1257- for tile in compressed_tiles :
1258- rel_offsets .append (offset )
1259- byte_counts .append (len (tile ))
1260- offset += len (tile )
1261-
1262- # Assemble TIFF on CPU (only metadata + compressed bytes)
1263- # _assemble_tiff needs an array in parts[0] to detect samples_per_pixel
1264- shape_stub = np .empty ((1 , 1 , samples ) if samples > 1 else (1 , 1 ), dtype = np_dtype )
1265- parts = [(shape_stub , width , height , rel_offsets , byte_counts , compressed_tiles )]
1267+ def _gpu_compress_to_part (gpu_arr , w , h , spp ):
1268+ """Compress a GPU array into a (stub, w, h, offsets, counts, tiles) tuple."""
1269+ compressed = gpu_compress_tiles (
1270+ gpu_arr , tile_size , tile_size , w , h ,
1271+ comp_tag , pred_val , np_dtype , spp )
1272+ rel_off = []
1273+ bc = []
1274+ off = 0
1275+ for tile in compressed :
1276+ rel_off .append (off )
1277+ bc .append (len (tile ))
1278+ off += len (tile )
1279+ stub = np .empty ((1 , 1 , spp ) if spp > 1 else (1 , 1 ), dtype = np_dtype )
1280+ return (stub , w , h , rel_off , bc , compressed )
1281+
1282+ # Full resolution
1283+ parts = [_gpu_compress_to_part (arr , width , height , samples )]
1284+
1285+ # Overview generation
1286+ if cog :
1287+ if overview_levels is None :
1288+ overview_levels = []
1289+ oh , ow = height , width
1290+ while oh > tile_size and ow > tile_size :
1291+ oh //= 2
1292+ ow //= 2
1293+ if oh > 0 and ow > 0 :
1294+ overview_levels .append (len (overview_levels ) + 1 )
1295+
1296+ current = arr
1297+ for _ in overview_levels :
1298+ current = make_overview_gpu (current , method = overview_resampling )
1299+ oh , ow = current .shape [:2 ]
1300+ parts .append (_gpu_compress_to_part (current , ow , oh , samples ))
12661301
12671302 file_bytes = _assemble_tiff (
12681303 width , height , np_dtype , comp_tag , predictor , True , tile_size ,
1269- parts , geo_transform , epsg , nodata , is_cog = False ,
1304+ parts , geo_transform , epsg , nodata ,
1305+ is_cog = (cog and len (parts ) > 1 ),
12701306 raster_type = raster_type )
12711307
12721308 _write_bytes (file_bytes , path )
0 commit comments