@@ -703,24 +703,39 @@ def extract_geo_info_with_overview_inheritance(
703703 """Extract geo metadata, inheriting from level 0 when the IFD lacks it.
704704
705705 Wraps :func:`extract_geo_info` for overview reads. GDAL-style COG
706- writers (including this package's :func:`to_geotiff`) put the
707- GeoKeyDirectory, ModelPixelScale and ModelTiepoint only on the
708- level-0 IFD. Calling ``extract_geo_info`` directly on an overview
709- IFD therefore returns a default :class:`GeoTransform` with
710- ``has_georef=False`` and no CRS, so overview reads silently lose
711- their georeferencing.
706+ writers (including this package's :func:`to_geotiff`) put a handful
707+ of tags only on the level-0 IFD:
712708
713- When ``ifd`` is a reduced-resolution overview (NewSubfileType bit 0
714- set) that lacks its own georef, we re-run ``extract_geo_info`` on
715- the first full-resolution IFD (NewSubfileType bit 0 clear, bit 2
716- clear) and rescale the pixel size by ``width_full / width_overview``
717- so coords cover the same extent as level 0.
709+ * GeoKeyDirectory, ModelPixelScale, ModelTiepoint (georef)
710+ * GDAL_NODATA, GDAL_METADATA (per-IFD pass-through tags)
711+ * XResolution, YResolution, ResolutionUnit (resolution tags)
712+ * ColorMap, ImageDescription, ExtraSamples (extra-tag pass-through)
713+
714+ Calling ``extract_geo_info`` directly on an overview IFD therefore
715+ returns a default :class:`GeoTransform` with ``has_georef=False``,
716+ no CRS, and a ``nodata=None`` field, so overview reads silently
717+ lose their georeferencing and their nodata sentinel.
718718
719- If the overview IFD already carries its own geokeys (some writers do
720- replicate them), this returns its own ``extract_geo_info`` output
721- unchanged. If no full-resolution sibling exists or the parent's geo
722- info is also missing, the overview's own (possibly empty) info is
723- returned -- callers get the same fallback behaviour they used to.
719+ When ``ifd`` is a reduced-resolution overview (NewSubfileType bit 0
720+ set), we re-run ``extract_geo_info`` on the first full-resolution
721+ IFD (NewSubfileType bit 0 clear, bit 2 clear). Per-IFD pass-through
722+ tags (nodata, GDAL metadata, resolution, colormap, extra tags,
723+ image description, extra samples) are inherited when the overview
724+ lacks its own value, regardless of whether the overview has its own
725+ georef. The transform and CRS-side fields are additionally
726+ inherited when the overview lacks its own georef, with the pixel
727+ size rescaled by ``width_full / width_overview`` so coords cover
728+ the same extent as level 0.
729+
730+ If the overview IFD already carries its own value for a given
731+ field, that value wins -- inheritance is per-field and only fills
732+ in missing entries. If no full-resolution sibling exists, the
733+ overview's own (possibly empty) info is returned -- callers get the
734+ same fallback behaviour they used to.
735+
736+ Inheriting nodata + the rich-tag set fixes #1739 (silent numerical
737+ corruption when reading COG overview pixels because attrs['nodata']
738+ was lost). The georef inheritance is the original fix from #1640.
724739
725740 Parameters
726741 ----------
@@ -744,7 +759,7 @@ def extract_geo_info_with_overview_inheritance(
744759 # page IFDs (bit 1) are filtered out by ``select_overview_ifd``
745760 # before reaching here, so we never inherit a mask's geo info.
746761 is_overview = bool (ifd .subfile_type & 1 )
747- if not is_overview or info . has_georef :
762+ if not is_overview :
748763 return info
749764
750765 # Find the level-0 IFD: NewSubfileType has bit 0 clear (not an
@@ -763,6 +778,51 @@ def extract_geo_info_with_overview_inheritance(
763778 return info
764779
765780 base_info = extract_geo_info (base_ifd , data , byte_order )
781+
782+ # Inherit the per-IFD metadata that the COG writer emits only on the
783+ # level-0 IFD: GDAL_NODATA, GDAL_METADATA, x/y resolution, colormap,
784+ # extra tags, image description, extra samples. Without this block
785+ # an overview read silently drops attrs['nodata'] (so the sentinel
786+ # pixels the writer baked into the overview survive as ordinary data
787+ # and poison downstream stats) and attrs['gdal_metadata'] (user
788+ # metadata loss). See issue #1739.
789+ #
790+ # Each field is inherited only when the overview lacks its own
791+ # value, so an overview IFD that does re-declare any of these keeps
792+ # its own copy. Mirrors the gate the CRS-side inheritance applies
793+ # below: prefer the overview's own value when present.
794+ if info .nodata is None and base_info .nodata is not None :
795+ info .nodata = base_info .nodata
796+ if (info .gdal_metadata is None
797+ and base_info .gdal_metadata is not None ):
798+ info .gdal_metadata = base_info .gdal_metadata
799+ if (info .gdal_metadata_xml is None
800+ and base_info .gdal_metadata_xml is not None ):
801+ info .gdal_metadata_xml = base_info .gdal_metadata_xml
802+ if info .x_resolution is None and base_info .x_resolution is not None :
803+ info .x_resolution = base_info .x_resolution
804+ if info .y_resolution is None and base_info .y_resolution is not None :
805+ info .y_resolution = base_info .y_resolution
806+ if (info .resolution_unit is None
807+ and base_info .resolution_unit is not None ):
808+ info .resolution_unit = base_info .resolution_unit
809+ if info .colormap is None and base_info .colormap is not None :
810+ info .colormap = base_info .colormap
811+ if info .extra_tags is None and base_info .extra_tags is not None :
812+ info .extra_tags = base_info .extra_tags
813+ if (info .image_description is None
814+ and base_info .image_description is not None ):
815+ info .image_description = base_info .image_description
816+ if (info .extra_samples is None
817+ and base_info .extra_samples is not None ):
818+ info .extra_samples = base_info .extra_samples
819+
820+ # If the overview already has its own georef, the rest of the
821+ # inheritance (transform + CRS-side fields) is unnecessary -- return
822+ # now with just the per-IFD-tag inheritance applied above.
823+ if info .has_georef :
824+ return info
825+
766826 if not base_info .has_georef :
767827 return info
768828
0 commit comments