Spaces:
Sleeping
Sleeping
File size: 15,607 Bytes
f0196c3 | 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 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 | import numpy as np
from PIL import Image, ImageOps
import os
from tqdm import tqdm
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
import pickle
from collections import Counter
import random
class utils:
def __init__(self):
pass
@staticmethod
def calculate_padding(
image_height: int, image_width: int, window_size: tuple, stride: tuple
):
"""
Calculate padding needed for height and width
:param image_height:
:param image_width:
:param window_size:
:param stride:
:return: pad_height, pad_width
"""
# Calculate padding needed for height and width
if (image_height - window_size[0]) // stride[0] != (
image_height - window_size[0]
) / stride[0]:
if window_size[0] == stride[0]:
pad_height = window_size[0] - (image_height % stride[0])
else:
pad_height = image_height - (
((image_height - window_size[0]) // stride[0]) * stride[0]
+ window_size[0]
)
else:
pad_height = 0
if (image_width - window_size[1]) // stride[1] != (
image_width - window_size[1]
) / stride[1]:
if window_size[1] == stride[1]:
pad_width = window_size[1] - (image_width % stride[1])
else:
pad_width = image_width - (
((image_width - window_size[1]) // stride[1]) * stride[1]
+ window_size[1]
)
else:
pad_width = 0
return pad_height, pad_width
@staticmethod
def sliding_window(
image_dir: str, window_size: tuple, stride: tuple, padding_value: int = 0
):
"""
Slide a window across the image and extract patches.
:param image_dir: Path to the image file.
:param window_size: Tuple (height, width) of the window.
:param stride: Tuple (height, width) of the stride.
:param padding_value: Value to use for padding.
:return: List of tuples (window, (y, x)) and the size of the padded image.
"""
# Open the image
if isinstance(image_dir, str):
image = Image.open(image_dir)
else:
image = image_dir
# Convert image to a numpy array
image_np = np.array(image)
# Get image dimensions
image_height, image_width = image_np.shape[:2]
# if window_size[0] > image_height or window_size[1] > image_width:
# raise ValueError("Window size should be smaller than the image size.")
# if stride[0] > window_size[0] or stride[1] > window_size[1]:
# raise ValueError("Stride should be smaller than the window size.")
# Calculate padding needed for height and width
pad_height, pad_width = utils.calculate_padding(
image_height, image_width, window_size, stride
)
# Pad the image using Pillow (symmetric padding)
padded_image = ImageOps.expand(
image, border=(0, 0, pad_width, pad_height), fill=padding_value
)
# Convert padded image back to a numpy array
padded_image_np = np.array(padded_image)
# Get padded image dimensions
padded_height, padded_width = padded_image_np.shape[:2]
# List to store cropped windows and their positions
windows = []
# Slide the window across the padded image
for y in range(0, padded_height - window_size[0] + 1, stride[0]):
for x in range(0, padded_width - window_size[1] + 1, stride[1]):
# Crop the window from the image
window = padded_image_np[y : y + window_size[0], x : x + window_size[1]]
# Append the window and its position to the list
windows.append((Image.fromarray(window), (y, x)))
return windows, padded_image.size # Return windows and padded image size
@staticmethod
def save_windows(windows: list, out_dir: str):
"""
Save the cropped windows to the output directory.
Args:
windows: List of tuples (window, (y, x)) from the sliding_window function.
out_dir: Path to the output directory.
"""
# Create the output directory if it doesn't exist
if not os.path.exists(out_dir):
os.makedirs(out_dir)
# Save each window to the output directory
for i, (window, _) in enumerate(windows):
window.save(os.path.join(out_dir, f"window_{i}.png"))
@staticmethod
def reconstruct_image(windows: list, padded_size: tuple, window_size: tuple):
"""
Reconstruct the original image from the sliding window patches. Strides are averaged.
:param windows: List of tuples (window, (y, x)) from the sliding_window function.
:param padded_size: Tuple (height, width) of the padded image.
:param window_size: Tuple (height, width) of the window.
:return: Reconstructed image and count map.
"""
# Initialize an empty numpy array for the reconstructed image
num_channels = 4 if windows[0][0].mode == "RGBA" else 3
reconstructed_image = np.zeros(
(padded_size[1], padded_size[0], num_channels), dtype=np.float32
)
# Initialize an array to count the number of overlapping windows
count_map = np.zeros(
(padded_size[1], padded_size[0], num_channels), dtype=np.float32
)
# Place each window back into the reconstructed image
for window, (y, x) in windows:
window_np = np.array(window, dtype=np.float32)
# Add the window to the corresponding position in the reconstructed image
reconstructed_image[
y : y + window_size[1], x : x + window_size[0], :
] += window_np
# Increment the count map to handle overlaps
count_map[y : y + window_size[1], x : x + window_size[0], :] += 1
# Divide by the count map to average overlapping areas
reconstructed_image = np.divide(
reconstructed_image, count_map, where=count_map != 0
)
# Clip and convert to 8-bit image
reconstructed_image = np.clip(reconstructed_image, 0, 255).astype(np.uint8)
mode = "RGBA" if num_channels == 4 else "RGB"
reconstructed_image = Image.fromarray(reconstructed_image, mode=mode)
return reconstructed_image, count_map
@staticmethod
def zip_images(directory: str, label: int):
"""
This function reads images from a directory and returns them as a list of tuples (image, label).
:param directory: The directory containing images
:param label: The label to assign to all images in the directory
:return: List of tuples where each tuple is (image, label)
"""
data = []
range_dir = os.listdir(directory)
for file_name in tqdm(range_dir):
img_path = os.path.join(directory, file_name)
img = Image.open(img_path).convert("RGB") # Open image and convert to RGB
img_array = np.array(img) # Convert image to a numpy array
data.append((img_array, label)) # Append tuple (image, label) to the list
return data
@staticmethod
def unzip_images(data: list, base_directory: str):
"""
Unzips a list of tuples (image, label) and saves the images into directories named after their labels.
:param data: List of tuples (image, label)
:param base_directory: Base directory where the images will be saved
"""
for i, (img_array, label) in enumerate(data):
label_directory = os.path.join(base_directory, str(label))
# Create the label directory if it doesn't exist
if not os.path.exists(label_directory):
os.makedirs(label_directory)
# Define the image path (e.g., "label_directory/image_0.png")
img_path = os.path.join(label_directory, f"image_{i}.png")
# Convert the numpy array back to an image and save it
img = Image.fromarray(img_array)
img.save(img_path)
@staticmethod
def plot_image(image: np.array, title: str = None):
"""
This function plots an image using matplotlib.
:param image: Numpy array representing the image.
:param title: Title of the plot.
:return: None
"""
plt.imshow(image)
plt.axis("off")
if title:
plt.title(title)
plt.show()
@staticmethod
def plot_images(images: list, titles: list):
"""
This function plots multiple images side by side.
:param images: List of numpy arrays representing the images.
:param titles: List of titles for each image.
:return: None
"""
fig, axes = plt.subplots(1, len(images), figsize=(20, 20))
for i, (image, title) in enumerate(zip(images, titles)):
axes[i].imshow(image)
axes[i].axis("off")
axes[i].set_title(title)
plt.show()
@staticmethod
def split_data(
data,
train_size: float,
val_size: float,
test_size: float,
random_state: int = None,
):
"""
This function splits the dataset into training, validation, and test sets.
:param data: List of tuples where each tuple is (image, label)
:param train_size: Proportion of the dataset to include in the training set
:param val_size: Proportion of the dataset to include in the validation set
:param test_size: Proportion of the dataset to include in the test set
:param random_state: Controls the shuffling applied to the data before applying the split
:return: Tuple of (train_data, val_data, test_data) where each is a list of (image, label)
"""
# Ensure the split sizes add up to 1
assert np.isclose(
train_size + val_size + test_size, 1.0
), "Split sizes must add up to 1"
# First split: Train + (Val + Test)
train_data, temp_data = train_test_split(
data, train_size=train_size, random_state=random_state
)
# Second split: Val + Test
val_ratio = val_size / (
val_size + test_size
) # Adjust val_size to be relative to the size of temp_data
val_data, test_data = train_test_split(
temp_data, train_size=val_ratio, random_state=random_state
)
return train_data, val_data, test_data
@staticmethod
def save_to_pickle(data: list, file_path: str):
"""
This function saves data to a pickle file.
:param data: Data to save.
:param file_path: Path to save the pickle file.
:return: None
"""
with open(file_path, "wb") as f:
pickle.dump(data, f)
@staticmethod
def load_from_pickle(file_path: str):
"""
This function loads data from a pickle file.
:param file_path: Path to the pickle file.
:return: Loaded data.
"""
with open(file_path, "rb") as f:
return pickle.load(f)
@staticmethod
def plot_distribution(
train_data,
val_data,
test_data,
class_names,
title: str = "Split Data Distribution",
):
"""
Plots the distribution of data after splitting into training, validation, and test sets.
:param train_data: List of tuples (image, label) for the training set
:param val_data: List of tuples (image, label) for the validation set
:param test_data: List of tuples (image, label) for the test set
:param class_names: List of class names corresponding to the label
:param title: Title of the plot
:return: None
"""
# Count the number of samples for each label in each dataset
train_labels = [label for _, label in train_data]
val_labels = [label for _, label in val_data]
test_labels = [label for _, label in test_data]
train_counter = Counter(train_labels)
val_counter = Counter(val_labels)
test_counter = Counter(test_labels)
# Prepare the data for plotting
labels = sorted(set(train_labels + val_labels + test_labels))
train_counts = [train_counter[label] for label in labels]
val_counts = [val_counter[label] for label in labels]
test_counts = [test_counter[label] for label in labels]
# Plot the distribution
x = range(len(labels))
width = 0.25 # Width of the bars
plt.figure(figsize=(10, 6))
plt.bar(
x, train_counts, width=width, label="Train", color="blue", align="center"
)
plt.bar(
[p + width for p in x],
val_counts,
width=width,
label="Validation",
color="orange",
align="center",
)
plt.bar(
[p + width * 2 for p in x],
test_counts,
width=width,
label="Test",
color="green",
align="center",
)
plt.xlabel("Classes")
plt.ylabel("Number of Samples")
plt.title(title)
plt.xticks([p + width for p in x], [class_names[label] for label in labels])
plt.legend()
plt.show()
@staticmethod
def balance_data(train_data: list, random_state=None):
"""
This function balances the data by randomly selecting samples from the majority class to match the number of
samples in the minority class.
:param train_data: List of tuples (image, label)
:param random_state: int, random seed for reproducibility
:return: List of tuples (image, label)
with balanced classes and a list of tuples (image, label) with the excess data
"""
# get all labels from the train data
labels = [sample[1] for sample in train_data]
# count the number of samples for each label
counter = Counter(labels)
# find the label with the fewest samples
min_samples = min(counter.values())
# create a list to store the balanced data
balanced_data = []
# create a list to store the excess data
excess_data = []
# get how many classes we have
num_classes = len(set(labels))
# for each class
for i in range(num_classes):
# get all the samples for that class
samples = [sample for sample in train_data if sample[1] == i]
# shuffle the samples
random.seed(random_state)
random.shuffle(samples)
# add the first min_samples samples to the balanced data
balanced_data += samples[:min_samples]
# add the remaining samples to the excess data
excess_data += samples[min_samples:]
return balanced_data, excess_data
@staticmethod
def shuffle_data(data: list, random_state=None):
"""
This function shuffles the data.
:param data: List of tuples (image, label)
:param random_state: int, random seed for reproducibility
:return: List of tuples (image, label) with shuffled data
"""
random.seed(random_state)
random.shuffle(data)
return data
|