Advertisement
zelenooki87

auto_split.py

May 25th, 2025 (edited)
157
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.35 KB | None | 0 0
  1. #chaiNNer-windows-latest-nightly\resources\src\nodes\impl\upscale\auto_split.py
  2. from __future__ import annotations
  3.  
  4. import math
  5. from typing import Callable, Union
  6.  
  7. import numpy as np
  8. from sanic.log import logger
  9.  
  10. from ...utils.utils import Region, Size, get_h_w_c
  11. from .exact_split import exact_split
  12. from .tile_blending import BlendDirection, TileBlender, TileOverlap, half_sin_blend_fn
  13. from .tiler import Tiler
  14.  
  15.  
  16. class Split:
  17.     pass
  18.  
  19.  
  20. SplitImageOp = Callable[[np.ndarray, Region], Union[np.ndarray, Split]]
  21.  
  22.  
  23. def auto_split(
  24.     img: np.ndarray,
  25.     upscale: SplitImageOp,
  26.     tiler: Tiler,
  27.     overlap: int = 128,
  28. ) -> np.ndarray:
  29.     """
  30.    Splits the image into tiles according to the given tiler.
  31.  
  32.    This method only changes the size of the given image, the tiles passed into the upscale function will have same number of channels.
  33.  
  34.    The region passed into the upscale function is the region of the current tile.
  35.    The size of the region is guaranteed to be the same as the size of the given tile.
  36.  
  37.    ## Padding
  38.  
  39.    If the given tiler allows smaller tile sizes, then it is guaranteed that no padding will be added.
  40.    Otherwise, no padding is only guaranteed if the starting tile size is not larger than the size of the given image.
  41.    """
  42.  
  43.     h, w, c = get_h_w_c(img)
  44.     split = _max_split if tiler.allow_smaller_tile_size() else _exact_split
  45.  
  46.     return split(
  47.         img,
  48.         upscale=upscale,
  49.         starting_tile_size=tiler.starting_tile_size(w, h, c),
  50.         split_tile_size=tiler.split,
  51.         overlap=overlap,
  52.     )
  53.  
  54.  
  55. class _SplitEx(Exception):
  56.     pass
  57.  
  58.  
  59. def _exact_split(
  60.     img: np.ndarray,
  61.     upscale: SplitImageOp,
  62.     starting_tile_size: Size,
  63.     split_tile_size: Callable[[Size], Size],
  64.     overlap: int,
  65. ) -> np.ndarray:
  66.     h, w, c = get_h_w_c(img)
  67.     logger.debug(
  68.         f"Exact size split image ({w}x{h}px @ {c}) with exact tile size {starting_tile_size[0]}x{starting_tile_size[1]}px."
  69.     )
  70.  
  71.     def no_split_upscale(i: np.ndarray, r: Region) -> np.ndarray:
  72.         result = upscale(i, r)
  73.         if isinstance(result, Split):
  74.             raise _SplitEx
  75.         return result
  76.  
  77.     MAX_ITER = 20  # noqa: N806
  78.  
  79.     for _ in range(MAX_ITER):
  80.         try:
  81.             max_overlap = min(*starting_tile_size) // 2  # umesto // 4
  82.             return exact_split(
  83.                 img=img,
  84.                 exact_size=starting_tile_size,
  85.                 upscale=no_split_upscale,
  86.                 overlap=min(max_overlap, overlap),
  87.             )
  88.         except _SplitEx:
  89.             starting_tile_size = split_tile_size(starting_tile_size)
  90.  
  91.     raise ValueError(f"Aborting after {MAX_ITER} splits. Unable to upscale image.")
  92.  
  93.  
  94. def _max_split(
  95.     img: np.ndarray,
  96.     upscale: SplitImageOp,
  97.     starting_tile_size: Size,
  98.     split_tile_size: Callable[[Size], Size],
  99.     overlap: int,
  100. ) -> np.ndarray:
  101.     """
  102.    Splits the image into tiles with at most the given tile size.
  103.  
  104.    If the upscale method requests a split, then the tile size will be lowered.
  105.    """
  106.  
  107.     h, w, c = get_h_w_c(img)
  108.  
  109.     img_region = Region(0, 0, w, h)
  110.  
  111.     max_tile_size = starting_tile_size
  112.     logger.debug(
  113.         f"Auto split image ({w}x{h}px @ {c}) with initial tile size {max_tile_size}."
  114.     )
  115.  
  116.     if w <= max_tile_size[0] and h <= max_tile_size[1]:
  117.         # the image might be small enough so that we don't have to split at all
  118.         upscale_result = upscale(img, img_region)
  119.         if not isinstance(upscale_result, Split):
  120.             return upscale_result
  121.  
  122.         # the image was too large
  123.         max_tile_size = split_tile_size(max_tile_size)
  124.  
  125.         logger.warn(
  126.             f"Unable to upscale the whole image at once. Reduced tile size to {max_tile_size}."
  127.         )
  128.  
  129.     # The upscale method is allowed to request splits at any time.
  130.     # When a split occurs, we have to "restart" the loop and
  131.     # this variable allow us to split the already processed tiles.
  132.     start_y = 0
  133.  
  134.     # To allocate the result image, we need to know the upscale factor first,
  135.     # and we only get to know this factor after the first successful upscale.
  136.     result: TileBlender | None = None
  137.     scale: int = 0
  138.     out_channels: int = 0
  139.  
  140.     restart = True
  141.     while restart:
  142.         restart = False
  143.  
  144.         # This is a bit complex.
  145.         # We don't actually use the current tile size to partition the image.
  146.         # If we did, then tile_size=1024 and w=1200 would result in very uneven tiles.
  147.         # Instead, we use tile_size to calculate how many tiles we get in the x and y direction
  148.         # and then calculate the optimal tile size for the x and y direction using the counts.
  149.         # This yields optimal tile sizes which should prevent unnecessary splitting.
  150.         tile_count_x = math.ceil(w / max_tile_size[0])
  151.         tile_count_y = math.ceil(h / max_tile_size[1])
  152.         tile_size_x = math.ceil(w / tile_count_x)
  153.         tile_size_y = math.ceil(h / tile_count_y)
  154.  
  155.         logger.debug(
  156.             f"Currently {tile_count_x}x{tile_count_y} tiles each {tile_size_x}x{tile_size_y}px."
  157.         )
  158.  
  159.         prev_row_result: TileBlender | None = None
  160.  
  161.         for y in range(tile_count_y):
  162.             if y < start_y:
  163.                 continue
  164.  
  165.             row_result: TileBlender | None = None
  166.             row_overlap: TileOverlap | None = None
  167.  
  168.             for x in range(tile_count_x):
  169.                 tile = Region(
  170.                     x * tile_size_x, y * tile_size_y, tile_size_x, tile_size_y
  171.                 ).intersect(img_region)
  172.                 pad = img_region.child_padding(tile).min(overlap)
  173.                 padded_tile = tile.add_padding(pad)
  174.  
  175.                 upscale_result = upscale(padded_tile.read_from(img), padded_tile)
  176.  
  177.                 if isinstance(upscale_result, Split):
  178.                     max_tile_size = split_tile_size(max_tile_size)
  179.  
  180.                     new_tile_count_y = math.ceil(h / max_tile_size[1])
  181.                     new_tile_size_y = math.ceil(h / new_tile_count_y)
  182.                     start_y = (y * tile_size_x) // new_tile_size_y
  183.  
  184.                     logger.debug(
  185.                         f"Split occurred. New tile size is {max_tile_size}. Starting at row {start_y}."
  186.                     )
  187.  
  188.                     # reset result
  189.                     if result is not None:
  190.                         # we already added at least one row, so we have to set the offset back
  191.                         result.offset = start_y * new_tile_size_y
  192.  
  193.                     restart = True
  194.                     break
  195.  
  196.                 # figure out by how much the image was upscaled by
  197.                 up_h, up_w, up_c = get_h_w_c(upscale_result)
  198.                 current_scale = up_h // padded_tile.height
  199.                 assert current_scale > 0
  200.                 assert padded_tile.height * current_scale == up_h
  201.                 assert padded_tile.width * current_scale == up_w
  202.  
  203.                 if row_result is None:
  204.                     # allocate the result image
  205.                     scale = current_scale
  206.                     out_channels = up_c
  207.                     row_result = TileBlender(
  208.                         width=w * scale,
  209.                         height=padded_tile.height * scale,
  210.                         channels=out_channels,
  211.                         direction=BlendDirection.X,
  212.                         blend_fn=half_sin_blend_fn,
  213.                         _prev=prev_row_result,
  214.                     )
  215.                     prev_row_result = row_result
  216.                     row_overlap = TileOverlap(pad.top * scale, pad.bottom * scale)
  217.  
  218.                 assert current_scale == scale
  219.  
  220.                 # add to row
  221.                 row_result.add_tile(
  222.                     upscale_result, TileOverlap(pad.left * scale, pad.right * scale)
  223.                 )
  224.  
  225.             if restart:
  226.                 break
  227.  
  228.             assert row_result is not None
  229.             assert row_overlap is not None
  230.  
  231.             if result is None:
  232.                 result = TileBlender(
  233.                     width=w * scale,
  234.                     height=h * scale,
  235.                     channels=out_channels,
  236.                     direction=BlendDirection.Y,
  237.                     blend_fn=half_sin_blend_fn,
  238.                 )
  239.  
  240.             # add row
  241.             result.add_tile(row_result.get_result(), row_overlap)
  242.  
  243.     assert result is not None
  244.     return result.get_result()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement