File size: 9,467 Bytes
798d901
 
 
 
0404f22
 
61ae52e
798d901
0404f22
 
 
 
61ae52e
0404f22
61ae52e
 
 
0404f22
 
 
 
61ae52e
 
 
 
 
0404f22
61ae52e
 
0404f22
61ae52e
 
 
 
 
0404f22
61ae52e
798d901
0404f22
61ae52e
0404f22
 
 
 
61ae52e
0404f22
 
 
 
 
61ae52e
0404f22
 
61ae52e
0404f22
61ae52e
0404f22
 
 
 
 
 
798d901
0404f22
 
798d901
0404f22
 
 
 
61ae52e
0404f22
 
 
 
 
 
61ae52e
798d901
61ae52e
0404f22
798d901
 
61ae52e
798d901
 
61ae52e
798d901
 
 
 
 
61ae52e
 
 
798d901
 
 
61ae52e
0404f22
 
 
61ae52e
0404f22
 
 
 
61ae52e
0404f22
 
 
61ae52e
798d901
 
 
61ae52e
0404f22
798d901
61ae52e
 
798d901
 
 
0404f22
 
61ae52e
 
798d901
 
 
 
 
 
61ae52e
798d901
 
 
 
 
61ae52e
 
 
798d901
 
 
 
61ae52e
798d901
 
 
61ae52e
798d901
 
 
 
0404f22
61ae52e
 
 
 
 
 
 
 
 
 
0404f22
61ae52e
 
 
 
0404f22
61ae52e
 
 
0404f22
798d901
 
61ae52e
0404f22
61ae52e
798d901
 
 
 
0404f22
 
61ae52e
 
 
 
0404f22
61ae52e
 
0404f22
61ae52e
0404f22
 
61ae52e
 
 
 
 
81a80cc
 
 
 
 
 
 
 
61ae52e
798d901
 
 
 
 
 
 
 
61ae52e
 
 
 
798d901
 
61ae52e
 
 
 
798d901
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
import gradio as gr
import cv2
import numpy as np
import os
import pickle
import math
from skimage.metrics import structural_similarity as ssim

# ----------------- Constants -----------------
DATASET_FOLDER = "Dataset"  # (Not used directly now)
KD_TREE_PATH = "kdtree_dataset.pkl"  # Path to the precomputed KDTree file
KD_TILE_SIZE = (50, 50)  # Must match the tile size used when building the KDTree

# ----------------- Feature Extraction Functions -----------------
def compute_features(image):
    """
    Compute a set of features for an image:
      - Average Lab color (using a Gaussian-blurred version)
      - Edge density using Canny edge detection (normalized)
      - Texture measure using the standard deviation of the grayscale image (normalized)
      - Average gradient magnitude computed via Sobel operators (normalized)
    Returns: (avg_lab, avg_edge, avg_texture, avg_grad)
    """
    blurred = cv2.GaussianBlur(image, (5, 5), 0)
    img_lab = cv2.cvtColor(blurred, cv2.COLOR_RGB2LAB)
    avg_lab = np.mean(img_lab, axis=(0, 1))
    
    gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
    edges = cv2.Canny(gray, 100, 200)
    avg_edge = np.mean(edges) / 255.0
    avg_texture = np.std(gray) / 255.0
    grad_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
    grad_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
    grad_mag = np.sqrt(grad_x**2 + grad_y**2)
    avg_grad = np.mean(grad_mag) / 255.0
    
    return avg_lab, avg_edge, avg_texture, avg_grad

def compute_weighted_features(image):
    """
    Compute the weighted feature vector for KDTree search.
    The image should be resized to KD_TILE_SIZE before feature extraction.
    Weights: for Lab channels use 1.0; for edge, texture, and gradient use 0.5
    (implemented as multiplying by sqrt(0.5)).
    """
    scale = np.array([1.0, 1.0, 1.0, math.sqrt(0.5), math.sqrt(0.5), math.sqrt(0.5)])
    avg_lab, avg_edge, avg_texture, avg_grad = compute_features(image)
    raw_feature = np.concatenate([avg_lab, [avg_edge, avg_texture, avg_grad]])
    weighted_feature = raw_feature * scale
    return weighted_feature

