Skip to content

Conversation

@codeflash-ai
Copy link

@codeflash-ai codeflash-ai bot commented Oct 28, 2025

📄 240% (2.40x) speedup for image_entropy in modules/textual_inversion/autocrop.py

⏱️ Runtime : 21.9 milliseconds 6.42 milliseconds (best of 21 runs)

📝 Explanation and details

The optimization achieves a 240% speedup by replacing the expensive np.histogram operation with direct counting for binary images.

Key optimization: Since im.convert("1") produces a binary image with only two possible values (0 and 255), the original code's np.histogram(band, bins=range(0, 256)) creates 256 bins when only 2 are needed. The optimized version:

  1. Direct counting: Uses np.count_nonzero(band == 0) to count zeros, then calculates the count of 255s by subtraction (band.size - count0)
  2. Eliminates histogram overhead: Avoids creating 254 empty bins and the associated memory allocation/processing
  3. Single array pass: The subtraction trick avoids a second pass through the array to count 255s

Performance impact: Line profiler shows the histogram operation dropped from 68.4% of runtime (17.4ms) to just the counting operations taking 9.2% + 0.3% (0.85ms total) - a ~20x improvement on the bottleneck operation.

Test case performance: The optimization consistently delivers 100-150% speedups across all test cases, with particularly strong gains on large uniform images (360-374% faster) where the binary nature is most pronounced. Both small edge cases (single pixels) and large-scale images (1000x1000) benefit equally, showing the optimization scales well with image size.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 🔘 None Found
🌀 Generated Regression Tests 39 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 🔘 None Found
📊 Tests Coverage 100.0%
🌀 Generated Regression Tests and Runtime
import io
import math

# function to test
import numpy as np
# imports
import pytest
from modules.textual_inversion.autocrop import image_entropy
from PIL import Image

# --------------------
# UNIT TESTS
# --------------------

# --- BASIC TEST CASES ---

def test_entropy_all_black():
    # All black image should have zero entropy
    im = Image.new("L", (10, 10), color=0)
    codeflash_output = image_entropy(im); entropy = codeflash_output # 122μs -> 57.4μs (113% faster)

def test_entropy_all_white():
    # All white image should have zero entropy
    im = Image.new("L", (10, 10), color=255)
    codeflash_output = image_entropy(im); entropy = codeflash_output # 106μs -> 43.6μs (145% faster)

def test_entropy_half_black_half_white():
    # Half black, half white image should have entropy 1.0
    im = Image.new("L", (10, 10))
    for x in range(10):
        for y in range(10):
            if x < 5:
                im.putpixel((x, y), 0)
            else:
                im.putpixel((x, y), 255)
    codeflash_output = image_entropy(im); entropy = codeflash_output # 101μs -> 41.5μs (144% faster)

def test_entropy_checkerboard():
    # Checkerboard pattern, equal number of black and white pixels
    im = Image.new("L", (4, 4))
    for x in range(4):
        for y in range(4):
            if (x + y) % 2 == 0:
                im.putpixel((x, y), 0)
            else:
                im.putpixel((x, y), 255)
    codeflash_output = image_entropy(im); entropy = codeflash_output # 99.0μs -> 40.8μs (142% faster)

def test_entropy_gradient():
    # Gradient image: all pixels are either black or white after conversion to "1"
    im = Image.new("L", (2, 2))
    im.putpixel((0, 0), 0)
    im.putpixel((0, 1), 64)
    im.putpixel((1, 0), 192)
    im.putpixel((1, 1), 255)
    # After conversion to "1", all but the last pixel will be black (threshold=128)
    # So, 3 black, 1 white: entropy = -[3/4*log2(3/4) + 1/4*log2(1/4)]
    p0 = 3/4
    p1 = 1/4
    expected = - (p0 * math.log2(p0) + p1 * math.log2(p1))
    codeflash_output = image_entropy(im); entropy = codeflash_output # 99.6μs -> 40.3μs (147% faster)

# --- EDGE TEST CASES ---

def test_entropy_single_pixel_black():
    # Single black pixel image
    im = Image.new("L", (1, 1), color=0)
    codeflash_output = image_entropy(im); entropy = codeflash_output # 101μs -> 40.9μs (147% faster)

def test_entropy_single_pixel_white():
    # Single white pixel image
    im = Image.new("L", (1, 1), color=255)
    codeflash_output = image_entropy(im); entropy = codeflash_output # 98.1μs -> 40.2μs (144% faster)


def test_entropy_non_square_image():
    # Non-square image with half black, half white
    im = Image.new("L", (2, 4))
    for x in range(2):
        for y in range(4):
            if y < 2:
                im.putpixel((x, y), 0)
            else:
                im.putpixel((x, y), 255)
    codeflash_output = image_entropy(im); entropy = codeflash_output # 126μs -> 61.2μs (106% faster)

