File size: 7,275 Bytes
e0336bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# Base configuration for scaling bucket options
_BASE_RESOLUTION = 640
_BASE_BUCKET_OPTIONS = [
    (416, 960), (448, 864), (480, 832), (512, 768), (544, 704),
    (576, 672), (608, 640), (640, 608), (672, 576), (704, 544),
    (768, 512), (832, 480), (864, 448), (960, 416),
]

# Cache for generated bucket options to avoid redundant calculations
_generated_bucket_cache = {}

def _round_to_multiple(number, multiple):
    """Rounds a number to the nearest multiple of a given number."""
    if multiple == 0:
        # Default behavior: round to nearest int. Could also raise an error.
        return int(round(number)) 
    return int(multiple * round(float(number) / multiple))

def _adjust_resolution(resolution, divisor=32):
    """
    Adjusts a given resolution to the nearest multiple of 'divisor'.
    If the input resolution is positive but rounds to 0 (e.g., resolution=10, divisor=32),
    it's adjusted to 'divisor'.
    If the input resolution is non-positive (<=0), it defaults to 'divisor'.
    """
    if resolution <= 0:
        return divisor  # Default to minimum valid resolution for non-positive inputs
    
    adjusted = _round_to_multiple(resolution, divisor)
    
    # If resolution was positive but _round_to_multiple resulted in 0 
    # (e.g. input 10 for divisor 32 rounds to 0), ensure it's at least the divisor.
    if adjusted == 0: 
        return divisor 
    return adjusted

def generate_scaled_buckets(target_resolution_input, 
                            base_resolution=_BASE_RESOLUTION, 
                            base_options=_BASE_BUCKET_OPTIONS, 
                            divisor=32):
    """
    Generates scaled bucket options for a target resolution.
    
    The target_resolution_input is first adjusted to the nearest multiple of 'divisor'.
    Bucket dimensions are scaled from 'base_options' (which are for 'base_resolution')
    to the adjusted target resolution. These scaled dimensions are then rounded to the 
    nearest multiple of 'divisor' and ensured to be at least 'divisor'.
    
    Args:
        target_resolution_input (int): The desired target resolution.
        base_resolution (int): The resolution for which 'base_options' are defined.
        base_options (list of tuples): A list of (height, width) tuples for 'base_resolution'.
        divisor (int): The number that resolutions and bucket dimensions should be multiples of.

    Returns:
        list of tuples: Scaled and adjusted bucket options (height, width).
    """
    # Adjust the target resolution for scaling
    actual_target_resolution = _adjust_resolution(target_resolution_input, divisor)

    if actual_target_resolution in _generated_bucket_cache:
        return _generated_bucket_cache[actual_target_resolution]

    # Optimization: If adjusted target resolution matches base resolution.
    # This assumes base_options are already compliant with the divisor.
    # (Our _BASE_BUCKET_OPTIONS are multiples of 32, so this is fine for divisor=32).
    if actual_target_resolution == base_resolution:
        options_to_return = list(base_options) # Return a copy
        _generated_bucket_cache[actual_target_resolution] = options_to_return
        return options_to_return

    scaled_options = []
    seen_options = set() # To handle potential duplicates after rounding

    # Prevent division by zero if base_resolution is 0 (though _BASE_RESOLUTION is 640).
    if base_resolution == 0:
        # Fallback: return a single square bucket of the target resolution.
        # This case should not be hit with current constants.
        default_bucket = (actual_target_resolution, actual_target_resolution)
        _generated_bucket_cache[actual_target_resolution] = [default_bucket]
        return [default_bucket]
        
    scale_factor = float(actual_target_resolution) / base_resolution

    for base_h, base_w in base_options:
        scaled_h_float = base_h * scale_factor
        scaled_w_float = base_w * scale_factor

        scaled_h = _round_to_multiple(scaled_h_float, divisor)
        scaled_w = _round_to_multiple(scaled_w_float, divisor)

        # Ensure minimum dimension is at least the divisor
        scaled_h = max(scaled_h, divisor)
        scaled_w = max(scaled_w, divisor)
        
        bucket_tuple = (scaled_h, scaled_w)
        if bucket_tuple not in seen_options:
            scaled_options.append(bucket_tuple)
            seen_options.add(bucket_tuple)
    
    # If base_options was empty (not the case for internal use but could be if called externally),
    # scaled_options would be empty. Provide a default bucket in such a scenario.
    # actual_target_resolution is guaranteed to be >= divisor by _adjust_resolution.
    if not scaled_options: 
        default_bucket = (actual_target_resolution, actual_target_resolution)
        scaled_options.append(default_bucket)

    _generated_bucket_cache[actual_target_resolution] = scaled_options
    return scaled_options

def find_nearest_bucket(h, w, resolution=640):
    """
    Finds the nearest bucket for a given height (h) and width (w)
    at a specified target resolution.

    The 'resolution' parameter is the user's intended target resolution.
    This function will:
    1. Adjust this resolution to the nearest multiple of 32 (minimum 32).
    2. Generate a list of bucket options (height, width pairs) by scaling
       predefined base options (for 640px) to this adjusted resolution.
       All generated bucket dimensions will also be multiples of 32 and at least 32.
    3. Find the bucket from this generated list that is "nearest" to the
       aspect ratio of the input h, w. The nearness metric is
       abs(input_h * bucket_w - input_w * bucket_h).

    Args:
        h (int): The height of the image/item.
        w (int): The width of the image/item.
        resolution (int): The target resolution for which to find buckets.
                          Defaults to 640.

    Returns:
        tuple: A (bucket_h, bucket_w) tuple representing the best bucket found.
    """
    # generate_scaled_buckets handles the adjustment of 'resolution' internally
    # and uses a divisor of 32 by default for its calculations.
    # The problem statement implies a fixed divisor of 32 for this tool.
    current_bucket_options = generate_scaled_buckets(resolution, divisor=32)

    # Failsafe: If generate_scaled_buckets somehow returned an empty list (e.g., if _BASE_BUCKET_OPTIONS was empty),
    # provide a default bucket based on the adjusted resolution.
    if not current_bucket_options:
        adjusted_res_for_fallback = _adjust_resolution(resolution, 32)
        return (adjusted_res_for_fallback, adjusted_res_for_fallback)

    min_metric = float('inf')
    best_bucket = None 
    # Since current_bucket_options is guaranteed to be non-empty by the check above (or by generate_scaled_buckets's own logic
    # when _BASE_BUCKET_OPTIONS is populated), best_bucket will be assigned in the loop.

    for (bucket_h, bucket_w) in current_bucket_options:
        metric = abs(h * bucket_w - w * bucket_h)
        if metric <= min_metric: # Using "<=" preserves original behavior (last encountered wins on ties)
            min_metric = metric
            best_bucket = (bucket_h, bucket_w)
    
    return best_bucket