Spaces:
Sleeping
Sleeping
[email protected]
commited on
Commit
·
0404f22
1
Parent(s):
1042da1
Add preprocess files
Browse files- app.py +70 -133
- kdtree_dataset.pkl +3 -0
- preprocess.py +104 -0
app.py
CHANGED
@@ -2,165 +2,127 @@ import gradio as gr
|
|
2 |
import cv2
|
3 |
import numpy as np
|
4 |
import os
|
|
|
|
|
5 |
from skimage.metrics import structural_similarity as ssim
|
6 |
|
7 |
-
#
|
8 |
-
DATASET_FOLDER = "Dataset"
|
|
|
|
|
9 |
|
|
|
10 |
def compute_features(image):
|
11 |
"""
|
12 |
Compute a set of features for an image:
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
Returns: (avg_lab, avg_edge, avg_texture, avg_grad)
|
18 |
"""
|
19 |
-
# Apply Gaussian blur to reduce noise before computing Lab color
|
20 |
blurred = cv2.GaussianBlur(image, (5, 5), 0)
|
21 |
img_lab = cv2.cvtColor(blurred, cv2.COLOR_RGB2LAB)
|
22 |
avg_lab = np.mean(img_lab, axis=(0, 1))
|
23 |
-
|
24 |
-
# Convert to grayscale for edge and texture computations
|
25 |
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
|
26 |
-
|
27 |
-
# Edge density: apply Canny and normalize the average edge intensity
|
28 |
edges = cv2.Canny(gray, 100, 200)
|
29 |
-
avg_edge = np.mean(edges) / 255.0
|
30 |
-
|
31 |
-
# Texture measure: standard deviation of grayscale values (normalized)
|
32 |
avg_texture = np.std(gray) / 255.0
|
33 |
-
|
34 |
-
# Gradient magnitude: using Sobel operators in x and y directions, then average
|
35 |
grad_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
|
36 |
grad_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
|
37 |
grad_mag = np.sqrt(grad_x**2 + grad_y**2)
|
38 |
avg_grad = np.mean(grad_mag) / 255.0
|
39 |
-
|
40 |
return avg_lab, avg_edge, avg_texture, avg_grad
|
41 |
|
42 |
-
def
|
43 |
"""
|
44 |
-
|
45 |
-
|
|
|
|
|
46 |
"""
|
47 |
-
|
48 |
-
|
49 |
-
|
|
|
|
|
50 |
|
51 |
-
|
52 |
-
|
53 |
-
if img is None:
|
54 |
-
continue # Skip unreadable images
|
55 |
-
# Resize the image to the given tile size
|
56 |
-
img = cv2.resize(img, tile_size)
|
57 |
-
# Convert from BGR to RGB
|
58 |
-
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
59 |
-
|
60 |
-
# Compute the feature vector for this dataset image
|
61 |
-
avg_lab, avg_edge, avg_texture, avg_grad = compute_features(img)
|
62 |
-
dataset.append((img, avg_lab, avg_edge, avg_texture, avg_grad, img_path))
|
63 |
-
|
64 |
-
return dataset
|
65 |
-
|
66 |
-
def find_best_match(tile_features, dataset, weights=(1.0, 0.5, 0.5, 0.5)):
|
67 |
"""
|
68 |
-
|
69 |
-
- Color difference (in Lab space)
|
70 |
-
- Edge density difference
|
71 |
-
- Texture difference
|
72 |
-
- Gradient magnitude difference
|
73 |
-
|
74 |
-
The weights parameter is a tuple with weights for each feature in the same order.
|
75 |
"""
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
# Compute the difference for each feature:
|
84 |
-
color_diff = np.linalg.norm(tile_lab - ds_lab)
|
85 |
-
edge_diff = abs(tile_edge - ds_edge)
|
86 |
-
texture_diff = abs(tile_texture - ds_texture)
|
87 |
-
grad_diff = abs(tile_grad - ds_grad)
|
88 |
-
|
89 |
-
# Compute a weighted distance (using a Euclidean combination)
|
90 |
-
dist = np.sqrt(weights[0] * (color_diff ** 2) +
|
91 |
-
weights[1] * (edge_diff ** 2) +
|
92 |
-
weights[2] * (texture_diff ** 2) +
|
93 |
-
weights[3] * (grad_diff ** 2))
|
94 |
-
|
95 |
-
if dist < min_dist:
|
96 |
-
min_dist = dist
|
97 |
-
best_match = ds_img
|
98 |
-
|
99 |
-
return best_match
|
100 |
|
101 |
-
|
|
|
102 |
"""
|
103 |
-
|
104 |
-
|
105 |
-
|
|
|
106 |
"""
|
107 |
-
#
|
|
|
|
|
|
|
|
|
|
|
108 |
original_image = input_image.copy()
|
109 |
height, width, _ = original_image.shape
|
110 |
|
111 |
-
#
|
112 |
tile_height = height // num_tiles_y
|
113 |
aspect_ratio = width / height
|
114 |
num_tiles_x = int(num_tiles_y * aspect_ratio)
|
115 |
tile_width = width // num_tiles_x
|
116 |
print(f"Adjusted number of tiles: {num_tiles_x} (width) x {num_tiles_y} (height)")
|
117 |
|
118 |
-
# Load the dataset images with the new feature set
|
119 |
-
dataset = load_dataset_images(dataset_folder, (tile_width, tile_height))
|
120 |
-
if not dataset:
|
121 |
-
print("No images found in dataset folder!")
|
122 |
-
return None
|
123 |
-
|
124 |
-
# Create an empty mosaic image in RGB
|
125 |
mosaic = np.zeros_like(original_image)
|
126 |
-
|
127 |
-
# Calculate the grid ranges and total tile count for progress tracking
|
128 |
rows = list(range(0, height, tile_height))
|
129 |
cols = list(range(0, width, tile_width))
|
130 |
total_tiles = len(rows) * len(cols)
|
131 |
tile_count = 0
|
132 |
|
133 |
-
# Process each tile of the input image
|
134 |
for y in rows:
|
135 |
for x in cols:
|
136 |
y_end = min(y + tile_height, height)
|
137 |
x_end = min(x + tile_width, width)
|
138 |
tile = original_image[y:y_end, x:x_end]
|
139 |
|
140 |
-
#
|
141 |
-
|
|
|
142 |
|
143 |
-
#
|
144 |
-
|
|
|
|
|
145 |
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
|
150 |
tile_count += 1
|
151 |
if progress is not None:
|
152 |
progress(tile_count / total_tiles)
|
153 |
|
154 |
-
# Save the final mosaic
|
155 |
output_path = "mosaic_output.jpg"
|
156 |
cv2.imwrite(output_path, cv2.cvtColor(mosaic, cv2.COLOR_RGB2BGR))
|
157 |
-
|
158 |
return output_path
|
159 |
|
160 |
def create_color_mosaic(input_image, num_tiles_y, progress=None):
|
161 |
"""
|
162 |
-
|
163 |
-
|
164 |
"""
|
165 |
original_image = input_image.copy()
|
166 |
height, width, _ = original_image.shape
|
@@ -191,8 +153,7 @@ def create_color_mosaic(input_image, num_tiles_y, progress=None):
|
|
191 |
cv2.imwrite(output_path, cv2.cvtColor(mosaic, cv2.COLOR_RGB2BGR))
|
192 |
return output_path
|
193 |
|
194 |
-
# ----------------- Performance Metrics
|
195 |
-
|
196 |
def compute_mse(original, mosaic):
|
197 |
"""
|
198 |
Compute Mean Squared Error (MSE) between two images.
|
@@ -203,66 +164,42 @@ def compute_mse(original, mosaic):
|
|
203 |
mse = err / float(original.shape[0] * original.shape[1] * original.shape[2])
|
204 |
return mse
|
205 |
|
206 |
-
def
|
207 |
"""
|
208 |
Compute Structural Similarity Index (SSIM) between two images.
|
209 |
"""
|
210 |
min_dim = min(original.shape[0], original.shape[1])
|
211 |
-
if min_dim >= 7
|
212 |
-
win_size = 7
|
213 |
-
else:
|
214 |
-
# Ensure the window size is odd.
|
215 |
-
win_size = min_dim if min_dim % 2 == 1 else min_dim - 1
|
216 |
ssim_value, _ = ssim(original, mosaic, win_size=win_size, channel_axis=-1, full=True)
|
217 |
return ssim_value
|
218 |
|
219 |
-
|
220 |
-
"""
|
221 |
-
Ensure that the image has a minimum size; if not, resize it.
|
222 |
-
"""
|
223 |
-
h, w = image.shape[:2]
|
224 |
-
if h < min_size or w < min_size:
|
225 |
-
new_w = max(min_size, w)
|
226 |
-
new_h = max(min_size, h)
|
227 |
-
image = cv2.resize(image, (new_w, new_h))
|
228 |
-
return image
|
229 |
-
|
230 |
-
# ----------------- Gradio Interface Function -----------------
|
231 |
-
|
232 |
def mosaic_gradio(input_image, num_tiles_y, mosaic_type, progress=gr.Progress()):
|
233 |
"""
|
234 |
Gradio interface function to generate and return the mosaic image along with performance metrics.
|
235 |
-
mosaic_type:
|
236 |
Returns: (mosaic_image_file, performance_metrics_string)
|
237 |
"""
|
238 |
-
# Generate mosaic based on selected type
|
239 |
if mosaic_type == "Color Mosaic":
|
240 |
mosaic_path = create_color_mosaic(input_image, num_tiles_y, progress)
|
241 |
else:
|
242 |
-
mosaic_path = create_photo_mosaic(input_image,
|
243 |
-
|
244 |
-
# Load the mosaic image from file (convert from BGR to RGB)
|
245 |
mosaic_image = cv2.imread(mosaic_path)
|
246 |
if mosaic_image is None:
|
247 |
return None, "Error: Mosaic image could not be loaded."
|
248 |
mosaic_image = cv2.cvtColor(mosaic_image, cv2.COLOR_BGR2RGB)
|
249 |
-
|
250 |
-
# Ensure both images meet minimum size requirements for metric calculations
|
251 |
input_for_metrics = ensure_min_size(input_image.copy())
|
252 |
mosaic_for_metrics = ensure_min_size(mosaic_image.copy())
|
253 |
-
|
254 |
-
# Compute performance metrics
|
255 |
mse_value = compute_mse(input_for_metrics, mosaic_for_metrics)
|
256 |
-
ssim_value =
|
257 |
-
|
258 |
metrics_text = f"MSE: {mse_value:.2f}\nSSIM: {ssim_value:.4f}"
|
259 |
-
|
260 |
return mosaic_path, metrics_text
|
261 |
|
262 |
# ----------------- Gradio App Setup -----------------
|
263 |
-
|
264 |
-
# Adding examples so that test images appear as clickable examples.
|
265 |
-
# Adjust the paths as needed.
|
266 |
examples = [
|
267 |
["input_images/1.jpg", 90, "Image Mosaic"],
|
268 |
["input_images/2.jpg", 90, "Image Mosaic"],
|
|
|
2 |
import cv2
|
3 |
import numpy as np
|
4 |
import os
|
5 |
+
import pickle
|
6 |
+
import math
|
7 |
from skimage.metrics import structural_similarity as ssim
|
8 |
|
9 |
+
# ----------------- Constants -----------------
|
10 |
+
DATASET_FOLDER = "Dataset" # (Not used directly now)
|
11 |
+
KD_TREE_PATH = "kdtree_dataset.pkl" # Path to the precomputed KDTree file
|
12 |
+
KD_TILE_SIZE = (50, 50) # Must match the tile size used when building the KDTree
|
13 |
|
14 |
+
# ----------------- Feature Extraction Functions -----------------
|
15 |
def compute_features(image):
|
16 |
"""
|
17 |
Compute a set of features for an image:
|
18 |
+
- Average Lab color (using a Gaussian-blurred version)
|
19 |
+
- Edge density using Canny edge detection (normalized)
|
20 |
+
- Texture measure using the standard deviation of the grayscale image (normalized)
|
21 |
+
- Average gradient magnitude computed via Sobel operators (normalized)
|
22 |
Returns: (avg_lab, avg_edge, avg_texture, avg_grad)
|
23 |
"""
|
|
|
24 |
blurred = cv2.GaussianBlur(image, (5, 5), 0)
|
25 |
img_lab = cv2.cvtColor(blurred, cv2.COLOR_RGB2LAB)
|
26 |
avg_lab = np.mean(img_lab, axis=(0, 1))
|
27 |
+
|
|
|
28 |
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
|
|
|
|
|
29 |
edges = cv2.Canny(gray, 100, 200)
|
30 |
+
avg_edge = np.mean(edges) / 255.0
|
|
|
|
|
31 |
avg_texture = np.std(gray) / 255.0
|
|
|
|
|
32 |
grad_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
|
33 |
grad_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
|
34 |
grad_mag = np.sqrt(grad_x**2 + grad_y**2)
|
35 |
avg_grad = np.mean(grad_mag) / 255.0
|
36 |
+
|
37 |
return avg_lab, avg_edge, avg_texture, avg_grad
|
38 |
|
39 |
+
def compute_weighted_features(image):
|
40 |
"""
|
41 |
+
Compute the weighted feature vector for KDTree search.
|
42 |
+
The image should be resized to KD_TILE_SIZE before feature extraction.
|
43 |
+
Weights: for Lab channels use 1.0; for edge, texture, and gradient use 0.5
|
44 |
+
(implemented as multiplying by sqrt(0.5)).
|
45 |
"""
|
46 |
+
scale = np.array([1.0, 1.0, 1.0, math.sqrt(0.5), math.sqrt(0.5), math.sqrt(0.5)])
|
47 |
+
avg_lab, avg_edge, avg_texture, avg_grad = compute_features(image)
|
48 |
+
raw_feature = np.concatenate([avg_lab, [avg_edge, avg_texture, avg_grad]])
|
49 |
+
weighted_feature = raw_feature * scale
|
50 |
+
return weighted_feature
|
51 |
|
52 |
+
# ----------------- Utility Function -----------------
|
53 |
+
def ensure_min_size(image, min_size=7):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
"""
|
55 |
+
Ensure that the image has at least a minimum size; if not, resize it.
|
|
|
|
|
|
|
|
|
|
|
|
|
56 |
"""
|
57 |
+
h, w = image.shape[:2]
|
58 |
+
if h < min_size or w < min_size:
|
59 |
+
new_w = max(min_size, w)
|
60 |
+
new_h = max(min_size, h)
|
61 |
+
image = cv2.resize(image, (new_w, new_h))
|
62 |
+
return image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
63 |
|
64 |
+
# ----------------- Mosaic Generation Functions -----------------
|
65 |
+
def create_photo_mosaic(input_image, kdtree_path, num_tiles_y, progress=None):
|
66 |
"""
|
67 |
+
Create an image mosaic using a precomputed KDTree.
|
68 |
+
For each mosaic tile in the input image, the tile is resized to KD_TILE_SIZE,
|
69 |
+
its weighted features are computed, and the KDTree is queried to find the best match.
|
70 |
+
The matched dataset image is then resized to the tile’s actual size before placement.
|
71 |
"""
|
72 |
+
# Load the precomputed KDTree and dataset images
|
73 |
+
with open(kdtree_path, "rb") as f:
|
74 |
+
tree_data = pickle.load(f)
|
75 |
+
tree = tree_data['tree']
|
76 |
+
dataset_images = tree_data['images']
|
77 |
+
|
78 |
original_image = input_image.copy()
|
79 |
height, width, _ = original_image.shape
|
80 |
|
81 |
+
# Determine mosaic grid dimensions
|
82 |
tile_height = height // num_tiles_y
|
83 |
aspect_ratio = width / height
|
84 |
num_tiles_x = int(num_tiles_y * aspect_ratio)
|
85 |
tile_width = width // num_tiles_x
|
86 |
print(f"Adjusted number of tiles: {num_tiles_x} (width) x {num_tiles_y} (height)")
|
87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
88 |
mosaic = np.zeros_like(original_image)
|
|
|
|
|
89 |
rows = list(range(0, height, tile_height))
|
90 |
cols = list(range(0, width, tile_width))
|
91 |
total_tiles = len(rows) * len(cols)
|
92 |
tile_count = 0
|
93 |
|
|
|
94 |
for y in rows:
|
95 |
for x in cols:
|
96 |
y_end = min(y + tile_height, height)
|
97 |
x_end = min(x + tile_width, width)
|
98 |
tile = original_image[y:y_end, x:x_end]
|
99 |
|
100 |
+
# Resize the tile to the KDTree tile size for feature extraction
|
101 |
+
tile_resized = cv2.resize(tile, KD_TILE_SIZE)
|
102 |
+
query_feature = compute_weighted_features(tile_resized)
|
103 |
|
104 |
+
# Query the KDTree for the best match (returns index)
|
105 |
+
dist, ind = tree.query([query_feature], k=1)
|
106 |
+
best_index = ind[0][0]
|
107 |
+
best_match = dataset_images[best_index]
|
108 |
|
109 |
+
# Resize the best match image to the current tile size and place it into the mosaic
|
110 |
+
best_match_resized = cv2.resize(best_match, (x_end - x, y_end - y))
|
111 |
+
mosaic[y:y_end, x:x_end] = best_match_resized
|
112 |
|
113 |
tile_count += 1
|
114 |
if progress is not None:
|
115 |
progress(tile_count / total_tiles)
|
116 |
|
117 |
+
# Save the final mosaic (convert from RGB to BGR for saving with cv2)
|
118 |
output_path = "mosaic_output.jpg"
|
119 |
cv2.imwrite(output_path, cv2.cvtColor(mosaic, cv2.COLOR_RGB2BGR))
|
|
|
120 |
return output_path
|
121 |
|
122 |
def create_color_mosaic(input_image, num_tiles_y, progress=None):
|
123 |
"""
|
124 |
+
Create a simple color mosaic by dividing the image into grid cells and filling
|
125 |
+
each cell with its average RGB color.
|
126 |
"""
|
127 |
original_image = input_image.copy()
|
128 |
height, width, _ = original_image.shape
|
|
|
153 |
cv2.imwrite(output_path, cv2.cvtColor(mosaic, cv2.COLOR_RGB2BGR))
|
154 |
return output_path
|
155 |
|
156 |
+
# ----------------- Performance Metrics -----------------
|
|
|
157 |
def compute_mse(original, mosaic):
|
158 |
"""
|
159 |
Compute Mean Squared Error (MSE) between two images.
|
|
|
164 |
mse = err / float(original.shape[0] * original.shape[1] * original.shape[2])
|
165 |
return mse
|
166 |
|
167 |
+
def compute_ssim_metric(original, mosaic):
|
168 |
"""
|
169 |
Compute Structural Similarity Index (SSIM) between two images.
|
170 |
"""
|
171 |
min_dim = min(original.shape[0], original.shape[1])
|
172 |
+
win_size = 7 if min_dim >= 7 else (min_dim if min_dim % 2 == 1 else min_dim - 1)
|
|
|
|
|
|
|
|
|
173 |
ssim_value, _ = ssim(original, mosaic, win_size=win_size, channel_axis=-1, full=True)
|
174 |
return ssim_value
|
175 |
|
176 |
+
# ----------------- Gradio Interface -----------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
177 |
def mosaic_gradio(input_image, num_tiles_y, mosaic_type, progress=gr.Progress()):
|
178 |
"""
|
179 |
Gradio interface function to generate and return the mosaic image along with performance metrics.
|
180 |
+
mosaic_type: "Color Mosaic" or "Image Mosaic"
|
181 |
Returns: (mosaic_image_file, performance_metrics_string)
|
182 |
"""
|
|
|
183 |
if mosaic_type == "Color Mosaic":
|
184 |
mosaic_path = create_color_mosaic(input_image, num_tiles_y, progress)
|
185 |
else:
|
186 |
+
mosaic_path = create_photo_mosaic(input_image, KD_TREE_PATH, num_tiles_y, progress)
|
187 |
+
|
|
|
188 |
mosaic_image = cv2.imread(mosaic_path)
|
189 |
if mosaic_image is None:
|
190 |
return None, "Error: Mosaic image could not be loaded."
|
191 |
mosaic_image = cv2.cvtColor(mosaic_image, cv2.COLOR_BGR2RGB)
|
192 |
+
|
|
|
193 |
input_for_metrics = ensure_min_size(input_image.copy())
|
194 |
mosaic_for_metrics = ensure_min_size(mosaic_image.copy())
|
195 |
+
|
|
|
196 |
mse_value = compute_mse(input_for_metrics, mosaic_for_metrics)
|
197 |
+
ssim_value = compute_ssim_metric(input_for_metrics, mosaic_for_metrics)
|
198 |
+
|
199 |
metrics_text = f"MSE: {mse_value:.2f}\nSSIM: {ssim_value:.4f}"
|
|
|
200 |
return mosaic_path, metrics_text
|
201 |
|
202 |
# ----------------- Gradio App Setup -----------------
|
|
|
|
|
|
|
203 |
examples = [
|
204 |
["input_images/1.jpg", 90, "Image Mosaic"],
|
205 |
["input_images/2.jpg", 90, "Image Mosaic"],
|
kdtree_dataset.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:82d2da18cad1927be1094563f598f33668595a1c62ca336fd86eadcfeda75c59
|
3 |
+
size 44070974
|
preprocess.py
ADDED
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# build_kdtree.py
|
2 |
+
|
3 |
+
import os
|
4 |
+
import cv2
|
5 |
+
import numpy as np
|
6 |
+
import pickle
|
7 |
+
import math
|
8 |
+
from sklearn.neighbors import KDTree
|
9 |
+
|
10 |
+
# ----------------- Constants -----------------
|
11 |
+
DATASET_FOLDER = "Dataset" # Folder containing your dataset images
|
12 |
+
KD_TILE_SIZE = (50, 50) # Fixed size to which each dataset image will be resized
|
13 |
+
KD_TREE_PATH = "kdtree_dataset.pkl" # Output pickle file
|
14 |
+
|
15 |
+
# ----------------- Feature Extraction -----------------
|
16 |
+
def compute_features(image):
|
17 |
+
"""
|
18 |
+
Compute a set of features for an image:
|
19 |
+
- Average Lab color (using a Gaussian-blurred version)
|
20 |
+
- Edge density using Canny edge detection (normalized)
|
21 |
+
- Texture measure using the standard deviation of the grayscale image (normalized)
|
22 |
+
- Average gradient magnitude computed via Sobel operators (normalized)
|
23 |
+
Returns: (avg_lab, avg_edge, avg_texture, avg_grad)
|
24 |
+
"""
|
25 |
+
# Gaussian blur to reduce noise before computing Lab color
|
26 |
+
blurred = cv2.GaussianBlur(image, (5, 5), 0)
|
27 |
+
img_lab = cv2.cvtColor(blurred, cv2.COLOR_RGB2LAB)
|
28 |
+
avg_lab = np.mean(img_lab, axis=(0, 1))
|
29 |
+
|
30 |
+
# Convert to grayscale for edge and texture computations
|
31 |
+
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
|
32 |
+
|
33 |
+
# Edge density: apply Canny and normalize
|
34 |
+
edges = cv2.Canny(gray, 100, 200)
|
35 |
+
avg_edge = np.mean(edges) / 255.0
|
36 |
+
|
37 |
+
# Texture: standard deviation (normalized)
|
38 |
+
avg_texture = np.std(gray) / 255.0
|
39 |
+
|
40 |
+
# Gradient magnitude using Sobel operators
|
41 |
+
grad_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
|
42 |
+
grad_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
|
43 |
+
grad_mag = np.sqrt(grad_x**2 + grad_y**2)
|
44 |
+
avg_grad = np.mean(grad_mag) / 255.0
|
45 |
+
|
46 |
+
return avg_lab, avg_edge, avg_texture, avg_grad
|
47 |
+
|
48 |
+
def build_kdtree():
|
49 |
+
"""
|
50 |
+
Build a KDTree from dataset images. Each image is resized to KD_TILE_SIZE,
|
51 |
+
its features are computed and then weighted (using weights: 1.0 for Lab channels,
|
52 |
+
0.5 for edge, texture, and gradient differences).
|
53 |
+
The KDTree along with the list of dataset images is stored in a pickle file.
|
54 |
+
"""
|
55 |
+
# Weights: for the Lab channels, weight = 1.0 (so sqrt(1.0)=1),
|
56 |
+
# for the other features, weight = 0.5 (so multiply by sqrt(0.5)).
|
57 |
+
scale = np.array([1.0, 1.0, 1.0, math.sqrt(0.5), math.sqrt(0.5), math.sqrt(0.5)])
|
58 |
+
|
59 |
+
feature_list = []
|
60 |
+
images_list = []
|
61 |
+
|
62 |
+
# Get full paths for images in the dataset folder
|
63 |
+
image_paths = [os.path.join(DATASET_FOLDER, img) for img in os.listdir(DATASET_FOLDER)
|
64 |
+
if img.lower().endswith(('.png', '.jpg', '.jpeg'))]
|
65 |
+
|
66 |
+
for img_path in image_paths:
|
67 |
+
img = cv2.imread(img_path)
|
68 |
+
if img is None:
|
69 |
+
continue
|
70 |
+
# Resize image to KD_TILE_SIZE and convert BGR -> RGB
|
71 |
+
img = cv2.resize(img, KD_TILE_SIZE)
|
72 |
+
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
73 |
+
|
74 |
+
# Compute features for the image
|
75 |
+
avg_lab, avg_edge, avg_texture, avg_grad = compute_features(img)
|
76 |
+
# Concatenate the features into a 6-dimensional vector:
|
77 |
+
raw_feature = np.concatenate([avg_lab, [avg_edge, avg_texture, avg_grad]])
|
78 |
+
# Apply weighting: multiply each element by the square-root of its weight
|
79 |
+
weighted_feature = raw_feature * scale
|
80 |
+
feature_list.append(weighted_feature)
|
81 |
+
images_list.append(img)
|
82 |
+
|
83 |
+
if not feature_list:
|
84 |
+
print("No images found in dataset folder!")
|
85 |
+
return
|
86 |
+
|
87 |
+
features = np.array(feature_list)
|
88 |
+
# Build the KDTree using the weighted features
|
89 |
+
tree = KDTree(features)
|
90 |
+
|
91 |
+
tree_data = {
|
92 |
+
'tree': tree,
|
93 |
+
'images': images_list,
|
94 |
+
'features': features # optional: may be used for debugging
|
95 |
+
}
|
96 |
+
|
97 |
+
# Save the KDTree and dataset images to a pickle file
|
98 |
+
with open(KD_TREE_PATH, "wb") as f:
|
99 |
+
pickle.dump(tree_data, f)
|
100 |
+
|
101 |
+
print(f"KDTree built and saved to {KD_TREE_PATH}. Total images: {len(images_list)}")
|
102 |
+
|
103 |
+
if __name__ == "__main__":
|
104 |
+
build_kdtree()
|