AIOmarRehan commited on
Commit
54dd6b7
·
verified ·
1 Parent(s): dc76bf9

Upload 10 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ Results/Animal[[:space:]]Classifier.mp4 filter=lfs diff=lfs merge=lfs -text
Notebook and Py File/Custom_Animal_Classification.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
Notebook and Py File/InceptionV3_Image_Classification.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
Notebook and Py File/finetuning_inceptionv3_image_classification.py ADDED
@@ -0,0 +1,786 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """InceptionV3_Image_Classification.ipynb
3
+
4
+ Automatically generated by Colab.
5
+
6
+ Original file is located at
7
+ https://colab.research.google.com/drive/1UmUDdji0iQ-LK2g0MtxytO8M7cpioH3C
8
+
9
+ # **Import Dependencies**
10
+ """
11
+
12
+ import warnings
13
+ warnings.filterwarnings('ignore')
14
+
15
+ import zipfile
16
+ import hashlib
17
+ import matplotlib.pyplot as plt
18
+ import pandas as pd
19
+ import os
20
+ import uuid
21
+ import re
22
+ import random
23
+ import cv2
24
+ import numpy as np
25
+ import tensorflow as tf
26
+ import seaborn as sns
27
+ from google.colab import drive
28
+ from google.colab import files
29
+ from pathlib import Path
30
+ from PIL import Image, ImageStat, UnidentifiedImageError, ImageEnhance
31
+ from matplotlib import patches
32
+ from tqdm import tqdm
33
+ from collections import defaultdict
34
+ from sklearn.preprocessing import LabelEncoder, label_binarize
35
+ from sklearn.model_selection import train_test_split
36
+ from sklearn.utils import resample
37
+ from tensorflow import keras
38
+ from tensorflow.keras.applications.inception_v3 import InceptionV3, preprocess_input
39
+ from tensorflow.keras.utils import to_categorical
40
+ from tensorflow.keras import layers, models, optimizers, callbacks, regularizers
41
+ from tensorflow.keras.models import Sequential
42
+ from tensorflow.keras.layers import Conv2D, BatchNormalization, MaxPooling2D,Dropout, Flatten, Dense, GlobalAveragePooling2D
43
+ from tensorflow.keras.regularizers import l2
44
+ from tensorflow.keras.optimizers import Adam, AdamW
45
+ from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping, ModelCheckpoint
46
+ from tensorflow.keras import Input, Model
47
+ from tensorflow.keras.preprocessing.image import ImageDataGenerator, img_to_array, load_img, array_to_img
48
+ from tensorflow.keras.preprocessing import image
49
+ from sklearn.metrics import classification_report,ConfusionMatrixDisplay, confusion_matrix, roc_auc_score, roc_curve, precision_score, recall_score, f1_score, precision_recall_fscore_support, auc
50
+
51
+ print(tf.__version__)
52
+
53
+ drive.mount('/content/drive')
54
+ zip_path = '/content/drive/MyDrive/Animals.zip'
55
+ extract_to = '/content/my_data'
56
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
57
+ zip_ref.extractall(extract_to)
58
+
59
+ """# **Convert Dataset to a Data Frame**"""
60
+
61
+ image_extensions = {'.jpg', '.jpeg', '.png'}
62
+ paths = [(path.parts[-2], path.name, str(path)) for path in Path(extract_to).rglob('*.*') if path.suffix.lower() in image_extensions]
63
+
64
+ df = pd.DataFrame(paths, columns = ['class', 'image', 'full_path'])
65
+ df = df.sort_values('class', ascending = True)
66
+ df.reset_index(drop = True, inplace = True)
67
+ df
68
+
69
+ """# **EDA Process**"""
70
+
71
+ class_count = df['class'].value_counts()
72
+ for cls, count in class_count.items():
73
+ print(f'Class: {cls}, Count: {count} images')
74
+
75
+ print(f"\nTotal dataset size is: {len(df)} images")
76
+ print(f"Number of classes: {df['class'].nunique()} classes")
77
+
78
+ plt.figure(figsize = (32, 16))
79
+ class_count.plot(kind = 'bar', color = 'skyblue', edgecolor = 'black')
80
+ plt.title('Number of Images per Class')
81
+ plt.xlabel('Class')
82
+ plt.ylabel('Count')
83
+ plt.xticks(rotation = 45)
84
+ plt.show()
85
+
86
+ plt.figure(figsize = (32, 16))
87
+ class_count.plot(kind = 'pie', autopct = '%1.1f%%', colors = plt.cm.Paired.colors)
88
+ plt.title('Percentage of Images per Class')
89
+ plt.ylabel('')
90
+ plt.show()
91
+
92
+ percentages = (class_count / len(df)) * 100
93
+ imbalance_df = pd.DataFrame({'Count': class_count, 'Percentage %': percentages.round(2)})
94
+ print(imbalance_df)
95
+
96
+ plt.figure(figsize = (32, 16))
97
+ class_count.plot(kind = 'bar', color = 'lightgreen', edgecolor = 'black')
98
+ plt.title('Class Distribution Check')
99
+ plt.xlabel('Class')
100
+ plt.ylabel('Count')
101
+ plt.xticks(rotation = 45)
102
+ plt.axhline(y = class_count.mean(), color = 'red', linestyle = '--', label = 'Average Count')
103
+ plt.legend()
104
+ plt.show()
105
+
106
+ image_sizes = []
107
+
108
+ for file_path in df['full_path']:
109
+ with Image.open(file_path) as img:
110
+ image_sizes.append(img.size)
111
+
112
+ sizes_df = pd.DataFrame(image_sizes, columns=['Width', 'Height'])
113
+
114
+ #Width
115
+ plt.figure(figsize=(8,5))
116
+ plt.scatter(x = range(len(sizes_df)), y = sizes_df['Width'], color='skyblue', s=10)
117
+ plt.title('Image Width Distribution')
118
+ plt.xlabel('Width (pixels)')
119
+ plt.ylabel('Frequency')
120
+ plt.show()
121
+
122
+ #Height
123
+ plt.figure(figsize=(8,5))
124
+ plt.scatter(x = sizes_df['Height'], y = range(len(sizes_df)), color='lightgreen', s=10)
125
+ plt.title('Image Height Distribution')
126
+ plt.xlabel('Height (pixels)')
127
+ plt.ylabel('Frequency')
128
+ plt.show()
129
+
130
+ #For best sure the size of the whole images
131
+ unique_sizes = sizes_df.value_counts().reset_index(name='Count')
132
+ print(unique_sizes)
133
+
134
+ image_data = []
135
+
136
+ for file_path in df['full_path']:
137
+ with Image.open(file_path) as img:
138
+ width, height = img.size
139
+ mode = img.mode # e.g., 'RGB', 'L', 'RGBA', etc.
140
+ channels = len(img.getbands()) # Number of channels
141
+ image_data.append((width, height, mode, channels))
142
+
143
+ # Create DataFrame
144
+ image_df = pd.DataFrame(image_data, columns=['Width', 'Height', 'Mode', 'Channels'])
145
+
146
+ print("Image Mode Distribution:")
147
+ print(image_df['Mode'].value_counts())
148
+
149
+ print("\nNumber of Channels Distribution:")
150
+ print(image_df['Channels'].value_counts())
151
+
152
+ plt.figure(figsize=(6,4))
153
+ image_df['Mode'].value_counts().plot(kind='bar', color='coral')
154
+ plt.title("Image Mode Distribution")
155
+ plt.xlabel("Mode")
156
+ plt.ylabel("Count")
157
+ plt.xticks(rotation=45)
158
+ plt.tight_layout()
159
+ plt.show()
160
+
161
+ plt.figure(figsize=(6,4))
162
+ image_df['Channels'].value_counts().sort_index().plot(kind='bar', color='slateblue')
163
+ plt.title("Number of Channels per Image")
164
+ plt.xlabel("Channels")
165
+ plt.ylabel("Count")
166
+ plt.xticks(rotation=0)
167
+ plt.tight_layout()
168
+ plt.show()
169
+
170
+ sample_df = df.sample(n = 10, random_state = 42)
171
+
172
+ plt.figure(figsize=(32, 16))
173
+
174
+ for i, (cls, img_name, full_path) in enumerate(sample_df.values):
175
+ with Image.open(full_path) as img:
176
+ stat = ImageStat.Stat(img.convert("RGB")) #Convert images to RGB images
177
+ brightness = stat.mean[0]
178
+ contrast = stat.stddev[0]
179
+
180
+ width, height = img.size
181
+ # Print size to console
182
+ print(f"Image: {img_name} | Class: {cls} | Size: {width}x{height} | Brightness: {brightness:.1f} | Contrast: {contrast:.1f}")
183
+
184
+ plt.subplot(2, 5, i + 1)
185
+ plt.imshow(img)
186
+ plt.axis('off')
187
+ plt.title(f"Class: {cls}\nImage: {img_name}\nBrightness: {brightness:.2f}\nContrast: {contrast:.2f} \nSize: {width}x{height}")
188
+
189
+ plt.tight_layout
190
+ plt.show()
191
+
192
+ # Sample 20 random images
193
+ num_samples = 20
194
+ sample_df = df.sample(num_samples, random_state=42)
195
+
196
+ # Get sorted class list and color map
197
+ classes = sorted(df['class'].unique())
198
+ colors = plt.cm.tab10.colors
199
+
200
+ # Grid setup
201
+ cols = 4
202
+ rows = num_samples // cols + int(num_samples % cols > 0)
203
+
204
+ # Figure setup
205
+ plt.figure(figsize=(15, 5 * rows))
206
+
207
+ for idx, (cls, img_name, full_path) in enumerate(sample_df.values):
208
+ with Image.open(full_path) as img:
209
+ ax = plt.subplot(rows, cols, idx + 1)
210
+ ax.imshow(img)
211
+ ax.axis('off')
212
+
213
+ # Title with class info
214
+ ax.set_title(
215
+ f"Class: {cls} \nImage: {img_name} \nSize: {img.width} x {img.height}",
216
+ fontsize=10
217
+ )
218
+
219
+ # Rectangle in axes coords: full width, small height at top
220
+ label_height = 0.1 # 10% of image height
221
+ label_width = 1.0 # full width of the image
222
+
223
+ rect = patches.Rectangle(
224
+ (0, 1 - label_height), label_width, label_height,
225
+ transform=ax.transAxes,
226
+ linewidth=0,
227
+ edgecolor=None,
228
+ facecolor=colors[classes.index(cls) % len(colors)],
229
+ alpha=0.7
230
+ )
231
+ ax.add_patch(rect)
232
+
233
+ # Add class name text centered horizontally
234
+ ax.text(
235
+ 0.5, 1 - label_height / 2,
236
+ cls,
237
+ transform=ax.transAxes,
238
+ fontsize=12,
239
+ color="white",
240
+ fontweight="bold",
241
+ va="center",
242
+ ha="center"
243
+ )
244
+
245
+ # Figure title and layout
246
+ plt.suptitle("Random Dataset Samples - Sanity Check", fontsize=18, fontweight="bold")
247
+ plt.tight_layout(rect=[0, 0, 1, 0.96])
248
+ plt.show()
249
+
250
+ #Check missing files
251
+ print("Missing values per column: ")
252
+ print(df.isnull().sum())
253
+
254
+ #Check duplicate files
255
+ duplicate_names = df.duplicated().sum()
256
+ print(f"\nNumber of duplicate files: {duplicate_names}")
257
+
258
+ duplicate_names = df[df.duplicated(subset = ['image'], keep = False)]
259
+ print(f"Duplicate file names: {len(duplicate_names)}")
260
+
261
+ #Check if two images or more are the same even if they are having different file names
262
+ def get_hash(file_path):
263
+ with open(file_path, 'rb') as f:
264
+ return hashlib.md5(f.read()).hexdigest()
265
+
266
+ df['file_hash'] = df['full_path'].apply(get_hash)
267
+ duplicate_hashes = df[df.duplicated(subset = ['file_hash'], keep = False)]
268
+ print(f"Duplicate image files: {len(duplicate_hashes)}")
269
+
270
+ #This code below just removing the duplicate files, which means will not be feeded to the model, but will be still in the actual directory
271
+ #Important note: duplicates are removed from the dataframe only, not from the actual directory.
272
+ #Drop duplicates based on file_hash, keeping the first one
273
+
274
+ # df_unique = df.drop_duplicates(subset='file_hash', keep='first')
275
+ # print(f"After removing duplicates, unique images: {len(df_unique)}")
276
+
277
+ #Check for images extentions
278
+ df['extenstion'] = df['image'].apply(lambda x: Path(x).suffix.lower())
279
+ print("File type counts: ")
280
+ #print(df['extenstion'].value_counts)
281
+ print(df['extenstion'].value_counts())
282
+
283
+ #Check for resolution relationships
284
+ df['Width'] = sizes_df['Width']
285
+ df['Height'] = sizes_df['Height']
286
+ #print(df.groupby(['Width', 'Height']).size())
287
+ print(df.groupby('class')[['Width', 'Height']].agg(['min', 'max', 'mean']))
288
+
289
+ #Check for class balance (relationship between label and count)
290
+ class_summary = df['class'].value_counts(normalize = False).to_frame('Count')
291
+ #class_summary['Percentage'] = class_summary['Count'] / class_summary['Count'].sum() * 100
292
+ #class_summary
293
+ class_summary['Percentage %'] = round((class_summary['Count'] / len(df)) * 100, 2)
294
+ print(class_summary)
295
+
296
+ """# **Data Cleaning Process**"""
297
+
298
+ corrupted_files = []
299
+
300
+ for file_path in df['full_path']:
301
+ try:
302
+ with Image.open(file_path) as img:
303
+ img.verify()
304
+ except (UnidentifiedImageError, OSError):
305
+ corrupted_files.append(file_path)
306
+
307
+ print(f"Found {len(corrupted_files)} corrupted images.")
308
+ #except (IOError, SyntaxError) as e:
309
+ #corrupted_files.append(file_path)
310
+
311
+ #print(f"Number of corrupted files: {len(corrupted_files)}")
312
+
313
+ if corrupted_files:
314
+ df = df[~df['full_path'].isin(corrupted_files)].reset_index(drop = True)
315
+ print("Corrupted files removed.")
316
+
317
+ #Outliers detection
318
+ #Resolution-based outlier detection
319
+ #width_mean = width_std = sizes_df['Width'].mean(), sizes_df['Width'].std()
320
+ #height_mean = height_std = sizes_df['Height'].mean(), sizes_df['Height'].std()
321
+
322
+ width_mean = sizes_df['Width'].mean()
323
+ width_std = sizes_df['Width'].std()
324
+ height_mean = sizes_df['Height'].mean()
325
+ height_std = sizes_df['Height'].std()
326
+
327
+ outliers = df[(df['Width'] > width_mean + 3 * width_std) | (df['Width'] < width_mean - 3 * width_std) | (df['Height'] > height_mean + 3 * height_std) | (df['Height'] < height_mean - 3 * height_std)]
328
+ #print(f"Number of outliers: {len(outliers)}")
329
+ print(f"Found {len(outliers)} resolution outliers.")
330
+
331
+ df["image"] = df["full_path"].apply(lambda p: Image.open(p).convert('RGB')) #Convert it to RGB for flexibility
332
+
333
+ too_dark = []
334
+ too_bright = []
335
+ blank_or_gray = []
336
+
337
+ # Thresholds
338
+ dark_threshold = 30 # Below this is too dark
339
+ bright_threshold = 225 # Above this is too bright
340
+ low_contrast_threshold = 5 # Low contrast ~ blank/gray
341
+
342
+ for idx, img in enumerate(df["image"]):
343
+ gray = img.convert('L')
344
+ stat = ImageStat.Stat(gray) # Convert to grayscale for brightness/contrast analysis
345
+ brightness = stat.mean[0]
346
+ contrast = stat.stddev[0]
347
+
348
+ if brightness < dark_threshold:
349
+ too_dark.append(idx)
350
+ elif brightness > bright_threshold:
351
+ too_bright.append(idx)
352
+ elif contrast < low_contrast_threshold:
353
+ blank_or_gray.append(idx)
354
+
355
+ print(f"Too dark images: {len(too_dark)}")
356
+ print(f"Too bright images: {len(too_bright)}")
357
+ print(f"Blank/gray images: {len(blank_or_gray)}")
358
+
359
+ # df = df.drop(index=too_bright + blank_or_gray).reset_index(drop=True) --> DROPS too_bright + blank_or_gray TOGETHER!
360
+
361
+ for idx, row in tqdm(df.iterrows(), total=len(df), desc="Enhancing images"):
362
+ img = row["image"]
363
+
364
+ # Enhance too dark images
365
+ if row["full_path"] in df.loc[too_dark, "full_path"].values:
366
+ img = ImageEnhance.Brightness(img).enhance(1.5) # Increase brightness
367
+ img = ImageEnhance.Contrast(img).enhance(1.5) # Increase contrast
368
+
369
+ # Decrease brightness for too bright images
370
+ if row["full_path"] in df.loc[too_bright, "full_path"].values:
371
+ img = ImageEnhance.Brightness(img).enhance(0.7) # Decrease brightness (less than 1)
372
+ img = ImageEnhance.Contrast(img).enhance(1.2) # Optionally, you can also enhance contrast
373
+
374
+ # Overwrite the image back into the DataFrame
375
+ df.at[idx, "image"] = img
376
+
377
+ print(f"Enhanced images in memory: {len(df)}")
378
+
379
+ # Lists to store paths of still too dark and too bright images
380
+ still_dark = []
381
+ still_bright = []
382
+
383
+ # Threshold for "too bright" (already defined as bright_threshold)
384
+ for idx, img in enumerate(df["image"]):
385
+ gray = img.convert('L') # Convert to grayscale for brightness analysis
386
+ stat = ImageStat.Stat(gray)
387
+ brightness = stat.mean[0]
388
+
389
+ # Check if the image is still too dark
390
+ if brightness < dark_threshold:
391
+ still_dark.append(df.loc[idx, 'full_path'])
392
+
393
+ # Check if the image is too bright
394
+ if brightness > bright_threshold:
395
+ still_bright.append(df.loc[idx, 'full_path'])
396
+
397
+ print(f"Still too dark after enhancement: {len(still_dark)} images")
398
+ print(f"Still too bright after enhancement: {len(still_bright)} images")
399
+
400
+ # Point to the extracted dataset, not the zip file location
401
+ dataset_root = "/content/my_data/Animals"
402
+
403
+ # Check mislabeled images
404
+ mismatches = []
405
+ for i, row in df.iterrows():
406
+ folder_name = os.path.basename(os.path.dirname(row["full_path"]))
407
+ if row["class"] != folder_name:
408
+ mismatches.append((row["full_path"], row["class"], folder_name))
409
+
410
+ print(f"Found {len(mismatches)} mislabeled images (class vs folder mismatch).")
411
+
412
+ # Compare classes vs folders
413
+ classes_in_df = set(df["class"].unique())
414
+ folders_in_fs = {f for f in os.listdir(dataset_root) if os.path.isdir(os.path.join(dataset_root, f))}
415
+
416
+ print("Classes in DF but not in folders:", classes_in_df - folders_in_fs)
417
+ print("Folders in FS but not in DF:", folders_in_fs - classes_in_df)
418
+
419
+ def check_file_naming_issues(df):
420
+ issues = {"invalid_chars": [], "spaces": [], "long_paths": [], "case_conflicts": [], "duplicate_names_across_classes": []}
421
+
422
+ seen_names = {}
423
+
424
+ for _, row in df.iterrows():
425
+ fpath = row["full_path"] # full path
426
+ fname = os.path.basename(fpath) # just filename
427
+ cls = row["class"]
428
+
429
+ if re.search(r'[<>:"/\\|?*]', fname): # Windows restricted chars
430
+ issues["invalid_chars"].append(fpath)
431
+
432
+ if " " in fname or fname.startswith(" ") or fname.endswith(" "):
433
+ issues["spaces"].append(fpath)
434
+
435
+ if len(fpath) > 255:
436
+ issues["long_paths"].append(fpath)
437
+
438
+ lower_name = fname.lower()
439
+ if lower_name in seen_names and seen_names[lower_name] != cls:
440
+ issues["case_conflicts"].append((fpath, seen_names[lower_name]))
441
+ else:
442
+ seen_names[lower_name] = cls
443
+
444
+ duplicates = df.groupby(df["full_path"].apply(os.path.basename))["class"].nunique()
445
+ duplicates = duplicates[duplicates > 1].index.tolist()
446
+ for dup in duplicates:
447
+ dup_paths = df[df["full_path"].str.endswith(dup)]["full_path"].tolist()
448
+ issues["duplicate_names_across_classes"].extend(dup_paths)
449
+
450
+ return issues
451
+
452
+ # Run the check
453
+ naming_issues = check_file_naming_issues(df)
454
+
455
+ for issue_type, files in naming_issues.items():
456
+ print(f"\n{issue_type.upper()} ({len(files)})")
457
+ for f in files[:10]: # preview first 10
458
+ print(f)
459
+
460
+ """# **Data Preprocessing Process**"""
461
+
462
+ def preprocess_image(path, target_size=(256, 256), augment=True):
463
+ img = tf.io.read_file(path)
464
+ img = tf.image.decode_image(img, channels=3, expand_animations=False)
465
+ img = tf.image.resize(img, target_size)
466
+ img = tf.cast(img, tf.float32) / 255.0
467
+
468
+ if augment and tf.random.uniform(()) < 0.1: # Only 10% chance
469
+ img = tf.image.random_flip_left_right(img)
470
+ img = tf.image.random_flip_up_down(img)
471
+ img = tf.image.random_brightness(img, max_delta=0.1)
472
+ img = tf.image.random_contrast(img, lower=0.9, upper=1.1)
473
+
474
+ return img
475
+
476
+ le = LabelEncoder()
477
+ df['label'] = le.fit_transform(df['class'])
478
+
479
+ # Prepare paths and labels
480
+ paths = df['full_path'].values
481
+ labels = df['label'].values
482
+
483
+ AUTOTUNE = tf.data.AUTOTUNE
484
+ batch_size = 32
485
+
486
+ # Split data into train+val and test (10% test)
487
+ train_val_paths, test_paths, train_val_labels, test_labels = train_test_split(
488
+ paths, labels, test_size=0.1, random_state=42, stratify=labels
489
+ )
490
+
491
+ # Split train+val into train and val (10% of train_val as val)
492
+ train_paths, val_paths, train_labels, val_labels = train_test_split(
493
+ train_val_paths, train_val_labels, test_size=0.1, random_state=42, stratify=train_val_labels
494
+ )
495
+
496
+ # Create datasets
497
+ def load_and_preprocess(path, label):
498
+ return preprocess_image(path), label
499
+
500
+ train_ds = tf.data.Dataset.from_tensor_slices((train_paths, train_labels))
501
+ train_ds = train_ds.map(lambda x, y: (preprocess_image(x, augment=True), y), num_parallel_calls=AUTOTUNE)
502
+ train_ds = train_ds.shuffle(1024).batch(batch_size).prefetch(AUTOTUNE)
503
+
504
+ val_ds = tf.data.Dataset.from_tensor_slices((val_paths, val_labels))
505
+ val_ds = val_ds.map(load_and_preprocess, num_parallel_calls=AUTOTUNE)
506
+ val_ds = val_ds.batch(batch_size).prefetch(AUTOTUNE)
507
+
508
+ test_ds = tf.data.Dataset.from_tensor_slices((test_paths, test_labels))
509
+ test_ds = test_ds.map(load_and_preprocess, num_parallel_calls=AUTOTUNE)
510
+ test_ds = test_ds.batch(batch_size).prefetch(AUTOTUNE)
511
+
512
+ print("Dataset sizes:")
513
+ print(f"Train: {len(train_paths)} images")
514
+ print(f"Validation: {len(val_paths)} images")
515
+ print(f"Test: {len(test_paths)} images")
516
+ print("--------------------------------------------------")
517
+ print("Train labels sample:", train_labels[:10])
518
+ print("Validation labels sample:", val_labels[:10])
519
+ print("Test labels sample:", test_labels[:10])
520
+
521
+ # Preview normalized image stats and visualization
522
+ for image_batch, label_batch in train_ds.take(1):
523
+ # Print pixel value stats for first image in the batch
524
+ image = image_batch[0]
525
+ label = label_batch[0]
526
+ print("Image dtype:", image.dtype)
527
+ print("Min pixel value:", tf.reduce_min(image).numpy())
528
+ print("Max pixel value:", tf.reduce_max(image).numpy())
529
+ print("Label:", label.numpy())
530
+
531
+ # Show the image
532
+ plt.imshow(image.numpy())
533
+ plt.title(f"Label: {label.numpy()}")
534
+ plt.axis('off')
535
+ plt.show()
536
+ print("---------------------------------------------------")
537
+ print("Number of Classes: ", len(le.classes_))
538
+
539
+ # After train_ds is defined
540
+ for image_batch, label_batch in train_ds.take(1):
541
+ print("Image batch shape:", image_batch.shape) # full batch shape
542
+ print("Label batch shape:", label_batch.shape) # labels shape
543
+
544
+ input_shape = image_batch.shape[1:] # shape of a single image
545
+ print("Single image shape:", input_shape)
546
+ break
547
+
548
+ """# **Model Loading**"""
549
+
550
+ inception = InceptionV3(input_shape=input_shape, weights='imagenet', include_top=False)
551
+
552
+ # don't train existing weights
553
+ for layer in inception.layers:
554
+ layer.trainable = False
555
+
556
+ # Number of classes
557
+ print("Number of Classes: ", len(le.classes_))
558
+
559
+ x = GlobalAveragePooling2D()(inception.output)
560
+ x = Dense(512, activation='relu')(x)
561
+ x = Dropout(0.5)(x)
562
+ prediction = Dense(len(le.classes_), activation='softmax')(x)
563
+
564
+ # create a model object
565
+ model = Model(inputs=inception.input, outputs=prediction)
566
+
567
+ # view the structure of the model
568
+ model.summary()
569
+
570
+ # tell the model what cost and optimization method to use
571
+ model.compile(
572
+ loss='sparse_categorical_crossentropy',
573
+ optimizer='adam',
574
+ metrics=['accuracy']
575
+ )
576
+
577
+ """# **Model Feature Extraction**"""
578
+
579
+ callbacks = [
580
+ EarlyStopping(monitor='val_loss', patience=3, restore_best_weights=True, verbose = 1),
581
+ ModelCheckpoint("best_model.h5", save_best_only=True, monitor='val_loss', verbose = 1),
582
+ ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=2, min_lr=1e-5, verbose=1)
583
+ ]
584
+
585
+ history = model.fit(train_ds, validation_data=val_ds, epochs=5, callbacks=callbacks, verbose = 1)
586
+
587
+ """# **Model Fine-Tuning**"""
588
+
589
+ #Fine Tuning
590
+ for layer in inception.layers[-30:]: # Unfreeze last 30 layers (tune as needed)
591
+ layer.trainable = True
592
+
593
+ # tell the model what cost and optimization method to use
594
+ model.compile(
595
+ loss='sparse_categorical_crossentropy',
596
+ optimizer='adam',
597
+ metrics=['accuracy']
598
+ )
599
+
600
+ callbacks = [
601
+ EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True, verbose = 1),
602
+ ModelCheckpoint("best_model.h5", save_best_only=True, monitor='val_loss', verbose = 1),
603
+ ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=7, min_lr=1e-5, verbose=1)
604
+ ]
605
+
606
+ history = model.fit(train_ds, validation_data=val_ds, epochs=10, callbacks=callbacks, verbose = 1)
607
+
608
+ """# **Model Evaluation**"""
609
+
610
+ model.evaluate(test_ds)
611
+
612
+ fig, ax = plt.subplots(1, 2)
613
+ fig.set_size_inches(20, 8)
614
+
615
+ train_acc = history.history['accuracy']
616
+ train_loss = history.history['loss']
617
+ val_acc = history.history['val_accuracy']
618
+ val_loss = history.history['val_loss']
619
+
620
+ epochs = range(1, len(train_acc) + 1)
621
+
622
+ ax[0].plot(epochs, train_acc, 'g-o', label='Training Accuracy')
623
+ ax[0].plot(epochs, val_acc, 'y-o', label='Validation Accuracy')
624
+ ax[0].set_title('Training and Validation Accuracy')
625
+ ax[0].legend(loc = 'lower right')
626
+ ax[0].set_xlabel('Epochs')
627
+ ax[0].set_ylabel('Accuracy')
628
+
629
+ ax[1].plot(epochs, train_loss, 'g-o', label='Training Loss')
630
+ ax[1].plot(epochs, val_loss, 'y-o', label='Validation Loss')
631
+ ax[1].set_title('Training and Validation Loss')
632
+ ax[1].legend()
633
+ ax[1].set_xlabel('Epochs')
634
+ ax[1].set_ylabel('Loss')
635
+ plt.show()
636
+
637
+ true_labels = []
638
+ for _, labels in test_ds:
639
+ true_labels.extend(labels.numpy())
640
+
641
+ # Predict with the model
642
+ pred_probs = model.predict(test_ds)
643
+ pred_labels = np.argmax(pred_probs, axis=1)
644
+
645
+ # Compute confusion matrix
646
+ cm = confusion_matrix(true_labels, pred_labels)
647
+
648
+ # Display
649
+ cm_display = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=le.classes_)
650
+ cm_display.plot(cmap='Blues', values_format='d')
651
+ plt.show()
652
+
653
+ # Evaluate on test dataset
654
+ test_loss, test_accuracy = model.evaluate(test_ds, verbose=1)
655
+ print(f"Test Accuracy: {test_accuracy:.4f}")
656
+
657
+ # Predict probabilities
658
+ y_pred_probs = model.predict(test_ds)
659
+ y_pred = np.argmax(y_pred_probs, axis=1)
660
+
661
+ # True labels (same order as test_ds batching)
662
+ y_true = np.concatenate([y for x, y in test_ds], axis=0)
663
+
664
+ # Metrics
665
+ precision = precision_score(y_true, y_pred, average='macro')
666
+ recall = recall_score(y_true, y_pred, average='macro')
667
+ f1 = f1_score(y_true, y_pred, average='macro')
668
+
669
+ print(f"Precision: {precision:.4f}, Recall: {recall:.4f}, F1-score: {f1:.4f}")
670
+
671
+ # detailed report per class
672
+ print("\nClassification Report:")
673
+ print(classification_report(y_true, y_pred, target_names=le.classes_))
674
+
675
+ # Evaluate model
676
+ test_loss, test_accuracy = model.evaluate(test_ds, verbose=1)
677
+ print(f"Test Accuracy: {test_accuracy:.4f}")
678
+
679
+ # Predictions
680
+ y_probs = model.predict(test_ds) # shape: (num_samples, num_classes)
681
+ y_pred = np.argmax(y_probs, axis=1)
682
+
683
+ # True labels (extract from test_ds)
684
+ y_true = np.concatenate([y for _, y in test_ds], axis=0)
685
+
686
+ # Classification report
687
+ print("\nClassification Report:")
688
+ print(classification_report(y_true, y_pred, target_names=le.classes_))
689
+
690
+ # Confusion matrix
691
+ cm = confusion_matrix(y_true, y_pred)
692
+ plt.figure(figsize=(10,8))
693
+ sns.heatmap(cm, annot=True, fmt="d", cmap="Blues",
694
+ xticklabels=le.classes_, yticklabels=le.classes_)
695
+ plt.xlabel("Predicted")
696
+ plt.ylabel("True")
697
+ plt.title("Confusion Matrix")
698
+ plt.show()
699
+
700
+ # ROC curve (multi-class, one-vs-rest)
701
+ y_true_bin = label_binarize(y_true, classes=np.arange(len(le.classes_))) # binarized true labels
702
+
703
+ plt.figure(figsize=(10,8))
704
+ for i in range(len(le.classes_)):
705
+ fpr, tpr, _ = roc_curve(y_true_bin[:, i], y_probs[:, i])
706
+ plt.plot(fpr, tpr, label=f"{le.classes_[i]}")
707
+ plt.plot([0,1],[0,1],'k--', label='Random')
708
+ plt.xlabel("False Positive Rate")
709
+ plt.ylabel("True Positive Rate")
710
+ plt.title("ROC Curves (One-vs-Rest)")
711
+ plt.legend()
712
+ plt.show()
713
+
714
+ # Predictions
715
+ y_probs = model.predict(test_ds)
716
+ y_pred = np.argmax(y_probs, axis=1)
717
+
718
+ # True labels
719
+ y_true = np.concatenate([y for _, y in test_ds], axis=0)
720
+
721
+ # Metrics per class
722
+ precision, recall, f1, support = precision_recall_fscore_support(
723
+ y_true, y_pred, average=None, labels=np.arange(len(le.classes_))
724
+ )
725
+
726
+ df_metrics = pd.DataFrame({
727
+ 'Class': le.classes_, # use actual class names instead of 0,1,2,3
728
+ 'Precision': precision,
729
+ 'Recall': recall,
730
+ 'F1-score': f1,
731
+ 'Support': support
732
+ })
733
+
734
+ # Sort by F1-score ascending
735
+ df_metrics_sorted = df_metrics.sort_values(by='F1-score')
736
+ print(df_metrics_sorted)
737
+
738
+ # Macro averages
739
+ precision_macro, recall_macro, f1_macro, _ = precision_recall_fscore_support(
740
+ y_true, y_pred, average='macro'
741
+ )
742
+ print(f"\nMacro Avg -> Precision: {precision_macro:.4f}, Recall: {recall_macro:.4f}, F1-score: {f1_macro:.4f}")
743
+
744
+ # Confusion matrix (no annotations, just intensity heatmap)
745
+ cm = confusion_matrix(y_true, y_pred)
746
+
747
+ plt.figure(figsize=(15,12))
748
+ sns.heatmap(cm, annot=False, fmt='d', cmap='Blues',
749
+ xticklabels=le.classes_, yticklabels=le.classes_)
750
+ plt.xlabel("Predicted Class")
751
+ plt.ylabel("True Class")
752
+ plt.title("Confusion Matrix Heatmap")
753
+ plt.show()
754
+
755
+ # Binarize true labels
756
+ y_test_bin = label_binarize(y_true, classes=np.arange(len(le.classes_)))
757
+
758
+ # Predict class probabilities
759
+ y_probs = model.predict(test_ds)
760
+
761
+ # Compute macro-average ROC
762
+ all_fpr = np.linspace(0, 1, 100)
763
+ mean_tpr = 0
764
+
765
+ for i in range(len(le.classes_)):
766
+ fpr, tpr, _ = roc_curve(y_test_bin[:, i], y_probs[:, i])
767
+ mean_tpr += np.interp(all_fpr, fpr, tpr)
768
+
769
+ mean_tpr /= len(le.classes_)
770
+ roc_auc = auc(all_fpr, mean_tpr)
771
+
772
+ # Plot
773
+ plt.figure(figsize=(10,6))
774
+ plt.plot(all_fpr, mean_tpr, color='b',
775
+ label=f'Macro-average ROC (AUC = {roc_auc:.4f})')
776
+ plt.plot([0,1],[0,1],'k--', label='Random')
777
+ plt.xlabel('False Positive Rate')
778
+ plt.ylabel('True Positive Rate')
779
+ plt.title('Macro-average ROC Curve')
780
+ plt.legend()
781
+ plt.show()
782
+
783
+ """# **Saving the Model**"""
784
+
785
+ model.save("Simple_CNN_Classification.h5")
786
+ files.download("Simple_CNN_Classification.h5")
Notebook and Py File/inceptionv3_image_classification.py ADDED
@@ -0,0 +1,594 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """InceptionV3_Image_Classification.ipynb
3
+
4
+ Automatically generated by Colab.
5
+
6
+ Original file is located at
7
+ https://colab.research.google.com/drive/1qSA_Rg-2gDZ0lKcAuLivNsCgrBmJOcfz
8
+
9
+ # **Import Dependencies**
10
+ """
11
+
12
+ import warnings
13
+ warnings.filterwarnings('ignore')
14
+
15
+ import zipfile
16
+ import hashlib
17
+ import matplotlib.pyplot as plt
18
+ import pandas as pd
19
+ import os
20
+ import uuid
21
+ import re
22
+ import random
23
+ import cv2
24
+ import numpy as np
25
+ import tensorflow as tf
26
+ import seaborn as sns
27
+ from google.colab import drive
28
+ from google.colab import files
29
+ from pathlib import Path
30
+ from PIL import Image, ImageStat, UnidentifiedImageError, ImageEnhance
31
+ from matplotlib import patches
32
+ from tqdm import tqdm
33
+ from collections import defaultdict
34
+ from sklearn.preprocessing import LabelEncoder, label_binarize
35
+ from sklearn.model_selection import train_test_split
36
+ from sklearn.utils import resample
37
+ from tensorflow import keras
38
+ from tensorflow.keras.applications.inception_v3 import InceptionV3, preprocess_input
39
+ from tensorflow.keras.utils import to_categorical
40
+ from tensorflow.keras import layers, models, optimizers, callbacks, regularizers
41
+ from tensorflow.keras.models import Sequential
42
+ from tensorflow.keras.layers import Conv2D, BatchNormalization, MaxPooling2D,Dropout, Flatten, Dense, GlobalAveragePooling2D
43
+ from tensorflow.keras.regularizers import l2
44
+ from tensorflow.keras.optimizers import Adam, AdamW
45
+ from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping, ModelCheckpoint
46
+ from tensorflow.keras import Input, Model
47
+ from tensorflow.keras.preprocessing.image import ImageDataGenerator, img_to_array, load_img, array_to_img
48
+ from tensorflow.keras.preprocessing import image
49
+ from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score, roc_curve, precision_score, recall_score, f1_score, precision_recall_fscore_support, auc
50
+
51
+ print(tf.__version__)
52
+
53
+ drive.mount('/content/drive')
54
+ zip_path = '/content/drive/MyDrive/Animals.zip'
55
+ extract_to = '/content/my_data'
56
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
57
+ zip_ref.extractall(extract_to)
58
+
59
+ """# **Convert Dataset to a Data Frame**"""
60
+
61
+ image_extensions = {'.jpg', '.jpeg', '.png'}
62
+ paths = [(path.parts[-2], path.name, str(path)) for path in Path(extract_to).rglob('*.*') if path.suffix.lower() in image_extensions]
63
+
64
+ df = pd.DataFrame(paths, columns = ['class', 'image', 'full_path'])
65
+ df = df.sort_values('class', ascending = True)
66
+ df.reset_index(drop = True, inplace = True)
67
+ df
68
+
69
+ """# **EDA Process**"""
70
+
71
+ class_count = df['class'].value_counts()
72
+ for cls, count in class_count.items():
73
+ print(f'Class: {cls}, Count: {count} images')
74
+
75
+ print(f"\nTotal dataset size is: {len(df)} images")
76
+ print(f"Number of classes: {df['class'].nunique()} classes")
77
+
78
+ plt.figure(figsize = (32, 16))
79
+ class_count.plot(kind = 'bar', color = 'skyblue', edgecolor = 'black')
80
+ plt.title('Number of Images per Class')
81
+ plt.xlabel('Class')
82
+ plt.ylabel('Count')
83
+ plt.xticks(rotation = 45)
84
+ plt.show()
85
+
86
+ plt.figure(figsize = (32, 16))
87
+ class_count.plot(kind = 'pie', autopct = '%1.1f%%', colors = plt.cm.Paired.colors)
88
+ plt.title('Percentage of Images per Class')
89
+ plt.ylabel('')
90
+ plt.show()
91
+
92
+ percentages = (class_count / len(df)) * 100
93
+ imbalance_df = pd.DataFrame({'Count': class_count, 'Percentage %': percentages.round(2)})
94
+ print(imbalance_df)
95
+
96
+ plt.figure(figsize = (32, 16))
97
+ class_count.plot(kind = 'bar', color = 'lightgreen', edgecolor = 'black')
98
+ plt.title('Class Distribution Check')
99
+ plt.xlabel('Class')
100
+ plt.ylabel('Count')
101
+ plt.xticks(rotation = 45)
102
+ plt.axhline(y = class_count.mean(), color = 'red', linestyle = '--', label = 'Average Count')
103
+ plt.legend()
104
+ plt.show()
105
+
106
+ image_sizes = []
107
+
108
+ for file_path in df['full_path']:
109
+ with Image.open(file_path) as img:
110
+ image_sizes.append(img.size)
111
+
112
+ sizes_df = pd.DataFrame(image_sizes, columns=['Width', 'Height'])
113
+
114
+ #Width
115
+ plt.figure(figsize=(8,5))
116
+ plt.scatter(x = range(len(sizes_df)), y = sizes_df['Width'], color='skyblue', s=10)
117
+ plt.title('Image Width Distribution')
118
+ plt.xlabel('Width (pixels)')
119
+ plt.ylabel('Frequency')
120
+ plt.show()
121
+
122
+ #Height
123
+ plt.figure(figsize=(8,5))
124
+ plt.scatter(x = sizes_df['Height'], y = range(len(sizes_df)), color='lightgreen', s=10)
125
+ plt.title('Image Height Distribution')
126
+ plt.xlabel('Height (pixels)')
127
+ plt.ylabel('Frequency')
128
+ plt.show()
129
+
130
+ #For best sure the size of the whole images
131
+ unique_sizes = sizes_df.value_counts().reset_index(name='Count')
132
+ print(unique_sizes)
133
+
134
+ image_data = []
135
+
136
+ for file_path in df['full_path']:
137
+ with Image.open(file_path) as img:
138
+ width, height = img.size
139
+ mode = img.mode # e.g., 'RGB', 'L', 'RGBA', etc.
140
+ channels = len(img.getbands()) # Number of channels
141
+ image_data.append((width, height, mode, channels))
142
+
143
+ # Create DataFrame
144
+ image_df = pd.DataFrame(image_data, columns=['Width', 'Height', 'Mode', 'Channels'])
145
+
146
+ print("Image Mode Distribution:")
147
+ print(image_df['Mode'].value_counts())
148
+
149
+ print("\nNumber of Channels Distribution:")
150
+ print(image_df['Channels'].value_counts())
151
+
152
+ plt.figure(figsize=(6,4))
153
+ image_df['Mode'].value_counts().plot(kind='bar', color='coral')
154
+ plt.title("Image Mode Distribution")
155
+ plt.xlabel("Mode")
156
+ plt.ylabel("Count")
157
+ plt.xticks(rotation=45)
158
+ plt.tight_layout()
159
+ plt.show()
160
+
161
+ plt.figure(figsize=(6,4))
162
+ image_df['Channels'].value_counts().sort_index().plot(kind='bar', color='slateblue')
163
+ plt.title("Number of Channels per Image")
164
+ plt.xlabel("Channels")
165
+ plt.ylabel("Count")
166
+ plt.xticks(rotation=0)
167
+ plt.tight_layout()
168
+ plt.show()
169
+
170
+ sample_df = df.sample(n = 10, random_state = 42)
171
+
172
+ plt.figure(figsize=(32, 16))
173
+
174
+ for i, (cls, img_name, full_path) in enumerate(sample_df.values):
175
+ with Image.open(full_path) as img:
176
+ stat = ImageStat.Stat(img.convert("RGB")) #Convert images to RGB images
177
+ brightness = stat.mean[0]
178
+ contrast = stat.stddev[0]
179
+
180
+ width, height = img.size
181
+ # Print size to console
182
+ print(f"Image: {img_name} | Class: {cls} | Size: {width}x{height} | Brightness: {brightness:.1f} | Contrast: {contrast:.1f}")
183
+
184
+ plt.subplot(2, 5, i + 1)
185
+ plt.imshow(img)
186
+ plt.axis('off')
187
+ plt.title(f"Class: {cls}\nImage: {img_name}\nBrightness: {brightness:.2f}\nContrast: {contrast:.2f} \nSize: {width}x{height}")
188
+
189
+ plt.tight_layout
190
+ plt.show()
191
+
192
+ # Sample 20 random images
193
+ num_samples = 20
194
+ sample_df = df.sample(num_samples, random_state=42)
195
+
196
+ # Get sorted class list and color map
197
+ classes = sorted(df['class'].unique())
198
+ colors = plt.cm.tab10.colors
199
+
200
+ # Grid setup
201
+ cols = 4
202
+ rows = num_samples // cols + int(num_samples % cols > 0)
203
+
204
+ # Figure setup
205
+ plt.figure(figsize=(15, 5 * rows))
206
+
207
+ for idx, (cls, img_name, full_path) in enumerate(sample_df.values):
208
+ with Image.open(full_path) as img:
209
+ ax = plt.subplot(rows, cols, idx + 1)
210
+ ax.imshow(img)
211
+ ax.axis('off')
212
+
213
+ # Title with class info
214
+ ax.set_title(
215
+ f"Class: {cls} \nImage: {img_name} \nSize: {img.width} x {img.height}",
216
+ fontsize=10
217
+ )
218
+
219
+ # Rectangle in axes coords: full width, small height at top
220
+ label_height = 0.1 # 10% of image height
221
+ label_width = 1.0 # full width of the image
222
+
223
+ rect = patches.Rectangle(
224
+ (0, 1 - label_height), label_width, label_height,
225
+ transform=ax.transAxes,
226
+ linewidth=0,
227
+ edgecolor=None,
228
+ facecolor=colors[classes.index(cls) % len(colors)],
229
+ alpha=0.7
230
+ )
231
+ ax.add_patch(rect)
232
+
233
+ # Add class name text centered horizontally
234
+ ax.text(
235
+ 0.5, 1 - label_height / 2,
236
+ cls,
237
+ transform=ax.transAxes,
238
+ fontsize=12,
239
+ color="white",
240
+ fontweight="bold",
241
+ va="center",
242
+ ha="center"
243
+ )
244
+
245
+ # Figure title and layout
246
+ plt.suptitle("Random Dataset Samples - Sanity Check", fontsize=18, fontweight="bold")
247
+ plt.tight_layout(rect=[0, 0, 1, 0.96])
248
+ plt.show()
249
+
250
+ #Check missing files
251
+ print("Missing values per column: ")
252
+ print(df.isnull().sum())
253
+
254
+ #Check duplicate files
255
+ duplicate_names = df.duplicated().sum()
256
+ print(f"\nNumber of duplicate files: {duplicate_names}")
257
+
258
+ duplicate_names = df[df.duplicated(subset = ['image'], keep = False)]
259
+ print(f"Duplicate file names: {len(duplicate_names)}")
260
+
261
+ #Check if two images or more are the same even if they are having different file names
262
+ def get_hash(file_path):
263
+ with open(file_path, 'rb') as f:
264
+ return hashlib.md5(f.read()).hexdigest()
265
+
266
+ df['file_hash'] = df['full_path'].apply(get_hash)
267
+ duplicate_hashes = df[df.duplicated(subset = ['file_hash'], keep = False)]
268
+ print(f"Duplicate image files: {len(duplicate_hashes)}")
269
+
270
+ #This code below just removing the duplicate files, which means will not be feeded to the model, but will be still in the actual directory
271
+ #Important note: duplicates are removed from the dataframe only, not from the actual directory.
272
+ #Drop duplicates based on file_hash, keeping the first one
273
+
274
+ # df_unique = df.drop_duplicates(subset='file_hash', keep='first')
275
+ # print(f"After removing duplicates, unique images: {len(df_unique)}")
276
+
277
+ #Check for images extentions
278
+ df['extenstion'] = df['image'].apply(lambda x: Path(x).suffix.lower())
279
+ print("File type counts: ")
280
+ #print(df['extenstion'].value_counts)
281
+ print(df['extenstion'].value_counts())
282
+
283
+ #Check for resolution relationships
284
+ df['Width'] = sizes_df['Width']
285
+ df['Height'] = sizes_df['Height']
286
+ #print(df.groupby(['Width', 'Height']).size())
287
+ print(df.groupby('class')[['Width', 'Height']].agg(['min', 'max', 'mean']))
288
+
289
+ #Check for class balance (relationship between label and count)
290
+ class_summary = df['class'].value_counts(normalize = False).to_frame('Count')
291
+ #class_summary['Percentage'] = class_summary['Count'] / class_summary['Count'].sum() * 100
292
+ #class_summary
293
+ class_summary['Percentage %'] = round((class_summary['Count'] / len(df)) * 100, 2)
294
+ print(class_summary)
295
+
296
+ """# **Data Cleaning Process**"""
297
+
298
+ corrupted_files = []
299
+
300
+ for file_path in df['full_path']:
301
+ try:
302
+ with Image.open(file_path) as img:
303
+ img.verify()
304
+ except (UnidentifiedImageError, OSError):
305
+ corrupted_files.append(file_path)
306
+
307
+ print(f"Found {len(corrupted_files)} corrupted images.")
308
+ #except (IOError, SyntaxError) as e:
309
+ #corrupted_files.append(file_path)
310
+
311
+ #print(f"Number of corrupted files: {len(corrupted_files)}")
312
+
313
+ if corrupted_files:
314
+ df = df[~df['full_path'].isin(corrupted_files)].reset_index(drop = True)
315
+ print("Corrupted files removed.")
316
+
317
+ #Outliers detection
318
+ #Resolution-based outlier detection
319
+ #width_mean = width_std = sizes_df['Width'].mean(), sizes_df['Width'].std()
320
+ #height_mean = height_std = sizes_df['Height'].mean(), sizes_df['Height'].std()
321
+
322
+ width_mean = sizes_df['Width'].mean()
323
+ width_std = sizes_df['Width'].std()
324
+ height_mean = sizes_df['Height'].mean()
325
+ height_std = sizes_df['Height'].std()
326
+
327
+ outliers = df[(df['Width'] > width_mean + 3 * width_std) | (df['Width'] < width_mean - 3 * width_std) | (df['Height'] > height_mean + 3 * height_std) | (df['Height'] < height_mean - 3 * height_std)]
328
+ #print(f"Number of outliers: {len(outliers)}")
329
+ print(f"Found {len(outliers)} resolution outliers.")
330
+
331
+ df["image"] = df["full_path"].apply(lambda p: Image.open(p).convert('RGB')) #Convert it to RGB for flexibility
332
+
333
+ too_dark = []
334
+ too_bright = []
335
+ blank_or_gray = []
336
+
337
+ # Thresholds
338
+ dark_threshold = 30 # Below this is too dark
339
+ bright_threshold = 225 # Above this is too bright
340
+ low_contrast_threshold = 5 # Low contrast ~ blank/gray
341
+
342
+ for idx, img in enumerate(df["image"]):
343
+ gray = img.convert('L')
344
+ stat = ImageStat.Stat(gray) # Convert to grayscale for brightness/contrast analysis
345
+ brightness = stat.mean[0]
346
+ contrast = stat.stddev[0]
347
+
348
+ if brightness < dark_threshold:
349
+ too_dark.append(idx)
350
+ elif brightness > bright_threshold:
351
+ too_bright.append(idx)
352
+ elif contrast < low_contrast_threshold:
353
+ blank_or_gray.append(idx)
354
+
355
+ print(f"Too dark images: {len(too_dark)}")
356
+ print(f"Too bright images: {len(too_bright)}")
357
+ print(f"Blank/gray images: {len(blank_or_gray)}")
358
+
359
+ # df = df.drop(index=too_bright + blank_or_gray).reset_index(drop=True) --> DROPS too_bright + blank_or_gray TOGETHER!
360
+
361
+ for idx, row in tqdm(df.iterrows(), total=len(df), desc="Enhancing images"):
362
+ img = row["image"]
363
+
364
+ # Enhance too dark images
365
+ if row["full_path"] in df.loc[too_dark, "full_path"].values:
366
+ img = ImageEnhance.Brightness(img).enhance(1.5) # Increase brightness
367
+ img = ImageEnhance.Contrast(img).enhance(1.5) # Increase contrast
368
+
369
+ # Decrease brightness for too bright images
370
+ if row["full_path"] in df.loc[too_bright, "full_path"].values:
371
+ img = ImageEnhance.Brightness(img).enhance(0.7) # Decrease brightness (less than 1)
372
+ img = ImageEnhance.Contrast(img).enhance(1.2) # Optionally, you can also enhance contrast
373
+
374
+ # Overwrite the image back into the DataFrame
375
+ df.at[idx, "image"] = img
376
+
377
+ print(f"Enhanced images in memory: {len(df)}")
378
+
379
+ # Lists to store paths of still too dark and too bright images
380
+ still_dark = []
381
+ still_bright = []
382
+
383
+ # Threshold for "too bright" (already defined as bright_threshold)
384
+ for idx, img in enumerate(df["image"]):
385
+ gray = img.convert('L') # Convert to grayscale for brightness analysis
386
+ stat = ImageStat.Stat(gray)
387
+ brightness = stat.mean[0]
388
+
389
+ # Check if the image is still too dark
390
+ if brightness < dark_threshold:
391
+ still_dark.append(df.loc[idx, 'full_path'])
392
+
393
+ # Check if the image is too bright
394
+ if brightness > bright_threshold:
395
+ still_bright.append(df.loc[idx, 'full_path'])
396
+
397
+ print(f"Still too dark after enhancement: {len(still_dark)} images")
398
+ print(f"Still too bright after enhancement: {len(still_bright)} images")
399
+
400
+ # Point to the extracted dataset, not the zip file location
401
+ dataset_root = "/content/my_data/Animals"
402
+
403
+ # Check mislabeled images
404
+ mismatches = []
405
+ for i, row in df.iterrows():
406
+ folder_name = os.path.basename(os.path.dirname(row["full_path"]))
407
+ if row["class"] != folder_name:
408
+ mismatches.append((row["full_path"], row["class"], folder_name))
409
+
410
+ print(f"Found {len(mismatches)} mislabeled images (class vs folder mismatch).")
411
+
412
+ # Compare classes vs folders
413
+ classes_in_df = set(df["class"].unique())
414
+ folders_in_fs = {f for f in os.listdir(dataset_root) if os.path.isdir(os.path.join(dataset_root, f))}
415
+
416
+ print("Classes in DF but not in folders:", classes_in_df - folders_in_fs)
417
+ print("Folders in FS but not in DF:", folders_in_fs - classes_in_df)
418
+
419
+ def check_file_naming_issues(df):
420
+ issues = {"invalid_chars": [], "spaces": [], "long_paths": [], "case_conflicts": [], "duplicate_names_across_classes": []}
421
+
422
+ seen_names = {}
423
+
424
+ for _, row in df.iterrows():
425
+ fpath = row["full_path"] # full path
426
+ fname = os.path.basename(fpath) # just filename
427
+ cls = row["class"]
428
+
429
+ if re.search(r'[<>:"/\\|?*]', fname): # Windows restricted chars
430
+ issues["invalid_chars"].append(fpath)
431
+
432
+ if " " in fname or fname.startswith(" ") or fname.endswith(" "):
433
+ issues["spaces"].append(fpath)
434
+
435
+ if len(fpath) > 255:
436
+ issues["long_paths"].append(fpath)
437
+
438
+ lower_name = fname.lower()
439
+ if lower_name in seen_names and seen_names[lower_name] != cls:
440
+ issues["case_conflicts"].append((fpath, seen_names[lower_name]))
441
+ else:
442
+ seen_names[lower_name] = cls
443
+
444
+ duplicates = df.groupby(df["full_path"].apply(os.path.basename))["class"].nunique()
445
+ duplicates = duplicates[duplicates > 1].index.tolist()
446
+ for dup in duplicates:
447
+ dup_paths = df[df["full_path"].str.endswith(dup)]["full_path"].tolist()
448
+ issues["duplicate_names_across_classes"].extend(dup_paths)
449
+
450
+ return issues
451
+
452
+ # Run the check
453
+ naming_issues = check_file_naming_issues(df)
454
+
455
+ for issue_type, files in naming_issues.items():
456
+ print(f"\n{issue_type.upper()} ({len(files)})")
457
+ for f in files[:10]: # preview first 10
458
+ print(f)
459
+
460
+ """# **Data Preprocessing Process**"""
461
+
462
+ def preprocess_image(path, target_size=(256, 256), augment=True):
463
+ img = tf.io.read_file(path)
464
+ img = tf.image.decode_image(img, channels=3, expand_animations=False)
465
+ img = tf.image.resize(img, target_size)
466
+ img = tf.cast(img, tf.float32) / 255.0
467
+
468
+ if augment and tf.random.uniform(()) < 0.1: # Only 10% chance
469
+ img = tf.image.random_flip_left_right(img)
470
+ img = tf.image.random_flip_up_down(img)
471
+ img = tf.image.random_brightness(img, max_delta=0.1)
472
+ img = tf.image.random_contrast(img, lower=0.9, upper=1.1)
473
+
474
+ return img
475
+
476
+ le = LabelEncoder()
477
+ df['label'] = le.fit_transform(df['class'])
478
+
479
+ # Prepare paths and labels
480
+ paths = df['full_path'].values
481
+ labels = df['label'].values
482
+
483
+ AUTOTUNE = tf.data.AUTOTUNE
484
+ batch_size = 32
485
+
486
+ # Split data into train+val and test (10% test)
487
+ train_val_paths, test_paths, train_val_labels, test_labels = train_test_split(
488
+ paths, labels, test_size=0.1, random_state=42, stratify=labels
489
+ )
490
+
491
+ # Split train+val into train and val (10% of train_val as val)
492
+ train_paths, val_paths, train_labels, val_labels = train_test_split(
493
+ train_val_paths, train_val_labels, test_size=0.1, random_state=42, stratify=train_val_labels
494
+ )
495
+
496
+ # Create datasets
497
+ def load_and_preprocess(path, label):
498
+ return preprocess_image(path), label
499
+
500
+ train_ds = tf.data.Dataset.from_tensor_slices((train_paths, train_labels))
501
+ train_ds = train_ds.map(lambda x, y: (preprocess_image(x, augment=True), y), num_parallel_calls=AUTOTUNE)
502
+ train_ds = train_ds.shuffle(1024).batch(batch_size).prefetch(AUTOTUNE)
503
+
504
+ val_ds = tf.data.Dataset.from_tensor_slices((val_paths, val_labels))
505
+ val_ds = val_ds.map(load_and_preprocess, num_parallel_calls=AUTOTUNE)
506
+ val_ds = val_ds.batch(batch_size).prefetch(AUTOTUNE)
507
+
508
+ test_ds = tf.data.Dataset.from_tensor_slices((test_paths, test_labels))
509
+ test_ds = test_ds.map(load_and_preprocess, num_parallel_calls=AUTOTUNE)
510
+ test_ds = test_ds.batch(batch_size).prefetch(AUTOTUNE)
511
+
512
+ print("Dataset sizes:")
513
+ print(f"Train: {len(train_paths)} images")
514
+ print(f"Validation: {len(val_paths)} images")
515
+ print(f"Test: {len(test_paths)} images")
516
+ print("--------------------------------------------------")
517
+ print("Train labels sample:", train_labels[:10])
518
+ print("Validation labels sample:", val_labels[:10])
519
+ print("Test labels sample:", test_labels[:10])
520
+
521
+ # Preview normalized image stats and visualization
522
+ for image_batch, label_batch in train_ds.take(1):
523
+ # Print pixel value stats for first image in the batch
524
+ image = image_batch[0]
525
+ label = label_batch[0]
526
+ print("Image dtype:", image.dtype)
527
+ print("Min pixel value:", tf.reduce_min(image).numpy())
528
+ print("Max pixel value:", tf.reduce_max(image).numpy())
529
+ print("Label:", label.numpy())
530
+
531
+ # Show the image
532
+ plt.imshow(image.numpy())
533
+ plt.title(f"Label: {label.numpy()}")
534
+ plt.axis('off')
535
+ plt.show()
536
+ print("---------------------------------------------------")
537
+ print("Number of Classes: ", len(le.classes_))
538
+
539
+ # After train_ds is defined
540
+ for image_batch, label_batch in train_ds.take(1):
541
+ print("Image batch shape:", image_batch.shape) # full batch shape
542
+ print("Label batch shape:", label_batch.shape) # labels shape
543
+
544
+ input_shape = image_batch.shape[1:] # shape of a single image
545
+ print("Single image shape:", input_shape)
546
+ break
547
+
548
+ """# **Model Loading**"""
549
+
550
+ inception = InceptionV3(input_shape=input_shape, weights='imagenet', include_top=False)
551
+
552
+ # don't train existing weights
553
+ for layer in inception.layers:
554
+ layer.trainable = False
555
+
556
+ # Number of classes
557
+ print("Number of Classes: ", len(le.classes_))
558
+
559
+ x = GlobalAveragePooling2D()(inception.output)
560
+ x = Dense(512, activation='relu')(x)
561
+ x = Dropout(0.5)(x)
562
+ prediction = Dense(len(le.classes_), activation='softmax')(x)
563
+
564
+ # create a model object
565
+ model = Model(inputs=inception.input, outputs=prediction)
566
+
567
+ # view the structure of the model
568
+ model.summary()
569
+
570
+ # tell the model what cost and optimization method to use
571
+ model.compile(
572
+ loss='sparse_categorical_crossentropy',
573
+ optimizer='adam',
574
+ metrics=['accuracy']
575
+ )
576
+
577
+ """# **Model Training**"""
578
+
579
+ callbacks = [
580
+ EarlyStopping(monitor='val_loss', patience=3, restore_best_weights=True, verbose = 1),
581
+ ModelCheckpoint("best_model.h5", save_best_only=True, monitor='val_loss', verbose = 1),
582
+ ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=10, min_lr=1e-5, verbose=1)
583
+ ]
584
+
585
+ history = model.fit(train_ds, validation_data=val_ds, epochs=10, callbacks=callbacks, verbose = 1)
586
+
587
+ """# **Model Evaluation**"""
588
+
589
+ model.evaluate(test_ds)
590
+
591
+ """# **Saving the Model**"""
592
+
593
+ model.save("Simple_CNN_Classification.h5")
594
+ files.download("Simple_CNN_Classification.h5")
Results/Animal Classifier.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:80024a023ddb481729097772fa3003494e59ed4984fc7d8d5eb8a734cb43077b
3
+ size 8566790
app/__pycache__/main.cpython-313.pyc ADDED
Binary file (1.58 kB). View file
 