# ----------------- Utility Function -----------------
def ensure_min_size(image, min_size=7):
    """
    Ensure that the image has at least a minimum size; if not, resize it.
    """
    h, w = image.shape[:2]
    if h < min_size or w < min_size:
        new_w = max(min_size, w)
        new_h = max(min_size, h)
        image = cv2.resize(image, (new_w, new_h))
    return image

# ----------------- Mosaic Generation Functions -----------------
def create_photo_mosaic(input_image, kdtree_path, num_tiles_y, progress=None):
    """
    Create an image mosaic using a precomputed KDTree.
    For each mosaic tile in the input image, the tile is resized to KD_TILE_SIZE,
    its weighted features are computed, and the KDTree is queried to find the best match.
    The matched dataset image is then resized to the tile’s actual size before placement.
    """
    # Load the precomputed KDTree and dataset images
    with open(kdtree_path, "rb") as f:
        tree_data = pickle.load(f)
    tree = tree_data['tree']
    dataset_images = tree_data['images']

    original_image = input_image.copy()
    height, width, _ = original_image.shape

    # Determine mosaic grid dimensions
    tile_height = height // num_tiles_y
    aspect_ratio = width / height
    num_tiles_x = int(num_tiles_y * aspect_ratio)
    tile_width = width // num_tiles_x
    print(f"Adjusted number of tiles: {num_tiles_x} (width) x {num_tiles_y} (height)")

    mosaic = np.zeros_like(original_image)
    rows = list(range(0, height, tile_height))
    cols = list(range(0, width, tile_width))
    total_tiles = len(rows) * len(cols)
    tile_count = 0

    for y in rows:
        for x in cols:
            y_end = min(y + tile_height, height)
            x_end = min(x + tile_width, width)
            tile = original_image[y:y_end, x:x_end]

            # Resize the tile to the KDTree tile size for feature extraction
            tile_resized = cv2.resize(tile, KD_TILE_SIZE)
            query_feature = compute_weighted_features(tile_resized)

            # Query the KDTree for the best match (returns index)
            dist, ind = tree.query([query_feature], k=1)
            best_index = ind[0][0]
            best_match = dataset_images[best_index]

            # Resize the best match image to the current tile size and place it into the mosaic
            best_match_resized = cv2.resize(best_match, (x_end - x, y_end - y))
            mosaic[y:y_end, x:x_end] = best_match_resized

            tile_count += 1
            if progress is not None:
                progress(tile_count / total_tiles)

    # Save the final mosaic (convert from RGB to BGR for saving with cv2)
    output_path = "mosaic_output.jpg"
    cv2.imwrite(output_path, cv2.cvtColor(mosaic, cv2.COLOR_RGB2BGR))
    return output_path

def create_color_mosaic(input_image, num_tiles_y, progress=None):
    """
    Create a simple color mosaic by dividing the image into grid cells and filling
    each cell with its average RGB color.
    """
    original_image = input_image.copy()
    height, width, _ = original_image.shape
    tile_height = height // num_tiles_y
    aspect_ratio = width / height
    num_tiles_x = int(num_tiles_y * aspect_ratio)
    tile_width = width // num_tiles_x
    print(f"Adjusted number of tiles: {num_tiles_x} (width) x {num_tiles_y} (height)")

    mosaic = np.zeros_like(original_image)
    rows = list(range(0, height, tile_height))
    cols = list(range(0, width, tile_width))
    total_tiles = len(rows) * len(cols)
    tile_count = 0

    for y in rows:
        for x in cols:
            y_end = min(y + tile_height, height)
            x_end = min(x + tile_width, width)
            tile = original_image[y:y_end, x:x_end]
            avg_color = np.mean(tile, axis=(0, 1)).astype(np.uint8)
            mosaic[y:y_end, x:x_end] = avg_color
            tile_count += 1
            if progress is not None:
                progress(tile_count / total_tiles)

    output_path = "color_mosaic_output.jpg"
    cv2.imwrite(output_path, cv2.cvtColor(mosaic, cv2.COLOR_RGB2BGR))
    return output_path

