[email protected] commited on
Commit
0404f22
·
1 Parent(s): 1042da1

Add preprocess files

Browse files
Files changed (3) hide show
  1. app.py +70 -133
  2. kdtree_dataset.pkl +3 -0
  3. 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
- # Folder containing your dataset of tiles (small images)
8
- DATASET_FOLDER = "Dataset"
 
 
9
 
 
10
  def compute_features(image):
11
  """
12
  Compute a set of features for an image:
13
- - Average Lab color (using a Gaussian-blurred version)
14
- - Edge density using Canny edge detection (normalized)
15
- - Texture measure using the standard deviation of the grayscale image (normalized)
16
- - Average gradient magnitude computed via Sobel operators (normalized)
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 # Normalized edge density
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 load_dataset_images(folder_path, tile_size):
43
  """
44
- Loads images from a folder, resizes them to tile_size, and computes a set of features:
45
- (RGB image, average Lab color, edge density, texture measure, gradient magnitude, image path)
 
 
46
  """
47
- dataset = []
48
- image_paths = [os.path.join(folder_path, img) for img in os.listdir(folder_path)
49
- if img.lower().endswith(('.png', '.jpg', '.jpeg'))]
 
 
50
 
51
- for img_path in image_paths:
52
- img = cv2.imread(img_path)
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
- Finds the best matching dataset image based on a weighted combination of:
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
- tile_lab, tile_edge, tile_texture, tile_grad = tile_features
77
- min_dist = float('inf')
78
- best_match = None
79
-
80
- for data in dataset:
81
- ds_img, ds_lab, ds_edge, ds_texture, ds_grad, ds_path = data
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
- def create_photo_mosaic(input_image, dataset_folder, num_tiles_y, progress=None):
 
102
  """
103
- Creates an image mosaic using dataset images. For each tile of the input image,
104
- it computes a feature vector (color, edge, texture, gradient) and finds the best
105
- matching dataset image based on these features.
 
106
  """
107
- # Assume the uploaded image from Gradio is in RGB format
 
 
 
 
 
108
  original_image = input_image.copy()
109
  height, width, _ = original_image.shape
110
 
111
- # Compute tile height and determine tile width based on aspect ratio
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
- # Compute feature vector for the tile
141
- tile_features = compute_features(tile)
 
142
 
143
- # Find the best matching dataset image using the combined feature metric
144
- best_match = find_best_match(tile_features, dataset)
 
 
145
 
146
- if best_match is not None:
147
- # Crop the dataset image if necessary to match the tile size
148
- mosaic[y:y_end, x:x_end] = best_match[:y_end - y, :x_end - x]
149
 
150
  tile_count += 1
151
  if progress is not None:
152
  progress(tile_count / total_tiles)
153
 
154
- # Save the final mosaic. Since mosaic is in RGB, convert to BGR for cv2.imwrite.
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
- Creates a simple color mosaic by dividing the image into grid cells and
163
- filling each cell with its average RGB color.
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 Functions -----------------
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 compute_ssim(original, mosaic):
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
- def ensure_min_size(image, min_size=7):
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: Either "Color Mosaic" or "Image Mosaic"
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, DATASET_FOLDER, num_tiles_y, progress)
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 = compute_ssim(input_for_metrics, mosaic_for_metrics)
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()