File size: 13,637 Bytes
7b4e127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# -*- coding: utf-8 -*-
"""classification.ipynb

Automatically generated by Colab.

Original file is located at
    https://colab.research.google.com/drive/1JuZNV3fqC5XQ0L-jhIyVRbIDPfWWGkVI
"""

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, models, transforms
from torch.utils.data import DataLoader
from torch.utils.data import DataLoader, random_split
import os
import matplotlib.pyplot as plt
import random
from PIL import Image
import numpy as np
import pandas as pd

# Define the data directories
data_dir = 'drive/MyDrive/Ai_Hackathon_2024/plant_data/data_for_training'
augmented_data_dir = 'drive/MyDrive/Ai_Hackathon_2024/plant_data/augmented_data'

# Define the desired number of images per class
N = 50

# Define the augmentation transforms
augmentation_transforms = transforms.Compose([
    transforms.RandomHorizontalFlip(),
    transforms.RandomRotation(30),
    transforms.RandomAffine(degrees=0, translate=(0.1, 0.1)),
    transforms.RandomResizedCrop(size=(224, 224), scale=(0.8, 1.0)),
    transforms.Pad(padding=10, padding_mode='reflect'),  # Add padding with reflection
    transforms.ToTensor(),
])

# Load the dataset
print('loading dataset...')
dataset = datasets.ImageFolder(data_dir)
class_names = dataset.classes

print('loaded dataset.')

# Function to save augmented images
def save_image(img, path, idx):
    img.save(os.path.join(path, f'{idx}.png'))

# Augment the dataset
if not os.path.exists(augmented_data_dir):
    os.makedirs(augmented_data_dir)

print('starting augmentation process...')
for class_idx in range(len(dataset.classes)):
    print(f"class_idx = {class_idx}")
    class_dir = os.path.join(augmented_data_dir, dataset.classes[class_idx])
    if not os.path.exists(class_dir):
        os.makedirs(class_dir)

    class_images = [img_path for img_path, label in dataset.samples if label == class_idx]
    current_count = 0

    # Save original images first
    for img_path in class_images:
        img = Image.open(img_path)
        save_image(img, class_dir, current_count)
        current_count += 1

    # If there are fewer than N images, augment the dataset
    while current_count < N:
        img_path = random.choice(class_images)
        img = Image.open(img_path)
        img = augmentation_transforms(img)
        img = transforms.ToPILImage()(img)  # Convert back to PIL Image
        save_image(img, class_dir, current_count)
        current_count += 1

print('Data augmentation completed.')

# Define the data directory
data_dir = augmented_data_dir #'drive/MyDrive/Ai_Hackathon_2024/plant_data/data_for_training'


# Set the random seed for reproducibility
seed = 42
torch.manual_seed(seed)

# Define transforms
data_transforms = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.RandomHorizontalFlip(),
    transforms.RandomRotation(30),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

# Create the dataset
full_dataset = datasets.ImageFolder(data_dir, transform=data_transforms)

# Define the train-validation split ratio
train_size = int(0.8 * len(full_dataset))
val_size = len(full_dataset) - train_size

# Split the dataset
train_dataset, val_dataset = random_split(full_dataset, [train_size, val_size], generator=torch.Generator().manual_seed(seed))

# Create data loaders
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)

# Load the pre-trained ResNet50 model
resnet50 = models.resnet50(weights='ResNet50_Weights.DEFAULT')

# Freeze the parameters of the pre-trained model
for param in resnet50.parameters():
    param.requires_grad = False

# Remove the final fully connected layer
num_ftrs = resnet50.fc.in_features
resnet50.fc = nn.Identity()  # Replace the final layer with an identity function to get the feature vectors

# Define a custom neural network with one hidden layer and an output layer
class CustomNet(nn.Module):
    def __init__(self, num_ftrs, num_classes):
        super(CustomNet, self).__init__()
        self.resnet50 = resnet50
        self.hidden = nn.Linear(num_ftrs, 512)
        self.relu = nn.ReLU()
        self.output = nn.Linear(512, num_classes)

    def forward(self, x):
        x = self.resnet50(x)  # Extract features using the pre-trained model
        x = self.hidden(x)  # Pass through the hidden layer
        x = self.relu(x)  # Apply ReLU activation
        x = self.output(x)  # Output layer
        return x