def test_entropy_almost_all_black_one_white():
    # 99 black, 1 white in 10x10 image
    im = Image.new("L", (10, 10), color=0)
    im.putpixel((0, 0), 255)
    p0 = 99/100
    p1 = 1/100
    expected = - (p0 * math.log2(p0) + p1 * math.log2(p1))
    codeflash_output = image_entropy(im); entropy = codeflash_output # 102μs -> 43.8μs (134% faster)

def test_entropy_almost_all_white_one_black():
    # 99 white, 1 black in 10x10 image
    im = Image.new("L", (10, 10), color=255)
    im.putpixel((0, 0), 0)
    p0 = 1/100
    p1 = 99/100
    expected = - (p0 * math.log2(p0) + p1 * math.log2(p1))
    codeflash_output = image_entropy(im); entropy = codeflash_output # 98.4μs -> 41.2μs (139% faster)

def test_entropy_rgb_image():
    # Test with an RGB image (should convert to "1" mode correctly)
    im = Image.new("RGB", (2, 2), color=(0, 0, 0))
    im.putpixel((1, 1), (255, 255, 255))
    codeflash_output = image_entropy(im); entropy = codeflash_output # 101μs -> 40.1μs (152% faster)
    # 3 black, 1 white
    p0 = 3/4
    p1 = 1/4
    expected = - (p0 * math.log2(p0) + p1 * math.log2(p1))

def test_entropy_palette_image():
    # Test with a palette ("P") image
    im = Image.new("P", (2, 2))
    im.putpalette([0, 0, 0, 255, 255, 255] + [0]*750)
    im.putpixel((0, 0), 0)
    im.putpixel((0, 1), 0)
    im.putpixel((1, 0), 1)
    im.putpixel((1, 1), 1)
    codeflash_output = image_entropy(im); entropy = codeflash_output # 99.7μs -> 38.6μs (159% faster)
    # 2 black, 2 white
    p0 = 2/4
    p1 = 2/4
    expected = - (p0 * math.log2(p0) + p1 * math.log2(p1))

def test_entropy_non_image_input():
    # Should raise an exception if input is not a PIL Image
    with pytest.raises(AttributeError):
        image_entropy("not an image") # 1.31μs -> 1.32μs (1.28% slower)

def test_entropy_alpha_channel_image():
    # RGBA image, alpha channel should be ignored
    im = Image.new("RGBA", (2, 2), color=(0, 0, 0, 0))
    im.putpixel((1, 1), (255, 255, 255, 255))
    codeflash_output = image_entropy(im); entropy = codeflash_output # 113μs -> 51.8μs (120% faster)
    # 3 black, 1 white
    p0 = 3/4
    p1 = 1/4
    expected = - (p0 * math.log2(p0) + p1 * math.log2(p1))

def test_entropy_float_image():
    # F mode image (float pixel values)
    arr = np.zeros((2, 2), dtype=np.float32)
    arr[1, 1] = 255.0
    im = Image.fromarray(arr, mode="F")
    codeflash_output = image_entropy(im); entropy = codeflash_output # 100μs -> 43.9μs (128% faster)
    # 3 black, 1 white after thresholding
    p0 = 3/4
    p1 = 1/4
    expected = - (p0 * math.log2(p0) + p1 * math.log2(p1))

# --- LARGE SCALE TEST CASES ---

def test_entropy_large_all_black():
    # Large all-black image (1000x1)
    im = Image.new("L", (1000, 1), color=0)
    codeflash_output = image_entropy(im); entropy = codeflash_output # 104μs -> 43.0μs (143% faster)

def test_entropy_large_all_white():
    # Large all-white image (1000x1)
    im = Image.new("L", (1000, 1), color=255)
    codeflash_output = image_entropy(im); entropy = codeflash_output # 106μs -> 44.1μs (142% faster)

def test_entropy_large_half_and_half():
    # 1000x1 image, first 500 black, last 500 white
    im = Image.new("L", (1000, 1), color=0)
    for x in range(500, 1000):
        im.putpixel((x, 0), 255)
    codeflash_output = image_entropy(im); entropy = codeflash_output # 105μs -> 41.8μs (152% faster)

def test_entropy_large_checkerboard():
    # 32x32 checkerboard (1024 pixels), half black, half white
    im = Image.new("L", (32, 32))
    for x in range(32):
        for y in range(32):
            if (x + y) % 2 == 0:
                im.putpixel((x, y), 0)
            else:
                im.putpixel((x, y), 255)
    codeflash_output = image_entropy(im); entropy = codeflash_output # 108μs -> 45.0μs (140% faster)