# ----------------- Performance Metrics -----------------
def compute_mse(original, mosaic):
    """
    Compute Mean Squared Error (MSE) between two images.
    """
    original = original.astype("float")
    mosaic = mosaic.astype("float")
    err = np.sum((original - mosaic) ** 2)
    mse = err / float(original.shape[0] * original.shape[1] * original.shape[2])
    return mse

def compute_ssim_metric(original, mosaic):
    """
    Compute Structural Similarity Index (SSIM) between two images.
    """
    min_dim = min(original.shape[0], original.shape[1])
    win_size = 7 if min_dim >= 7 else (min_dim if min_dim % 2 == 1 else min_dim - 1)
    ssim_value, _ = ssim(original, mosaic, win_size=win_size, channel_axis=-1, full=True)
    return ssim_value

# ----------------- Gradio Interface -----------------
def mosaic_gradio(input_image, num_tiles_y, mosaic_type, progress=gr.Progress()):
    """
    Gradio interface function to generate and return the mosaic image along with performance metrics.
    mosaic_type: "Color Mosaic" or "Image Mosaic"
    Returns: (mosaic_image_file, performance_metrics_string)
    """
    if mosaic_type == "Color Mosaic":
        mosaic_path = create_color_mosaic(input_image, num_tiles_y, progress)
    else:
        mosaic_path = create_photo_mosaic(input_image, KD_TREE_PATH, num_tiles_y, progress)

    mosaic_image = cv2.imread(mosaic_path)
    if mosaic_image is None:
        return None, "Error: Mosaic image could not be loaded."
    mosaic_image = cv2.cvtColor(mosaic_image, cv2.COLOR_BGR2RGB)

    input_for_metrics = ensure_min_size(input_image.copy())
    mosaic_for_metrics = ensure_min_size(mosaic_image.copy())

    mse_value = compute_mse(input_for_metrics, mosaic_for_metrics)
    ssim_value = compute_ssim_metric(input_for_metrics, mosaic_for_metrics)

    metrics_text = f"MSE: {mse_value:.2f}\nSSIM: {ssim_value:.4f}"
    return mosaic_path, metrics_text

# ----------------- Gradio App Setup -----------------
examples = [
    ["input_images/1.jpg", 90, "Image Mosaic"],
    ["input_images/2.jpg", 90, "Image Mosaic"],
    ["input_images/3.jpg", 90, "Image Mosaic"],
    ["input_images/6.jpg", 90, "Image Mosaic"],
    ["input_images/7.jpg", 90, "Image Mosaic"],
    ["input_images/8.jpg", 90, "Image Mosaic"],
    ["input_images/9.jpg", 90, "Image Mosaic"],
    ["input_images/10.jpg", 90, "Image Mosaic"]
]

iface = gr.Interface(
    fn=mosaic_gradio,
    inputs=[
        gr.Image(type="numpy", label="Upload Image"),
        gr.Slider(10, 200, value=90, step=5, label="Number of Tiles (Height)"),
        gr.Radio(choices=["Color Mosaic", "Image Mosaic"], label="Mosaic Type", value="Image Mosaic")
    ],
    outputs=[
        gr.Image(type="filepath", label="Generated Mosaic"),
        gr.Textbox(label="Performance Metrics")
    ],
    title="Photo Mosaic Generator",
    description=("Upload an image, choose the number of tiles (height) and mosaic type. "
                 "Select 'Color Mosaic' for a mosaic using average colors, or 'Image Mosaic' to use dataset images "
                 "matched by color, edge density, texture, and gradient features. "
                 "After mosaic generation, performance metrics (MSE and SSIM) will be displayed."),
    examples=examples
)

iface.launch()