# Instantiate the custom network
num_classes = len(full_dataset.classes)
model = CustomNet(num_ftrs, num_classes)

# Move the model to the appropriate device
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = model.to(device)

# Define criterion and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

def train_model(model, dataloaders, criterion, optimizer, num_epochs=10):
    best_model_wts = model.state_dict()
    best_acc = 0.0

    train_losses = []
    val_losses = []

    for epoch in range(num_epochs):
        print(f'Epoch {epoch}/{num_epochs - 1}')
        print('-' * 10)

        # Each epoch has a training and validation phase
        for phase in ['train', 'val']:
            if phase == 'train':
                model.train()
            else:
                model.eval()

            running_loss = 0.0
            running_corrects = 0

            for inputs, labels in dataloaders[phase]:
                inputs, labels = inputs.to(device), labels.to(device)

                # Zero the parameter gradients
                optimizer.zero_grad()

                # Forward
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs)
                    _, preds = torch.max(outputs, 1)
                    loss = criterion(outputs, labels)

                    # Backward + optimize only if in training phase
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()

                running_loss += loss.item() * inputs.size(0)
                running_corrects += torch.sum(preds == labels.data)

            epoch_loss = running_loss / len(dataloaders[phase].dataset)
            epoch_acc = running_corrects.double() / len(dataloaders[phase].dataset)

            print(f'{phase} Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}')

            if phase == 'train':
                train_losses.append(epoch_loss)
            else:
                val_losses.append(epoch_loss)

            # Deep copy the model
            if phase == 'val' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = model.state_dict()

    print('Best val Acc: {:4f}'.format(best_acc))

    # Load best model weights
    model.load_state_dict(best_model_wts)

    # Plot the training and validation loss
    plt.figure(figsize=(10, 5))
    plt.plot(train_losses, label='Training Loss')
    plt.plot(val_losses, label='Validation Loss')
    plt.xlabel('Epochs')
    plt.ylabel('Loss')
    plt.legend()
    plt.show()

    return model

# Create a dictionary to hold the dataloaders
dataloaders = {'train': train_loader, 'val': val_loader}

# Train and evaluate the model
model = train_model(model, dataloaders, criterion, optimizer, num_epochs=10)

# Save the model
torch.save(model.state_dict(), 'drive/MyDrive/Ai_Hackathon_2024/plant_data/fine_tuned_plant_classifier.pth')

# Function to evaluate the model
def evaluate_model(model, dataloader):
    model.eval()
    correct = 0
    total = 0

    all_preds = []
    all_labels = []

    with torch.no_grad():
        for inputs, labels in dataloader:
            inputs, labels = inputs.to(device), labels.to(device)
            outputs = model(inputs)
            _, preds = torch.max(outputs, 1)

            all_preds.extend(preds.cpu().numpy())
            all_labels.extend(labels.cpu().numpy())

            correct += (preds == labels).sum().item()
            total += labels.size(0)

    accuracy = correct / total
    return accuracy, all_preds, all_labels

# Evaluate the model
dataloader = DataLoader(full_dataset, batch_size=32, shuffle=True)
accuracy, all_preds, all_labels = evaluate_model(model, dataloader)

# Calculate the number of correct and incorrect predictions
correct_preds = sum(np.array(all_preds) == np.array(all_labels))
incorrect_preds = len(all_labels) - correct_preds

print(f'Total images: {len(all_labels)}')
print(f'Correct predictions: {correct_preds}')
print(f'Incorrect predictions: {incorrect_preds}')
print(f'Accuracy: {accuracy:.4f}')

##-----------------------------------------------------------##
real_dataset = datasets.ImageFolder('drive/MyDrive/Ai_Hackathon_2024/plant_data/data_for_training', transform=data_transforms)