def test_entropy_large_gradient():
    # 1000x1 gradient from black to white
    im = Image.new("L", (1000, 1))
    for x in range(1000):
        im.putpixel((x, 0), int(x * 255 / 999))
    # After conversion to "1", pixels <128 are black, >=128 are white
    num_black = sum(1 for x in range(1000) if int(x * 255 / 999) < 128)
    num_white = 1000 - num_black
    p0 = num_black / 1000
    p1 = num_white / 1000
    expected = 0
    if p0 > 0 and p1 > 0:
        expected = - (p0 * math.log2(p0) + p1 * math.log2(p1))
    codeflash_output = image_entropy(im); entropy = codeflash_output # 111μs -> 47.3μs (136% faster)

def test_entropy_large_random():
    # 1000x1 image with random black/white pixels
    rng = np.random.default_rng(42)
    arr = rng.integers(0, 2, size=(1000, 1), dtype=np.uint8) * 255
    im = Image.fromarray(arr, mode="L")
    codeflash_output = image_entropy(im); entropy = codeflash_output # 111μs -> 45.5μs (146% faster)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
#------------------------------------------------
import io
import math

# imports
import pytest
from modules.textual_inversion.autocrop import image_entropy
from PIL import Image

# -------------------------
# Unit Tests for image_entropy
# -------------------------

# ----------- BASIC TEST CASES -----------

def test_entropy_single_color_black():
    # All black image: entropy should be 0
    img = Image.new("L", (10, 10), color=0)
    codeflash_output = image_entropy(img); ent = codeflash_output # 99.4μs -> 41.8μs (138% faster)

def test_entropy_single_color_white():
    # All white image: entropy should be 0
    img = Image.new("L", (10, 10), color=255)
    codeflash_output = image_entropy(img); ent = codeflash_output # 101μs -> 42.9μs (137% faster)

def test_entropy_half_black_half_white():
    # 2x2 image with 2 black and 2 white pixels
    img = Image.new("L", (2, 2))
    img.putpixel((0, 0), 0)
    img.putpixel((0, 1), 0)
    img.putpixel((1, 0), 255)
    img.putpixel((1, 1), 255)
    codeflash_output = image_entropy(img); ent = codeflash_output # 98.3μs -> 38.6μs (155% faster)

def test_entropy_checkerboard_4x4():
    # 4x4 checkerboard: exactly half black, half white
    img = Image.new("L", (4, 4))
    for y in range(4):
        for x in range(4):
            img.putpixel((x, y), 0 if (x + y) % 2 == 0 else 255)
    codeflash_output = image_entropy(img); ent = codeflash_output # 95.7μs -> 40.6μs (135% faster)

def test_entropy_varied_greyscale():
    # 2x2 image: 1 black, 1 white, 2 mid-grey (but will be thresholded in '1' mode)
    img = Image.new("L", (2, 2))
    img.putpixel((0, 0), 0)      # black
    img.putpixel((0, 1), 255)    # white
    img.putpixel((1, 0), 128)    # mid-grey (should become white in '1' mode)
    img.putpixel((1, 1), 127)    # mid-grey (should become black in '1' mode)
    codeflash_output = image_entropy(img); ent = codeflash_output # 94.9μs -> 37.9μs (151% faster)

# ----------- EDGE TEST CASES -----------


def test_entropy_one_pixel_black():
    # 1x1 black pixel: entropy 0
    img = Image.new("L", (1, 1), color=0)
    codeflash_output = image_entropy(img); ent = codeflash_output # 125μs -> 62.4μs (101% faster)

def test_entropy_one_pixel_white():
    # 1x1 white pixel: entropy 0
    img = Image.new("L", (1, 1), color=255)
    codeflash_output = image_entropy(img); ent = codeflash_output # 103μs -> 43.2μs (139% faster)

def test_entropy_alternating_row():
    # 1x4 image: black, white, black, white
    img = Image.new("L", (4, 1))
    for x in range(4):
        img.putpixel((x, 0), 0 if x % 2 == 0 else 255)
    codeflash_output = image_entropy(img); ent = codeflash_output # 97.4μs -> 40.5μs (141% faster)

def test_entropy_non_square_image():
    # 3x2 image: 3 black, 3 white
    img = Image.new("L", (3, 2))
    for i in range(3):
        img.putpixel((i, 0), 0)
        img.putpixel((i, 1), 255)
    codeflash_output = image_entropy(img); ent = codeflash_output # 97.6μs -> 39.8μs (145% faster)

def test_entropy_all_midgrey():
    # All pixels at 128: should be binarized to white in '1' mode
    img = Image.new("L", (10, 10), color=128)
    codeflash_output = image_entropy(img); ent = codeflash_output # 97.8μs -> 40.1μs (144% faster)

