Spaces:
Sleeping
Sleeping
Peter Idoko
commited on
Commit
·
3ff8d40
1
Parent(s):
5ed2f4c
added docstrings to each function
Browse files
app.py
CHANGED
@@ -1,9 +1,9 @@
|
|
1 |
"""
|
2 |
-
|
3 |
Author: Peter Chibuikem Idoko
|
4 |
Date: 2025-02-03
|
5 |
|
6 |
-
This
|
7 |
and replacing each tile with the closest-matching image from a folder.
|
8 |
"""
|
9 |
|
@@ -47,8 +47,29 @@ def get_average_color(image: np.ndarray) -> tuple:
|
|
47 |
avg_color = cv2.mean(image)[:3] # Get mean RGB values
|
48 |
return tuple(map(int, avg_color)) # Convert to integer RGB
|
49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
50 |
|
51 |
-
|
|
|
|
|
|
|
52 |
image_data = []
|
53 |
image_paths = []
|
54 |
|
@@ -60,73 +81,39 @@ def load_database_images_kdtree(database_folder: str) -> KDTree:
|
|
60 |
if image is None:
|
61 |
continue # Skip unreadable images
|
62 |
|
63 |
-
avg_color = get_average_color(image)
|
64 |
image_data.append(avg_color)
|
65 |
image_paths.append(img_path)
|
66 |
|
67 |
if not image_data:
|
68 |
return None, []
|
69 |
|
70 |
-
kd_tree = KDTree(image_data)
|
71 |
-
return kd_tree, image_paths, image_data # Return KD-Tree and
|
72 |
-
'''
|
73 |
-
def load_database_images(database_folder: str) -> list:
|
74 |
-
"""
|
75 |
-
Loads all images from the database and calculates their average color.
|
76 |
-
|
77 |
-
Args:
|
78 |
-
database_folder (str): Path to the folder containing database images.
|
79 |
-
|
80 |
-
Returns:
|
81 |
-
list: A list of tuples (image_path, avg_color).
|
82 |
-
|
83 |
-
Edge Cases:
|
84 |
-
- Skips unreadable or missing images.
|
85 |
-
- Returns an empty list if no valid images are found.
|
86 |
-
"""
|
87 |
-
image_data = []
|
88 |
-
|
89 |
-
for filename in os.listdir(database_folder):
|
90 |
-
if filename.lower().endswith((".jpg", ".png", ".jpeg")):
|
91 |
-
img_path = os.path.join(database_folder, filename)
|
92 |
-
image = cv2.imread(img_path)
|
93 |
|
94 |
-
if image is None:
|
95 |
-
logging.warning(f"Skipping unreadable image: {img_path}")
|
96 |
-
continue
|
97 |
-
|
98 |
-
avg_color = get_average_color(image)
|
99 |
-
image_data.append((img_path, avg_color))
|
100 |
|
101 |
-
|
102 |
-
|
103 |
-
def find_closest_image(query_color, kd_tree, image_paths, image_data):
|
104 |
-
"""Finds the image with the closest average color."""
|
105 |
-
_, index = kd_tree.query(query_color) # Find nearest color in O(log N)
|
106 |
-
return image_paths[index], image_data[index]
|
107 |
-
|
108 |
-
'''
|
109 |
-
def find_closest_image(avg_color: tuple, image_database: list) -> str:
|
110 |
"""
|
111 |
-
Finds the
|
112 |
|
113 |
Args:
|
114 |
-
|
115 |
-
|
|
|
|
|
116 |
|
117 |
Returns:
|
118 |
-
|
|
|
|
|
119 |
|
120 |
Edge Cases:
|
121 |
-
-
|
|
|
|
|
122 |
"""
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
closest_match = min(image_database, key=lambda img: distance.euclidean(avg_color, img[1]))
|
127 |
-
return closest_match[0] # Return the path
|
128 |
-
'''
|
129 |
-
|
130 |
|
131 |
def reconstruct_image(input_img, tile_size, image_size, database_folder):
|
132 |
"""
|
@@ -158,13 +145,36 @@ def reconstruct_image(input_img, tile_size, image_size, database_folder):
|
|
158 |
# Create a blank canvas for the reconstructed image
|
159 |
reconstructed_image = np.zeros_like(input_img)
|
160 |
|
161 |
-
def process_tile(row, col):
|
162 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
163 |
x_start, y_start = col * tile_size, row * tile_size
|
164 |
x_end, y_end = x_start + tile_size, y_start + tile_size
|
165 |
|
166 |
-
cell = input_img[y_start:y_end, x_start:x_end]
|
167 |
-
avg_color = get_average_color(cell)
|
168 |
|
169 |
# Find the closest matching image using KD-Tree
|
170 |
closest_image_path, _ = find_closest_image(avg_color, kd_tree, image_paths, image_data)
|
@@ -173,9 +183,30 @@ def reconstruct_image(input_img, tile_size, image_size, database_folder):
|
|
173 |
closest_image = cv2.imread(closest_image_path)
|
174 |
closest_image = cv2.resize(closest_image, (tile_size, tile_size))
|
175 |
return x_start, y_start, closest_image
|
|
|
176 |
return None
|
177 |
|
178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
179 |
results = Parallel(n_jobs=-1)(
|
180 |
delayed(process_tile)(row, col) for row in range(GRID_SIZE) for col in range(GRID_SIZE)
|
181 |
)
|
@@ -189,7 +220,6 @@ def reconstruct_image(input_img, tile_size, image_size, database_folder):
|
|
189 |
# Convert to RGB for Gradio
|
190 |
return cv2.cvtColor(reconstructed_image, cv2.COLOR_BGR2RGB)
|
191 |
|
192 |
-
|
193 |
examples = [
|
194 |
["scraped_photos/image_1002.jpg"], # Preloaded image files
|
195 |
["scraped_photos/image_1003.jpg"],
|
@@ -197,10 +227,69 @@ examples = [
|
|
197 |
]
|
198 |
|
199 |
def refresh_interface():
|
200 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
201 |
return gr.update(), gr.update(), gr.update(), gr.update()
|
202 |
|
203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
204 |
with gr.Blocks() as interface:
|
205 |
gr.Markdown("Mosaic Generator")
|
206 |
gr.Markdown("Upload an image, adjust parameters, and generate a mosaic.")
|
@@ -236,4 +325,21 @@ with gr.Blocks() as interface:
|
|
236 |
label="Try with Example Images",
|
237 |
)
|
238 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
239 |
interface.launch()
|
|
|
1 |
"""
|
2 |
+
Interactive Image Mosaic Generator
|
3 |
Author: Peter Chibuikem Idoko
|
4 |
Date: 2025-02-03
|
5 |
|
6 |
+
This program creates a mosaic by segmenting an input image into grid tiles
|
7 |
and replacing each tile with the closest-matching image from a folder.
|
8 |
"""
|
9 |
|
|
|
47 |
avg_color = cv2.mean(image)[:3] # Get mean RGB values
|
48 |
return tuple(map(int, avg_color)) # Convert to integer RGB
|
49 |
|
50 |
+
def load_database_images_kdtree(database_folder: str) -> tuple[KDTree, list, list]:
|
51 |
+
"""
|
52 |
+
Loads images from the specified database folder, computes their average colors,
|
53 |
+
and builds a KD-Tree for efficient nearest-neighbor searches.
|
54 |
+
|
55 |
+
Args:
|
56 |
+
database_folder (str): Path to the folder containing the database images.
|
57 |
+
|
58 |
+
Returns:
|
59 |
+
tuple: A tuple containing:
|
60 |
+
- KDTree: A KD-Tree built from the average colors of the images.
|
61 |
+
- list: A list of image file paths corresponding to the colors in the KD-Tree.
|
62 |
+
- list: A list of average colors (RGB tuples) used in the KD-Tree.
|
63 |
+
|
64 |
+
Edge Cases:
|
65 |
+
- Skips unreadable or corrupted images.
|
66 |
+
- Returns (None, []) if no valid images are found in the database.
|
67 |
+
- Converts image colors to integer tuples to avoid floating-point precision issues.
|
68 |
|
69 |
+
Efficiency:
|
70 |
+
- Building the KD-Tree takes O(N log N), where N is the number of images.
|
71 |
+
- Querying the KD-Tree for nearest neighbors takes O(log N), significantly faster than O(N) linear search.
|
72 |
+
"""
|
73 |
image_data = []
|
74 |
image_paths = []
|
75 |
|
|
|
81 |
if image is None:
|
82 |
continue # Skip unreadable images
|
83 |
|
84 |
+
avg_color = get_average_color(image) # Returns (R, G, B)
|
85 |
image_data.append(avg_color)
|
86 |
image_paths.append(img_path)
|
87 |
|
88 |
if not image_data:
|
89 |
return None, []
|
90 |
|
91 |
+
kd_tree = KDTree(image_data) # Build KD-Tree from image colors
|
92 |
+
return kd_tree, image_paths, image_data # Return KD-Tree, paths, and colors
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
94 |
|
95 |
+
def find_closest_image(query_color: tuple[int, int, int], kd_tree: KDTree, image_paths: list[str], image_data: list[tuple[int, int, int]]) -> tuple[str, tuple[int, int, int]]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
"""
|
97 |
+
Finds the image with the closest average color using a KD-Tree.
|
98 |
|
99 |
Args:
|
100 |
+
query_color (tuple[int, int, int]): The target color in (R, G, B) format.
|
101 |
+
kd_tree (KDTree): A KD-Tree containing the average colors of database images.
|
102 |
+
image_paths (list[str]): A list of file paths corresponding to the images in the database.
|
103 |
+
image_data (list[tuple[int, int, int]]): A list of average colors (R, G, B) for the images.
|
104 |
|
105 |
Returns:
|
106 |
+
tuple: A tuple containing:
|
107 |
+
- str: The file path of the image with the closest matching color.
|
108 |
+
- tuple[int, int, int]: The average color of the matched image.
|
109 |
|
110 |
Edge Cases:
|
111 |
+
- Assumes `kd_tree` is properly built and contains valid data.
|
112 |
+
- If `query_color` is not in `image_data`, the function returns the closest match.
|
113 |
+
- Handles nearest-neighbor search efficiently in O(log N) time.
|
114 |
"""
|
115 |
+
_, index = kd_tree.query(query_color) # Find nearest color in O(log N)
|
116 |
+
return image_paths[index], image_data[index]
|
|
|
|
|
|
|
|
|
|
|
117 |
|
118 |
def reconstruct_image(input_img, tile_size, image_size, database_folder):
|
119 |
"""
|
|
|
145 |
# Create a blank canvas for the reconstructed image
|
146 |
reconstructed_image = np.zeros_like(input_img)
|
147 |
|
148 |
+
def process_tile(row: int, col: int) -> tuple[int, int, np.ndarray] | None:
|
149 |
+
"""
|
150 |
+
Processes a single tile from the input image, finds the closest matching image
|
151 |
+
using a KD-Tree, and returns the tile's position and corresponding replacement image.
|
152 |
+
|
153 |
+
Args:
|
154 |
+
row (int): The row index of the tile in the mosaic grid.
|
155 |
+
col (int): The column index of the tile in the mosaic grid.
|
156 |
+
|
157 |
+
Returns:
|
158 |
+
tuple[int, int, np.ndarray] | None: A tuple containing:
|
159 |
+
- int: The x-coordinate (column) of the tile's top-left corner.
|
160 |
+
- int: The y-coordinate (row) of the tile's top-left corner.
|
161 |
+
- np.ndarray: The selected tile image resized to the correct tile size.
|
162 |
+
Returns None if no matching image is found.
|
163 |
+
|
164 |
+
Edge Cases:
|
165 |
+
- Handles cases where the input image is not properly loaded.
|
166 |
+
- Ensures the selected image is resized to match the tile size.
|
167 |
+
- If no valid image is found, returns None to avoid errors in reconstruction.
|
168 |
+
|
169 |
+
Efficiency:
|
170 |
+
- Uses a KD-Tree for nearest-neighbor search, making color matching O(log N).
|
171 |
+
- Operates in parallel (if used in a multi-threaded/multi-process pipeline) to speed up reconstruction.
|
172 |
+
"""
|
173 |
x_start, y_start = col * tile_size, row * tile_size
|
174 |
x_end, y_end = x_start + tile_size, y_start + tile_size
|
175 |
|
176 |
+
cell = input_img[y_start:y_end, x_start:x_end] # Extract the tile region
|
177 |
+
avg_color = get_average_color(cell) # Compute its average color
|
178 |
|
179 |
# Find the closest matching image using KD-Tree
|
180 |
closest_image_path, _ = find_closest_image(avg_color, kd_tree, image_paths, image_data)
|
|
|
183 |
closest_image = cv2.imread(closest_image_path)
|
184 |
closest_image = cv2.resize(closest_image, (tile_size, tile_size))
|
185 |
return x_start, y_start, closest_image
|
186 |
+
|
187 |
return None
|
188 |
|
189 |
+
"""
|
190 |
+
Parallel processing for faster tile processing.
|
191 |
+
|
192 |
+
This step uses joblib's Parallel and delayed functions to distribute
|
193 |
+
the workload across multiple CPU cores. Each tile in the image grid is
|
194 |
+
processed in parallel, significantly improving performance.
|
195 |
+
|
196 |
+
Implementation:
|
197 |
+
- `n_jobs=-1` ensures that all available CPU cores are utilized.
|
198 |
+
- `delayed(process_tile)(row, col)` schedules each tile for processing.
|
199 |
+
- The list comprehension iterates over all rows and columns in the grid.
|
200 |
+
|
201 |
+
Efficiency:
|
202 |
+
- Reduces runtime from O(N) sequential processing to approximately O(N / C),
|
203 |
+
where C is the number of CPU cores.
|
204 |
+
- Automatically distributes workload, making it scalable for larger images.
|
205 |
+
|
206 |
+
Edge Cases:
|
207 |
+
- Ensures that all tiles are processed even if some return None.
|
208 |
+
- Handles failures in individual tile processing without stopping execution.
|
209 |
+
"""
|
210 |
results = Parallel(n_jobs=-1)(
|
211 |
delayed(process_tile)(row, col) for row in range(GRID_SIZE) for col in range(GRID_SIZE)
|
212 |
)
|
|
|
220 |
# Convert to RGB for Gradio
|
221 |
return cv2.cvtColor(reconstructed_image, cv2.COLOR_BGR2RGB)
|
222 |
|
|
|
223 |
examples = [
|
224 |
["scraped_photos/image_1002.jpg"], # Preloaded image files
|
225 |
["scraped_photos/image_1003.jpg"],
|
|
|
227 |
]
|
228 |
|
229 |
def refresh_interface():
|
230 |
+
"""
|
231 |
+
Refreshes the user interface without clearing input fields.
|
232 |
+
|
233 |
+
This function updates all UI elements to reflect any changes while preserving
|
234 |
+
the existing input values. It is useful for refreshing outputs without requiring
|
235 |
+
users to re-enter data.
|
236 |
+
|
237 |
+
Returns:
|
238 |
+
tuple: A tuple of `gr.update()` calls, one for each UI component that needs refreshing.
|
239 |
+
|
240 |
+
Edge Cases:
|
241 |
+
- Ensures UI elements are updated without resetting user input.
|
242 |
+
- Prevents unnecessary clearing of fields while allowing the interface to be refreshed.
|
243 |
+
- May require additional `gr.update()` calls if more UI components are added in the future.
|
244 |
+
"""
|
245 |
return gr.update(), gr.update(), gr.update(), gr.update()
|
246 |
|
247 |
+
"""
|
248 |
+
Gradio Interface for the Mosaic Generator.
|
249 |
+
|
250 |
+
This interface allows users to upload an image, adjust parameters, and generate
|
251 |
+
a photomosaic using images from a specified database. It provides an interactive
|
252 |
+
UI for users to experiment with different settings and generate high-quality mosaics.
|
253 |
+
|
254 |
+
### Features:
|
255 |
+
- **Image Upload:** Users can upload an image as input.
|
256 |
+
- **Tile Size Adjustment:** A slider to modify the tile size.
|
257 |
+
- **Image Size Adjustment:** A slider to control the final mosaic size.
|
258 |
+
- **Tile Database Selection:** A textbox to specify the folder containing tile images.
|
259 |
+
- **Generate Mosaic Button:** Processes the image and constructs the mosaic.
|
260 |
+
- **Refresh Button:** Updates the UI without clearing input fields.
|
261 |
+
- **Example Images Section:** Allows users to try preloaded example images.
|
262 |
+
|
263 |
+
### Components:
|
264 |
+
- **Markdown Headers:** Display instructions and descriptions.
|
265 |
+
- **Image Inputs/Outputs:**
|
266 |
+
- `input_image`: User-uploaded image.
|
267 |
+
- `output_image`: Generated photomosaic.
|
268 |
+
- **Sliders:**
|
269 |
+
- `tile_size_slider`: Controls the size of mosaic tiles.
|
270 |
+
- `image_size_slider`: Controls the overall size of the generated mosaic.
|
271 |
+
- **Textbox:**
|
272 |
+
- `database_folder_input`: Specifies the folder containing database images.
|
273 |
+
- **Buttons:**
|
274 |
+
- `generate_button`: Calls `reconstruct_image()` to generate the mosaic.
|
275 |
+
- `refresh_button`: Calls `refresh_interface()` to update the UI.
|
276 |
+
- **Example Section:** Allows users to load predefined example images.
|
277 |
+
|
278 |
+
### Button Functionality:
|
279 |
+
- **Generate Button:**
|
280 |
+
- Calls `reconstruct_image()`
|
281 |
+
- Inputs: `[input_image, tile_size_slider, image_size_slider, database_folder_input]`
|
282 |
+
- Outputs: `[output_image]`
|
283 |
+
- **Refresh Button:**
|
284 |
+
- Calls `refresh_interface()`
|
285 |
+
- Inputs: `[]`
|
286 |
+
- Outputs: `[input_image, tile_size_slider, image_size_slider, database_folder_input]`
|
287 |
+
|
288 |
+
### Edge Cases:
|
289 |
+
- Ensures users can modify tile size and image resolution dynamically.
|
290 |
+
- Prevents the UI from resetting unnecessarily when refreshing.
|
291 |
+
- Provides example images for quick testing without requiring uploads.
|
292 |
+
"""
|
293 |
with gr.Blocks() as interface:
|
294 |
gr.Markdown("Mosaic Generator")
|
295 |
gr.Markdown("Upload an image, adjust parameters, and generate a mosaic.")
|
|
|
325 |
label="Try with Example Images",
|
326 |
)
|
327 |
|
328 |
+
"""
|
329 |
+
Launches the Gradio interface.
|
330 |
+
|
331 |
+
This function starts the Gradio web-based UI, allowing users to interact with
|
332 |
+
the Mosaic Generator. Once launched, users can upload images, adjust parameters,
|
333 |
+
and generate mosaics through a browser.
|
334 |
+
|
335 |
+
### Behavior:
|
336 |
+
- Initializes and serves the Gradio interface on a local or public URL.
|
337 |
+
- Provides an interactive UI for processing and visualizing image mosaics.
|
338 |
+
- Runs a web server that remains active until manually stopped.
|
339 |
+
|
340 |
+
### Edge Cases:
|
341 |
+
- If Gradio is running in a notebook, it may default to inline display.
|
342 |
+
- If launched in a standalone script, it opens a browser window.
|
343 |
+
- Requires all components (buttons, sliders, images) to be correctly defined before launching.
|
344 |
+
"""
|
345 |
interface.launch()
|