In this coding exercise, you will build a batch image processing system. You are given a set of images and transformation specifications in JSON format. Your task is to apply these transformations to each image and save the results to an output directory.
This is a practical coding challenge that tests your ability to:
You are provided with four directories: [Source: darkinterview.com]
project/
├── small_images/ # Small test images for development
│ ├── image1.png
│ ├── image2.jpg
│ └── ...
├── large_images/ # Large images for performance testing
│ ├── photo1.png
│ ├── photo2.jpg
│ └── ...
├── transformations/ # JSON files defining transformations
│ ├── transform1.json
│ ├── transform2.json
│ └── ...
└── output/ # Directory to save processed images
Helper utilities are provided to:
Each JSON file in the transformations/ directory contains a list of transformations to apply sequentially. There are six types of transformations:
| Type | Description |
|---|---|
grayscale | Convert image to grayscale |
flip_horizontal | Flip image horizontally (mirror) |
flip_vertical | Flip image vertically |
| Type | Parameter | Description |
|---|---|---|
scale | factor (float) | Scale image by the given factor (e.g., 0.5 = half size, 2.0 = double size) |
blur | radius (int) | Apply Gaussian blur with the specified radius |
rotate | angle (float) | Rotate image by the specified angle in degrees |
{
"transformations": [
{ "type": "grayscale" },
{ "type": "scale", "factor": 0.5 },
{ "type": "rotate", "angle": 90 }
]
}
This configuration would: [Source: darkinterview.com]
Choose an image processing library - Research and select a Python library capable of performing all six transformation types. Common choices include:
Implement transformation functions - Create functions for each of the six transformation types
Process images with transformations: [Source: darkinterview.com]
Test with small images - Verify correctness using the small_images/ directory before moving to large images
After verifying correctness with small images, process the large_images/ directory. You must complete processing within a target time limit (provided during the interview).
Key considerations: [Source: darkinterview.com]
def process_images(
image_dir: str,
transformation_dir: str,
output_dir: str,
get_output_path: Callable[[str, str], str]
) -> None:
"""
Process all images with all transformation configurations.
Args:
image_dir: Path to directory containing source images
transformation_dir: Path to directory containing transformation JSON files
output_dir: Path to directory for saving processed images
get_output_path: Utility function that generates output path
given (image_path, transform_json_path)
"""
pass
Disclaimer: This is a sample solution for reference. During the interview, you should develop your own approach and demonstrate your problem-solving process.
For this problem, Pillow (PIL) is a good choice because:
scikit-image is also a good option, especially if you're familiar with NumPy-based image processing. [Source: darkinterview.com]
When searching documentation, look for these modules:
| Transformation | Pillow Module/Method |
|---|---|
| Grayscale | PIL.ImageOps.grayscale() |
| Flip horizontal | PIL.ImageOps.mirror() |
| Flip vertical | PIL.ImageOps.flip() |
| Scale/Resize | Image.resize(size, resample) |
| Blur | PIL.ImageFilter.GaussianBlur(radius) |
| Rotate | Image.rotate(angle, expand=True) |
from PIL import Image, ImageFilter, ImageOps
import json
import os
from pathlib import Path
def load_transformations(json_path: str) -> list:
"""Load transformation specifications from a JSON file."""
with open(json_path, 'r') as f:
data = json.load(f)
return data.get('transformations', [])
def apply_transformation(image: Image.Image, transform: dict) -> Image.Image:
"""Apply a single transformation to an image."""
transform_type = transform['type']
if transform_type == 'grayscale':
# Convert to grayscale, then back to RGB to maintain consistent color mode
# for subsequent transformations (some operations require RGB)
return ImageOps.grayscale(image).convert('RGB')
elif transform_type == 'flip_horizontal':
return ImageOps.mirror(image)
elif transform_type == 'flip_vertical':
return ImageOps.flip(image)
elif transform_type == 'scale':
factor = transform['factor']
new_size = (int(image.width * factor), int(image.height * factor))
return image.resize(new_size, Image.Resampling.LANCZOS)
elif transform_type == 'blur':
radius = transform['radius']
return image.filter(ImageFilter.GaussianBlur(radius=radius))
transform_type == :
angle = transform[]
image.rotate(angle, expand=)
:
ValueError()
() -> Image.Image:
result = image.copy()
transform transformations:
result = apply_transformation(result, transform)
result
() -> :
image_files = [f f Path(image_dir).iterdir()
f.suffix.lower() (, , )]
transform_files = (Path(transformation_dir).glob())
transform_file transform_files:
transformations = load_transformations((transform_file))
image_file image_files:
image = Image.((image_file))
result = apply_all_transformations(image, transformations)
output_path = get_output_path((image_file), (transform_file))
result.save(output_path)
image.close()
result.close()
For large images, use ProcessPoolExecutor to parallelize the work. Key insight: image processing is CPU-bound, so multiprocessing (not threading) is needed to bypass Python's GIL.
Important: On Windows, multiprocessing requires the if __name__ == '__main__': guard around the executor code to prevent infinite process spawning. [Source: darkinterview.com]
from concurrent.futures import ProcessPoolExecutor
from PIL import Image, ImageFilter, ImageOps
import json
from pathlib import Path
import os
# Note: apply_transformation must be defined at module level for pickling
def apply_transformation(image: Image.Image, transform: dict) -> Image.Image:
"""Apply a single transformation to an image."""
transform_type = transform['type']
if transform_type == 'grayscale':
return ImageOps.grayscale(image).convert('RGB')
elif transform_type == 'flip_horizontal':
return ImageOps.mirror(image)
elif transform_type == 'flip_vertical':
return ImageOps.flip(image)
elif transform_type == 'scale':
factor = transform['factor']
new_size = (int(image.width * factor), int(image.height * factor))
return image.resize(new_size, Image.Resampling.LANCZOS)
elif transform_type == 'blur':
radius = transform['radius']
return image.filter(ImageFilter.GaussianBlur(radius=radius))
elif transform_type == 'rotate':
angle = transform['angle']
return image.rotate(angle, expand=True)
else:
raise ValueError(f"Unknown transformation type: {transform_type}")
def process_single_image() -> :
image_path, transform_path, output_path = args
(transform_path, ) f:
data = json.load(f)
transformations = data.get(, [])
image = Image.(image_path)
result = image.copy()
transform transformations:
result = apply_transformation(result, transform)
os.makedirs(os.path.dirname(output_path), exist_ok=)
result.save(output_path)
image.close()
result.close()
output_path
() -> :
image_files = [f f Path(image_dir).iterdir()
f.suffix.lower() (, , )]
transform_files = (Path(transformation_dir).glob())
work_items = []
transform_file transform_files:
image_file image_files:
output_path = get_output_path((image_file), (transform_file))
work_items.append(((image_file), (transform_file), output_path))
ProcessPoolExecutor(max_workers=max_workers) executor:
results = (executor.(process_single_image, work_items))
()
The most likely follow-up question is about parallelization strategy. Be prepared to discuss the trade-offs between threading and multiprocessing in Python.
Question: Why did you choose ProcessPoolExecutor instead of ThreadPoolExecutor? When would threading be preferable?
Key Points to Discuss: [Source: darkinterview.com]
| Aspect | ProcessPoolExecutor | ThreadPoolExecutor |
|---|---|---|
| Best for | CPU-bound tasks | I/O-bound tasks |
| GIL impact | Bypasses GIL (separate interpreters) | Limited by GIL |
| Memory | Separate memory per process | Shared memory |
| Overhead | Higher (process creation) | Lower (thread creation) |
| Data sharing | Requires serialization (pickling) | Direct access |
Why ProcessPoolExecutor for this problem:
When ThreadPoolExecutor would be better:
The GIL Explained: [Source: darkinterview.com]
Thread 1: [Python code] [wait] [Python code] [wait]
Thread 2: [wait] [Python code] [wait] [Python code]
↑ Only one thread executes Python at a time
Process 1: [Python code] [Python code] [Python code]
Process 2: [Python code] [Python code] [Python code]
↑ True parallelism with separate interpreters
Nuanced Answer (shows depth of understanding):
Memory Management: How to handle images too large for memory?
Error Handling: How to make this production-ready? [Source: darkinterview.com]
Scaling Further: What if you need to process millions of images?