def test_entropy_unusual_mode():
    # Image in RGB mode; should still work
    img = Image.new("RGB", (2, 2), color=(255, 255, 255))
    codeflash_output = image_entropy(img); ent = codeflash_output # 97.0μs -> 40.2μs (141% faster)

def test_entropy_palette_image():
    # Image in 'P' mode (palette)
    img = Image.new("P", (2, 2))
    img.putpalette([0,0,0, 255,255,255] + [0,0,0]*254)
    img.putpixel((0,0), 0)
    img.putpixel((0,1), 1)
    img.putpixel((1,0), 1)
    img.putpixel((1,1), 0)
    codeflash_output = image_entropy(img); ent = codeflash_output # 97.4μs -> 40.6μs (140% faster)

def test_entropy_file_like_object():
    # Image loaded from a file-like object
    img = Image.new("L", (2, 2), color=0)
    buf = io.BytesIO()
    img.save(buf, format="PNG")
    buf.seek(0)
    loaded_img = Image.open(buf)
    codeflash_output = image_entropy(loaded_img); ent = codeflash_output # 129μs -> 65.7μs (96.9% faster)

# ----------- LARGE SCALE TEST CASES -----------

def test_entropy_large_uniform_image():
    # 1000x1000 all black
    img = Image.new("L", (1000, 1000), color=0)
    codeflash_output = image_entropy(img); ent = codeflash_output # 5.51ms -> 1.20ms (360% faster)

def test_entropy_large_balanced_image():
    # 1000x1000: left half black, right half white
    img = Image.new("L", (1000, 1000))
    for x in range(1000):
        for y in range(1000):
            color = 0 if x < 500 else 255
            img.putpixel((x, y), color)
    codeflash_output = image_entropy(img); ent = codeflash_output # 7.08ms -> 2.47ms (186% faster)

def test_entropy_large_checkerboard():
    # 100x100 checkerboard
    img = Image.new("L", (100, 100))
    for y in range(100):
        for x in range(100):
            img.putpixel((x, y), 0 if (x + y) % 2 == 0 else 255)
    codeflash_output = image_entropy(img); ent = codeflash_output # 185μs -> 72.7μs (156% faster)

def test_entropy_large_random_binary():
    # 1000x1 image, random 0/255
    import random
    img = Image.new("L", (1000, 1))
    pixels = [0 if random.random() < 0.5 else 255 for _ in range(1000)]
    for x, val in enumerate(pixels):
        img.putpixel((x, 0), val)
    codeflash_output = image_entropy(img); ent = codeflash_output # 113μs -> 48.7μs (134% faster)

def test_entropy_large_one_pixel_diff():
    # 1000x1000 all black except one white pixel
    img = Image.new("L", (1000, 1000), color=0)
    img.putpixel((0, 0), 255)
    codeflash_output = image_entropy(img); ent = codeflash_output # 5.52ms -> 1.16ms (374% faster)
    # Entropy = -(999999/1000000)*log2(999999/1000000) - (1/1000000)*log2(1/1000000)
    p1 = 999999/1000000
    p2 = 1/1000000
    expected = -(p1*math.log2(p1) + p2*math.log2(p2))
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.

To edit these changes git checkout codeflash/optimize-image_entropy-mhaddgiv and push.

Codeflash

The optimization achieves a **240% speedup** by replacing the expensive `np.histogram` operation with direct counting for binary images.

**Key optimization**: Since `im.convert("1")` produces a binary image with only two possible values (0 and 255), the original code's `np.histogram(band, bins=range(0, 256))` creates 256 bins when only 2 are needed. The optimized version:

1. **Direct counting**: Uses `np.count_nonzero(band == 0)` to count zeros, then calculates the count of 255s by subtraction (`band.size - count0`)
2. **Eliminates histogram overhead**: Avoids creating 254 empty bins and the associated memory allocation/processing
3. **Single array pass**: The subtraction trick avoids a second pass through the array to count 255s

**Performance impact**: Line profiler shows the histogram operation dropped from 68.4% of runtime (17.4ms) to just the counting operations taking 9.2% + 0.3% (0.85ms total) - a ~20x improvement on the bottleneck operation.

**Test case performance**: The optimization consistently delivers 100-150% speedups across all test cases, with particularly strong gains on large uniform images (360-374% faster) where the binary nature is most pronounced. Both small edge cases (single pixels) and large-scale images (1000x1000) benefit equally, showing the optimization scales well with image size.
@codeflash-ai codeflash-ai bot requested a review from mashraf-222 October 28, 2025 09:34
@codeflash-ai codeflash-ai bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Oct 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant