|
1 | 1 | from collections.abc import Callable, Iterable |
2 | 2 | from dataclasses import dataclass |
| 3 | +from typing import cast |
3 | 4 |
|
4 | 5 | from PIL import Image, ImageChops, ImageStat |
5 | 6 | from typing_extensions import TypedDict |
@@ -446,17 +447,83 @@ def worst_tile_contrast( |
446 | 447 | tile_count = ((region.width + tile_size - 1) // tile_size) * ( |
447 | 448 | (region.height + tile_size - 1) // tile_size |
448 | 449 | ) |
| 450 | + background_region = background_image.crop( |
| 451 | + (region.x, region.y, region.right, region.bottom) |
| 452 | + ).convert("RGBA") |
| 453 | + foreground_region = foreground_image.crop( |
| 454 | + (region.x, region.y, region.right, region.bottom) |
| 455 | + ).convert("RGBA") |
| 456 | + foreground_alpha_region = foreground_alpha.crop( |
| 457 | + (region.x, region.y, region.right, region.bottom) |
| 458 | + ) |
449 | 459 |
|
450 | 460 | worst: TiledContrastMeasurement | None = None |
451 | 461 | for tile in _tiled_regions(region, tile_size): |
452 | | - alpha_tile = foreground_alpha.crop((tile.x, tile.y, tile.right, tile.bottom)) |
| 462 | + local_tile = BBox(tile.x - region.x, tile.y - region.y, tile.width, tile.height) |
| 463 | + alpha_tile = foreground_alpha_region.crop( |
| 464 | + (local_tile.x, local_tile.y, local_tile.right, local_tile.bottom) |
| 465 | + ) |
453 | 466 | if alpha_tile.getbbox() is None: |
454 | 467 | continue |
455 | 468 |
|
456 | | - background = average_visible_background(background_image, tile, mask=foreground_alpha) |
457 | | - foreground_crop = foreground_image.crop((tile.x, tile.y, tile.right, tile.bottom)) |
458 | | - foreground_mask = foreground_alpha.crop((tile.x, tile.y, tile.right, tile.bottom)) |
459 | | - foreground = tuple(ImageStat.Stat(foreground_crop, mask=foreground_mask).mean[:3]) |
| 469 | + measurement = _tile_contrast( |
| 470 | + background_region, |
| 471 | + foreground_region, |
| 472 | + local_tile, |
| 473 | + tile=tile, |
| 474 | + tile_count=tile_count, |
| 475 | + ) |
| 476 | + if measurement is not None and (worst is None or measurement.contrast < worst.contrast): |
| 477 | + worst = measurement |
| 478 | + return worst |
| 479 | + |
| 480 | + |
| 481 | +def _tile_contrast( |
| 482 | + background_region: Image.Image, |
| 483 | + foreground_region: Image.Image, |
| 484 | + region: BBox, |
| 485 | + *, |
| 486 | + tile: BBox, |
| 487 | + tile_count: int, |
| 488 | +) -> TiledContrastMeasurement | None: |
| 489 | + background_crop = background_region.crop((region.x, region.y, region.right, region.bottom)) |
| 490 | + foreground_crop = foreground_region.crop((region.x, region.y, region.right, region.bottom)) |
| 491 | + background_pixels = background_crop.load() |
| 492 | + foreground_pixels = foreground_crop.load() |
| 493 | + assert background_pixels is not None and foreground_pixels is not None |
| 494 | + groups: dict[tuple[int, int, int], list[float]] = {} |
| 495 | + |
| 496 | + for y in range(region.height): |
| 497 | + for x in range(region.width): |
| 498 | + background_pixel = cast(tuple[int, int, int, int], background_pixels[x, y]) |
| 499 | + foreground_pixel = cast(tuple[int, int, int, int], foreground_pixels[x, y]) |
| 500 | + foreground_r, foreground_g, foreground_b, foreground_a = foreground_pixel |
| 501 | + if foreground_a == 0: |
| 502 | + continue |
| 503 | + |
| 504 | + background_alpha = background_pixel[3] / 255 |
| 505 | + visible_background = ( |
| 506 | + background_pixel[0] * background_alpha + 255 * (1 - background_alpha), |
| 507 | + background_pixel[1] * background_alpha + 255 * (1 - background_alpha), |
| 508 | + background_pixel[2] * background_alpha + 255 * (1 - background_alpha), |
| 509 | + ) |
| 510 | + key = (foreground_r, foreground_g, foreground_b) |
| 511 | + group = groups.setdefault(key, [0.0, 0.0, 0.0, 0.0, 0.0]) |
| 512 | + group[0] += visible_background[0] |
| 513 | + group[1] += visible_background[1] |
| 514 | + group[2] += visible_background[2] |
| 515 | + group[3] += 1 |
| 516 | + group[4] = max(group[4], foreground_a) |
| 517 | + |
| 518 | + worst: TiledContrastMeasurement | None = None |
| 519 | + for foreground_raw, group in groups.items(): |
| 520 | + background = (group[0] / group[3], group[1] / group[3], group[2] / group[3]) |
| 521 | + opacity = group[4] / 255 |
| 522 | + foreground = ( |
| 523 | + foreground_raw[0] * opacity + background[0] * (1 - opacity), |
| 524 | + foreground_raw[1] * opacity + background[1] * (1 - opacity), |
| 525 | + foreground_raw[2] * opacity + background[2] * (1 - opacity), |
| 526 | + ) |
460 | 527 | contrast = contrast_ratio(foreground, background) |
461 | 528 | if worst is None or contrast < worst.contrast: |
462 | 529 | worst = TiledContrastMeasurement( |
|
0 commit comments