# Evaluate the model
dataloader = DataLoader(real_dataset, batch_size=32, shuffle=True)
accuracy, all_preds, all_labels = evaluate_model(model, dataloader)

# Calculate the number of correct and incorrect predictions
correct_preds = sum(np.array(all_preds) == np.array(all_labels))
incorrect_preds = len(all_labels) - correct_preds
print('-'*10)
print(f'Total images: {len(all_labels)}')
print(f'Correct predictions: {correct_preds}')
print(f'Incorrect predictions: {incorrect_preds}')
print(f'Accuracy: {accuracy:.4f}')

# Function to load and preprocess the image
def process_image(image_path):
    data_transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])
    image = Image.open(image_path).convert('RGB')
    image = data_transform(image)# data_transforms(image) # <-- data transforms uses all the random cropping as well
    image = image.unsqueeze(0)  # Add batch dimension
    return image

#----------------------------INFERENCE PART----------------------------

# Function to predict the class of a single image
def predict_single_image(image_path, model):
    # Load the image and preprocess it
    image = process_image(image_path)

    # Load the model
    model.eval()
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    model = model.to(device)

    # Pass the image through the model
    with torch.no_grad():
        image = image.to(device)
        outputs = model(image)
        probabilities = torch.nn.functional.softmax(outputs[0], dim=0)

    # Return the class names and their probabilities as a Pandas Series
    return pd.Series(probabilities.cpu().numpy(), index=class_names).sort_values(ascending=False)

def classify(img_path):
    # Path to the single image
    image_path = img_path

    # Initialize your custom model
    model = CustomNet(num_ftrs, num_classes)
    # Load the trained model weights
    model.load_state_dict(torch.load('./fine_tuned:plant_classifier.pth'))

    # Predict the class probabilities
    class_probabilities = predict_single_image(image_path, model)
    return class_probabilities


#----------------------------INFERENCE PART----------------------------


## script to automatically include larger drone images

import os
import shutil
from PIL import Image

# Define the paths
source_dir = 'path/to/source_images'  # The directory with new images
target_base_dir = 'path/to/training_images'  # The base directory containing original class folders
new_base_dir = 'path/to/training_images_2'  # The base directory for the new substructure

# Extract the class folders
class_folders = [d for d in os.listdir(target_base_dir) if os.path.isdir(os.path.join(target_base_dir, d))]

# Function to extract ID from a filename
def extract_id(filename):
    return filename.split('_')[0]  # Assumes ID is the first part of the filename separated by '_'

# Function to crop the middle section of an image
def crop_middle_section(image):
    width, height = image.size
    new_width = width // 3
    new_height = height // 3
    left = (width - new_width) // 2
    top = (height - new_height) // 2
    right = left + new_width
    bottom = top + new_height
    return image.crop((left, top, right, bottom))

# Create the new base directory if it does not exist
os.makedirs(new_base_dir, exist_ok=True)

# Create a dictionary to map IDs to their respective class folders
id_to_class_folder = {}
for class_folder in class_folders:
    class_folder_path = os.path.join(target_base_dir, class_folder)
    for filename in os.listdir(class_folder_path):
        if os.path.isfile(os.path.join(class_folder_path, filename)):
            file_id = extract_id(filename)
            id_to_class_folder[file_id] = class_folder

# Copy and manipulate the matching images
for filename in os.listdir(source_dir):
    if os.path.isfile(os.path.join(source_dir, filename)):
        file_id = extract_id(filename)
        if file_id in id_to_class_folder:
            target_class_folder = id_to_class_folder[file_id]
            new_class_folder_path = os.path.join(new_base_dir, target_class_folder)
            os.makedirs(new_class_folder_path, exist_ok=True)  # Create the class folder if it doesn't exist

            target_path = os.path.join(new_class_folder_path, filename)

            # Open and manipulate the image
            image_path = os.path.join(source_dir, filename)
            with Image.open(image_path) as img:
                cropped_img = crop_middle_section(img)
                cropped_img.save(target_path)

            print(f'Copied and cropped {filename} to {new_class_folder_path}')

print('Image processing and copying completed.')