Skip to content

Commit b863e76

Browse files
committed
Enhance image handling and cleanup in background processing
- Added logic to retain references to original images for proper cleanup after processing in cli.py and core.py. - Implemented image closing to ensure file handles are released, particularly on Windows, preventing potential resource leaks. - Updated test cases to utilize context managers for image opening, ensuring proper closure and resource management across various test files.
1 parent e6dd30f commit b863e76

8 files changed

Lines changed: 128 additions & 105 deletions

File tree

packages/python/src/withoutbg/cli.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ def progress_callback(progress: float) -> None:
172172

173173
# Save result
174174
save_kwargs: dict[str, Any] = {}
175+
original_result = result # Keep reference to original for cleanup
175176
if format.lower() == "jpg":
176177
# Convert RGBA to RGB for JPEG
177178
if result.mode == "RGBA":
@@ -191,6 +192,12 @@ def progress_callback(progress: float) -> None:
191192
format=pil_format.get(format.lower(), format.upper()),
192193
**save_kwargs,
193194
)
195+
196+
# Close the result image to ensure file handles are released on Windows
197+
result.close()
198+
# Also close the original if we created a new background
199+
if result is not original_result:
200+
original_result.close()
194201

195202

196203
def _process_batch(
@@ -244,6 +251,7 @@ def _process_batch(
244251

245252
# Save result
246253
save_kwargs: dict[str, Any] = {}
254+
original_result = result # Keep reference to original for cleanup
247255
if format.lower() == "jpg":
248256
if result.mode == "RGBA":
249257
background = Image.new("RGB", result.size, (255, 255, 255))
@@ -267,6 +275,12 @@ def _process_batch(
267275
format=pil_format.get(format.lower(), format.upper()),
268276
**save_kwargs,
269277
)
278+
279+
# Close the result image to ensure file handles are released on Windows
280+
result.close()
281+
# Also close the original if we created a new background
282+
if result is not original_result:
283+
original_result.close()
270284

271285
except Exception as e:
272286
if verbose:

packages/python/src/withoutbg/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ def remove_background_batch(
194194

195195
if output_path:
196196
result.save(output_path)
197+
# Note: Keep result in memory for return, don't close it yet
197198

198199
results.append(result)
199200

@@ -295,6 +296,7 @@ def remove_background_batch(
295296

296297
if output_path:
297298
result.save(output_path)
299+
# Note: Keep result in memory for return, don't close it yet
298300

299301
results.append(result)
300302

packages/python/tests/conftest.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,19 @@ def expected_outputs_dir(test_data_dir):
7474
@pytest.fixture
7575
def temp_dir():
7676
"""Create a temporary directory for test outputs."""
77+
import gc
78+
import platform
79+
import shutil
80+
import time
81+
7782
with tempfile.TemporaryDirectory() as temp_dir:
7883
yield Path(temp_dir)
84+
85+
# Windows-specific cleanup: force garbage collection and wait briefly
86+
# to ensure PIL releases file handles before cleanup
87+
if platform.system() == "Windows":
88+
gc.collect()
89+
time.sleep(0.1) # Small delay to allow file handles to be released
7990

8091

8192
@pytest.fixture

packages/python/tests/integration/test_real_image_processing.py

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,9 @@ def test_ice_cream_processing(
140140
# Check against expected output with IoU metric
141141
expected_path = expected_outputs_dir / "test-ice-cream.png"
142142
if expected_path.exists():
143-
expected = Image.open(expected_path)
144-
# Use IoU for interpretable alpha channel comparison
145-
assert_alpha_iou(result, expected, min_iou=0.97)
143+
with Image.open(expected_path) as expected:
144+
# Use IoU for interpretable alpha channel comparison
145+
assert_alpha_iou(result, expected, min_iou=0.97)
146146

147147
def test_core_api_with_real_model(self, test_images_dir):
148148
"""Test core API WithoutBG class with real processing."""
@@ -154,17 +154,16 @@ def test_core_api_with_real_model(self, test_images_dir):
154154
assert_alpha_channel_valid(result)
155155

156156
# Result should preserve original dimensions
157-
original = Image.open(input_path)
158-
assert result.size == original.size
157+
with Image.open(input_path) as original:
158+
assert result.size == original.size
159159

160160
def test_different_input_formats(self, real_opensource_model, test_images_dir):
161161
"""Test processing with different input formats."""
162162
input_path = test_images_dir / "test-ice-cream.png"
163-
original_image = Image.open(input_path)
164-
165-
# Test with PIL Image
166-
result_pil = real_opensource_model.remove_background(original_image)
167-
assert_alpha_channel_valid(result_pil)
163+
with Image.open(input_path) as original_image:
164+
# Test with PIL Image
165+
result_pil = real_opensource_model.remove_background(original_image)
166+
assert_alpha_channel_valid(result_pil)
168167

169168
# Test with file path (string)
170169
result_path = real_opensource_model.remove_background(str(input_path))
@@ -261,9 +260,8 @@ def test_processing_consistency(self, real_opensource_model, test_images_dir):
261260
def test_pipeline_stages_integration(self, real_opensource_model, test_images_dir):
262261
"""Test that the 3-stage pipeline produces reasonable outputs."""
263262
input_path = test_images_dir / "test-ice-cream.png"
264-
original = Image.open(input_path)
265-
266-
result = real_opensource_model.remove_background(original)
263+
with Image.open(input_path) as original:
264+
result = real_opensource_model.remove_background(original)
267265

268266
# Validate the result has expected characteristics for ice cream image
269267
result_array = np.array(result)
@@ -291,12 +289,11 @@ def test_alpha_iou_metric(
291289
result = real_opensource_model.remove_background(input_path)
292290

293291
if expected_path.exists():
294-
expected = Image.open(expected_path)
295-
296-
# Calculate IoU (should be very high since expected was generated
297-
# with same model)
298-
iou = calculate_alpha_iou(result, expected)
299-
print(f"Alpha IoU score: {iou:.4f}")
292+
with Image.open(expected_path) as expected:
293+
# Calculate IoU (should be very high since expected was generated
294+
# with same model)
295+
iou = calculate_alpha_iou(result, expected)
296+
print(f"Alpha IoU score: {iou:.4f}")
300297

301298
# Should be perfect since we generated expected output with same model
302299
assert iou >= 0.99, f"IoU should be near-perfect: {iou:.4f}"

packages/python/tests/performance/test_batch_performance.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,13 @@ def _mock_remove_bg(input_image, **kwargs):
4040

4141
# Load image to get size
4242
if isinstance(input_image, str):
43-
img = Image.open(input_image)
43+
with Image.open(input_image) as img:
44+
size = img.size
4445
else:
45-
img = input_image
46+
size = input_image.size
4647

4748
# Return mock result
48-
return Image.new("RGBA", img.size, color=(100, 150, 200, 128))
49+
return Image.new("RGBA", size, color=(100, 150, 200, 128))
4950

5051
return _mock_remove_bg
5152

@@ -280,8 +281,9 @@ def mock_processing_with_errors(input_image, **kwargs):
280281

281282
# Simulate normal processing for valid files
282283
time.sleep(0.01)
283-
img = Image.open(input_image)
284-
return Image.new("RGBA", img.size, color=(100, 150, 200, 128))
284+
with Image.open(input_image) as img:
285+
size = img.size
286+
return Image.new("RGBA", size, color=(100, 150, 200, 128))
285287

286288
try:
287289
model = WithoutBG.opensource()

packages/python/tests/test_api.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,16 @@ def mock_alpha_path(self):
3939
@pytest.fixture
4040
def test_image(self, test_image_path):
4141
"""Load test image."""
42-
return Image.open(test_image_path)
42+
img = Image.open(test_image_path)
43+
yield img
44+
img.close()
4345

4446
@pytest.fixture
4547
def mock_alpha_image(self, mock_alpha_path):
4648
"""Load mock alpha channel image."""
47-
return Image.open(mock_alpha_path)
49+
img = Image.open(mock_alpha_path)
50+
yield img
51+
img.close()
4852

4953
def _create_mock_alpha_response(self, mock_alpha_image):
5054
"""Create mock API response using real alpha channel image."""

packages/python/tests/test_cli.py

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,12 @@ def test_output_format_options(
304304
assert output_path.exists()
305305

306306
# Verify the file was saved with correct PIL format
307-
saved_image = Image.open(output_path)
308-
expected_pil_formats = {"png": "PNG", "jpg": "JPEG", "webp": "WEBP"}
309-
expected_format = expected_pil_formats[fmt]
310-
assert (
311-
saved_image.format == expected_format
312-
), f"Expected {expected_format}, got {saved_image.format}"
307+
with Image.open(output_path) as saved_image:
308+
expected_pil_formats = {"png": "PNG", "jpg": "JPEG", "webp": "WEBP"}
309+
expected_format = expected_pil_formats[fmt]
310+
assert (
311+
saved_image.format == expected_format
312+
), f"Expected {expected_format}, got {saved_image.format}"
313313

314314
@patch("src.withoutbg.cli.WithoutBG")
315315
def test_jpeg_quality_setting(
@@ -604,8 +604,8 @@ def test_rgba_to_jpg_conversion(self, mock_withoutbg_class, temp_dir):
604604
assert output_path.exists()
605605

606606
# Verify the saved image is RGB (not RGBA)
607-
saved_image = Image.open(output_path)
608-
assert saved_image.mode == "RGB"
607+
with Image.open(output_path) as saved_image:
608+
assert saved_image.mode == "RGB"
609609

610610
def test_directory_as_input_without_batch_flag(self, temp_dir):
611611
"""Test providing directory as input without --batch flag."""
@@ -650,9 +650,9 @@ def create_test_image(path, size=(100, 100), color=(255, 0, 0), mode="RGB"):
650650
assert image_path.exists()
651651

652652
# Verify image properties
653-
image = Image.open(image_path)
654-
assert image.size == (100, 100)
655-
assert image.mode == "RGB"
653+
with Image.open(image_path) as image:
654+
assert image.size == (100, 100)
655+
assert image.mode == "RGB"
656656

657657
def test_create_corrupted_file(self, temp_dir):
658658
"""Test utility for creating corrupted image files."""
@@ -679,30 +679,29 @@ def verify_output_properties(
679679
"""Verify properties of output image file."""
680680
assert output_path.exists(), f"Output file {output_path} does not exist"
681681

682-
image = Image.open(output_path)
683-
684-
if expected_format:
685-
assert image.format.lower() == expected_format.lower()
686-
if expected_mode:
687-
assert image.mode == expected_mode
688-
if expected_size:
689-
assert image.size == expected_size
690-
691-
return image
682+
with Image.open(output_path) as image:
683+
if expected_format:
684+
assert image.format.lower() == expected_format.lower()
685+
if expected_mode:
686+
assert image.mode == expected_mode
687+
if expected_size:
688+
assert image.size == expected_size
689+
690+
return image.size # Return size instead of image object
692691

693692
# Create test output file
694693
test_image = Image.new("RGBA", (200, 150), color=(255, 0, 0, 128))
695694
output_path = temp_dir / "output.png"
696695
test_image.save(output_path)
697696

698697
# Test verification
699-
verified_image = verify_output_properties(
698+
verified_size = verify_output_properties(
700699
output_path,
701700
expected_format="PNG",
702701
expected_mode="RGBA",
703702
expected_size=(200, 150),
704703
)
705-
assert verified_image.size == (200, 150)
704+
assert verified_size == (200, 150)
706705

707706

708707
class TestCLIPerformance:

0 commit comments

Comments
 (0)