@@ -256,43 +256,68 @@ def _alpha_region(
256256 return mask .point (lambda value : 255 if value else 0 )
257257
258258
259- def average_visible_background (image : Image .Image , region : BBox ) -> tuple [float , float , float ]:
259+ def average_visible_background (
260+ image : Image .Image , region : BBox , mask : Image .Image | None = None
261+ ) -> tuple [float , float , float ]:
260262 crop = image .crop ((region .x , region .y , region .right , region .bottom ))
261- mean_r , mean_g , mean_b , mean_a = ImageStat .Stat (crop ).mean
263+ mask_crop = (
264+ None if mask is None else mask .crop ((region .x , region .y , region .right , region .bottom ))
265+ )
266+ mean_r , mean_g , mean_b , mean_a = ImageStat .Stat (crop , mask = mask_crop ).mean
262267
263268 # Transparent areas read as white, matching JPEG export and typical viewers.
264269 alpha = mean_a / 255
265270 return tuple (channel * alpha + 255 * (1 - alpha ) for channel in (mean_r , mean_g , mean_b ))
266271
267272
268273def worst_tile_contrast (
269- image : Image .Image ,
274+ background_image : Image .Image ,
275+ foreground_image : Image .Image ,
270276 region : BBox ,
271- foregrounds : Iterable [tuple [float , float , float ]],
272277 * ,
273278 tile_size : int ,
274279) -> TiledContrastMeasurement | None :
275- """Measure the lowest foreground/background contrast across tiled samples."""
276- tiles = list (_tiled_regions (region , tile_size ))
277- if not tiles :
278- return None
280+ """Measure the lowest text-pixel contrast across tiled samples."""
281+ foreground_alpha = foreground_image .getchannel ("A" ).point (lambda value : 255 if value else 0 )
282+ tile_count = _tile_count (region , tile_size )
279283
280284 worst : TiledContrastMeasurement | None = None
281- for tile in tiles :
282- background = average_visible_background (image , tile )
283- for foreground in foregrounds :
284- contrast = contrast_ratio (foreground , background )
285- if worst is None or contrast < worst .contrast :
286- worst = TiledContrastMeasurement (
287- contrast = contrast ,
288- foreground = foreground ,
289- background = background ,
290- tile = tile ,
291- tile_count = len (tiles ),
292- )
285+ for tile in _tiled_regions (region , tile_size ):
286+ alpha_tile = foreground_alpha .crop ((tile .x , tile .y , tile .right , tile .bottom ))
287+ if alpha_tile .getbbox () is None :
288+ continue
289+
290+ background = average_visible_background (background_image , tile , mask = foreground_alpha )
291+ foreground = _average_visible_foreground (foreground_image , tile , foreground_alpha )
292+ contrast = contrast_ratio (foreground , background )
293+ if worst is None or contrast < worst .contrast :
294+ worst = TiledContrastMeasurement (
295+ contrast = contrast ,
296+ foreground = foreground ,
297+ background = background ,
298+ tile = tile ,
299+ tile_count = tile_count ,
300+ )
293301 return worst
294302
295303
304+ def _average_visible_foreground (
305+ image : Image .Image , region : BBox , mask : Image .Image
306+ ) -> tuple [float , float , float ]:
307+ crop = image .crop ((region .x , region .y , region .right , region .bottom ))
308+ mask_crop = mask .crop ((region .x , region .y , region .right , region .bottom ))
309+ mean_r , mean_g , mean_b , * _ = ImageStat .Stat (crop , mask = mask_crop ).mean
310+ return mean_r , mean_g , mean_b
311+
312+
313+ def _tile_count (region : BBox , tile_size : int ) -> int :
314+ if tile_size <= 0 :
315+ raise ValueError ("tile_size must be positive" )
316+ cols = (region .width + tile_size - 1 ) // tile_size
317+ rows = (region .height + tile_size - 1 ) // tile_size
318+ return cols * rows
319+
320+
296321def _tiled_regions (region : BBox , tile_size : int ) -> Iterable [BBox ]:
297322 if tile_size <= 0 :
298323 raise ValueError ("tile_size must be positive" )
0 commit comments