app/__pycache__/model.cpython-313.pyc ADDED
Binary file (1.93 kB). View file
 
app/main.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File
2
+ from fastapi.responses import JSONResponse
3
+ from app.model import predict
4
+ from PIL import Image
5
+ import io
6
+
7
+ app = FastAPI(title="Animal Image Classifier")
8
+
9
+ @app.post("/predict")
10
+ async def predict_image(file: UploadFile = File(...)):
11
+ try:
12
+ # Read image from uploaded file
13
+ contents = await file.read()
14
+ img = Image.open(io.BytesIO(contents))
15
+
16
+ # Run prediction
17
+ label, confidence, probs = predict(img)
18
+
19
+ return JSONResponse(content={
20
+ "predicted_label": label,
21
+ "confidence": round(confidence, 3),
22
+ "probabilities": {k: round(v, 3) for k, v in probs.items()}
23
+ })
24
+
25
+ except Exception as e:
26
+ return JSONResponse(content={"error": str(e)}, status_code=500)
app/model.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tensorflow as tf
2
+ import numpy as np
3
+ from PIL import Image
4
+
5
+ # Load your trained CNN model
6
+ model = tf.keras.models.load_model("saved_model/Inception_V3_Animals_Classification.h5")
7
+
8
+ # Same label order you used when training (from LabelEncoder)
9
+ CLASS_NAMES = ["Cat", "Dog", "Snake"]
10
+
11
+ def preprocess_image(img: Image.Image, target_size=(256, 256)):
12
+ """
13
+ Preprocess a PIL image to match training pipeline:
14
+ - Convert to RGB
15
+ - Resize
16
+ - Convert to float32
17
+ - Normalize to [0,1]
18
+ - Add batch dimension
19
+ """
20
+ img = img.convert("RGB") # ensure 3 channels
21
+ img = img.resize(target_size)
22
+ img = np.array(img).astype("float32") / 255.0 # normalize
23
+ img = np.expand_dims(img, axis=0) # (1, 256, 256, 3)
24
+ return img
25
+
26
+ def predict(img: Image.Image):
27
+ # Apply preprocessing
28
+ input_tensor = preprocess_image(img)
29
+
30
+ # Model prediction
31
+ preds = model.predict(input_tensor)
32
+ probs = preds[0]
33
+
34
+ class_idx = int(np.argmax(probs))
35
+ confidence = float(np.max(probs))
36
+
37
+ # Map all probabilities
38
+ prob_dict = {CLASS_NAMES[i]: float(probs[i]) for i in range(len(CLASS_NAMES))}
39
+
40
+ return CLASS_NAMES[class_idx], confidence, prob_dict