From cb065f907da19490fb2c59985cc48def996e234b Mon Sep 17 00:00:00 2001
From: Maciej Wielgosz <maciej.wielgosz@nibio.no>
Date: Tue, 2 Jan 2024 14:46:15 +0100
Subject: [PATCH] first version of the simple model implemented

---
 .gitignore                               |   2 +
 README.md                                |  67 ++++++++
 notebooks/train_model.ipynb              | 207 +++++++++++++++++++++++
 pipeline/data_loader.py                  |   2 +
 prepare_data/__init__.py                 |   0
 prepare_data/clean_file_names.py         |  39 +++++
 prepare_data/preparare_train_val_test.py | 143 ++++++++++++++++
 7 files changed, 460 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 notebooks/train_model.ipynb
 create mode 100644 pipeline/data_loader.py
 create mode 100644 prepare_data/__init__.py
 create mode 100644 prepare_data/clean_file_names.py
 create mode 100644 prepare_data/preparare_train_val_test.py

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f1955c4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/simple-needles-2-class
+/data
diff --git a/README.md b/README.md
index 65bd051..e00c2b2 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,73 @@
 
 The original dataset is from [here](https://zenodo.org/records/4446842).
 
+The local NIBIO copy of the dataset is [here](https://nibio-my.sharepoint.com/:f:/g/personal/maciej_wielgosz_nibio_no/Elq2J4unUjFOtVKnFwDvgecBnTRLdBm_F5H__1V1gs_upQ?e=XFoOl5)
+
+
+# Austrian Tree Dataset Overview
+
+Collected by: **Austrian Federal Forests AG**  
+Collection Period: **Autumn 2009 - Spring 2010**  
+Usage: **Non-commercial research only**
+
+## Publication Reference
+Fiel, S. & Sablatnig, R. (2010): Leaf classification using local features. In: Proc. of 34th annual Workshop of the Austrian Association for Pattern Recognition (AAPR), 2010, 69-74 pdf.
+
+## Dataset Contents
+
+### 1. Leaves of Broad Leaf Trees
+- **Total Images:** 134 
+- **Types:** 
+  - Ash (25 images)
+  - Beech (30 images)
+  - Hornbeam (34 images)
+  - Mountain Oak (22 images)
+  - Sycamore Maple (23 images)
+- **Details:** 
+  - Image Scale: 800 pixels height or 600 pixels width
+  - Note: Ash leaves are compound, specifically pinnate
+
+### 2. Bark of Trees
+- **Total Images:** 1183 
+- **Types:** 
+  - Ash (34 images)
+  - Beech (16 images)
+  - Black Pine (166 images, divided into 3 age-based sub-classes)
+  - Fir (127 images, divided into 3 age-based sub-classes)
+  - Hornbeam (42 images)
+  - Larch (200 images, divided into 3 age-based sub-classes)
+  - Mountain Oak (77 images)
+  - Scots Pine (190 images, divided into 3 age-based sub-classes)
+  - Spruce (213 images, divided into 3 age-based sub-classes)
+  - Swiss Stone Pine (96 images)
+  - Sycamore Maple (22 images)
+- **Details:** 
+  - Image Scale: 800 pixels height or 600 pixels width
+  - Age Categories: 
+    - Less than 60 years
+    - 60 to 80 years
+    - More than 80 years
+
+### 3. Needles of Conifers
+- **Total Images:** 275 
+- **Types:** 
+  - Black Pine (107 images)
+  - Fir (10 images)
+  - Larch (114 images)
+  - Scots Pine (10 images)
+  - Spruce (13 images)
+  - Swiss Stone Pine (21 images)
+- **Details:** 
+  - Needle Classes: 
+    - Separate growth (Fir, Spruce)
+    - Cluster growth (others)
+  - Lighting: 
+    - Perfect conditions (Fir, Scots Pine, Spruce)
+    - Natural conditions (others)
+
+
+
+
 
 ## Getting started
 
diff --git a/notebooks/train_model.ipynb b/notebooks/train_model.ipynb
new file mode 100644
index 0000000..f4ccb18
--- /dev/null
+++ b/notebooks/train_model.ipynb
@@ -0,0 +1,207 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from torchvision import transforms, datasets\n",
+    "from torch.utils.data import DataLoader\n",
+    "\n",
+    "# Define a transform to apply to each image\n",
+    "transform = transforms.Compose([\n",
+    "    transforms.Resize((256, 256)),  # Resize each image to 256x256\n",
+    "    transforms.ToTensor(),  # Convert image to a PyTorch tensor\n",
+    "    transforms.Normalize(mean=[0.485, 0.456, 0.406],  # Normalize for pre-trained models\n",
+    "                         std=[0.229, 0.224, 0.225])\n",
+    "])\n",
+    "\n",
+    "data_path = \"/home/nibio/mutable-outside-world/code/ml-department-workshop/data\"\n",
+    "\n",
+    "\n",
+    "# Create a dataset for each set: train, validation, and test\n",
+    "train_dataset = datasets.ImageFolder(root=data_path + '/train', transform=transform)\n",
+    "val_dataset = datasets.ImageFolder(root=data_path + '/val', transform=transform)\n",
+    "test_dataset = datasets.ImageFolder(root=data_path + '/test', transform=transform)\n",
+    "\n",
+    "# Create a DataLoader for each set\n",
+    "train_loader = DataLoader(train_dataset, batch_size=4, shuffle=True)\n",
+    "val_loader = DataLoader(val_dataset, batch_size=4, shuffle=False)\n",
+    "test_loader = DataLoader(test_dataset, batch_size=4, shuffle=False)\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import matplotlib.pyplot as plt\n",
+    "import numpy as np\n",
+    "import torchvision\n",
+    "from torchvision import transforms, datasets\n",
+    "from torch.utils.data import DataLoader\n",
+    "\n",
+    "# Assuming 'train_dataset' is already defined and loaded as in previous examples\n",
+    "# Make sure you have the 'train_dataset.class_to_idx' attribute available\n",
+    "# which is automatically created when using datasets.ImageFolder\n",
+    "\n",
+    "# Function to show an image with labels\n",
+    "def imshow(img, labels):\n",
+    "    img = img / 2 + 0.5  # unnormalize\n",
+    "    npimg = img.numpy()\n",
+    "    plt.imshow(np.transpose(npimg, (1, 2, 0)))\n",
+    "    # Display labels below the image\n",
+    "    plt.xticks([])  # Remove x-axis ticks\n",
+    "    plt.yticks([])  # Remove y-axis ticks\n",
+    "    plt.xlabel(' - '.join('%5s' % train_dataset.classes[label] for label in labels), fontsize=10)\n",
+    "    plt.show()\n",
+    "\n",
+    "# Define transformations\n",
+    "transform = transforms.Compose([\n",
+    "    transforms.Resize((256, 256)),\n",
+    "    transforms.ToTensor(),\n",
+    "    transforms.Normalize((0.5,), (0.5,))\n",
+    "])\n",
+    "\n",
+    "# Create the train_dataset and train_loader as before\n",
+    "train_dataset = datasets.ImageFolder(root=data_path + '/train', transform=transform)\n",
+    "train_loader = DataLoader(train_dataset, batch_size=4, shuffle=True)\n",
+    "\n",
+    "# Get some random training images\n",
+    "dataiter = iter(train_loader)\n",
+    "images, labels = next(dataiter)\n",
+    "\n",
+    "# Show images with labels\n",
+    "imshow(torchvision.utils.make_grid(images), labels)\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# create a simple CNN model\n",
+    "import torch\n",
+    "import torch.nn as nn\n",
+    "import torch.nn.functional as F\n",
+    "import torch.optim as optim\n",
+    "\n",
+    "\n",
+    "class SimpleCNN(nn.Module):\n",
+    "    def __init__(self):\n",
+    "        super(SimpleCNN, self).__init__()\n",
+    "        self.conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, padding=1)\n",
+    "        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)\n",
+    "        self.conv2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=3, padding=1)\n",
+    "        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)\n",
+    "        self.conv3 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, padding=1)\n",
+    "        self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2)\n",
+    "        self.fc1 = nn.Linear(in_features=64 * 32 * 32, out_features=500)\n",
+    "        self.fc2 = nn.Linear(in_features=500, out_features=2)\n",
+    "\n",
+    "    def forward(self, x):\n",
+    "        x = self.pool1(F.relu(self.conv1(x)))  # 16 x 128 x 128\n",
+    "        x = self.pool2(F.relu(self.conv2(x)))  # 32 x 64 x 64\n",
+    "        x = self.pool3(F.relu(self.conv3(x)))  # 64 x 32 x 32\n",
+    "        x = x.view(-1, 64 * 32 * 32)  # Flatten\n",
+    "        x = F.relu(self.fc1(x))\n",
+    "        x = self.fc2(x)\n",
+    "        return x\n",
+    "    \n",
+    "# train the model\n",
+    "\n",
+    "\n",
+    "# Create an instance of the model\n",
+    "model = SimpleCNN()\n",
+    "\n",
+    "# Define the loss function and optimizer\n",
+    "criterion = nn.CrossEntropyLoss()\n",
+    "\n",
+    "# Use Adam optimizer\n",
+    "optimizer = optim.Adam(model.parameters(), lr=0.001)\n",
+    "\n",
+    "# Train the model\n",
+    "num_epochs = 5\n",
+    "for epoch in range(num_epochs):\n",
+    "    running_loss = 0.0\n",
+    "    for i, data in enumerate(train_loader):\n",
+    "        # Get the inputs\n",
+    "        inputs, labels = data\n",
+    "\n",
+    "        # Zero the parameter gradients\n",
+    "        optimizer.zero_grad()\n",
+    "\n",
+    "        # Forward + backward + optimize\n",
+    "        outputs = model(inputs)\n",
+    "        loss = criterion(outputs, labels)\n",
+    "        loss.backward()\n",
+    "        optimizer.step()\n",
+    "\n",
+    "        # Print statistics\n",
+    "        print('[%d, %5d] loss: %.3f' % (epoch + 1, i + 1, loss))\n",
+    "\n",
+    "\n",
+    "# save the model\n",
+    "torch.save(model.state_dict(), 'simple_cnn.pth')\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 20,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Accuracy of the network on the test images: 77 %\n"
+     ]
+    }
+   ],
+   "source": [
+    "# load the model\n",
+    "model = SimpleCNN()\n",
+    "model.load_state_dict(torch.load('simple_cnn.pth'))\n",
+    "\n",
+    "# run the model on the test set and print the accuracy\n",
+    "correct = 0\n",
+    "total = 0\n",
+    "\n",
+    "with torch.no_grad():\n",
+    "    for data in test_loader:\n",
+    "        images, labels = data\n",
+    "        outputs = model(images)\n",
+    "        _, predicted = torch.max(outputs.data, dim=1)\n",
+    "        total += labels.size(0)\n",
+    "        correct += (predicted == labels).sum().item()\n",
+    "\n",
+    "print('Accuracy of the network on the test images: %d %%' % (100 * correct / total))"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.10"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/pipeline/data_loader.py b/pipeline/data_loader.py
new file mode 100644
index 0000000..5bc70a2
--- /dev/null
+++ b/pipeline/data_loader.py
@@ -0,0 +1,2 @@
+import torch
+# import dataset from torch 
diff --git a/prepare_data/__init__.py b/prepare_data/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/prepare_data/clean_file_names.py b/prepare_data/clean_file_names.py
new file mode 100644
index 0000000..4209668
--- /dev/null
+++ b/prepare_data/clean_file_names.py
@@ -0,0 +1,39 @@
+import sys
+import os
+import re
+
+
+def clean_file_names(path):
+    """
+    Clean file names in a directory. This function will replace all spaces with underscores,
+    replace all dashes with underscores, and change all file names to lowercase. If there are
+    numbers in brackets, they will be replaced with an underscore and the number.
+
+    Parameters
+    ----------
+    path : str
+        Path to directory containing files to be renamed.
+
+    Returns
+    -------
+    None.
+
+    """
+
+    for filename in os.listdir(path):
+        if filename.lower().endswith((".png", ".jpg")):
+            # replace all spaces with underscores
+            new_filename = re.sub(r"\s+", "_", filename)
+            # replace all dashes with underscores
+            new_filename = re.sub(r"-", "_", new_filename)
+            # if there are numbers in bruckets, change to underscore number
+            new_filename = re.sub(r"\(\d+\)", lambda x: "_" + x.group()[1:-1], new_filename)
+            print(new_filename)
+            # rename file to new filename and change to lowercase
+            os.rename(
+                os.path.join(path, filename), os.path.join(path, new_filename.lower())
+            )
+
+if __name__ == "__main__":
+
+    clean_file_names(sys.argv[1])
\ No newline at end of file
diff --git a/prepare_data/preparare_train_val_test.py b/prepare_data/preparare_train_val_test.py
new file mode 100644
index 0000000..19241f3
--- /dev/null
+++ b/prepare_data/preparare_train_val_test.py
@@ -0,0 +1,143 @@
+import os
+import shutil
+import numpy as np
+
+
+class PrepareTrainValTest:
+    def __init__(self, 
+                 data_in_path, 
+                 data_out_path,
+                 train_size=0.7, 
+                 val_size=0.15, 
+                 test_size=0.15, 
+                 verbose=False
+                 ):
+        
+        self.data_in_path = data_in_path
+        self.data_out_path = data_out_path
+        self.train_size = train_size
+        self.val_size = val_size
+        self.test_size = test_size
+        self.verbose = verbose
+
+    def prepare_train_val_test(self):
+        """
+        Prepare train, validation, and test data sets from raw data. This function will
+        create a directory structure that looks like this:
+        
+        data
+        ├── test
+        │   ├── class_1
+        │   ├── class_2
+        │   ├── class_3
+        │   └── class_4
+        ├── train
+        │   ├── class_1
+        │   ├── class_2
+        │   ├── class_3
+        │   └── class_4
+        └── val
+            ├── class_1
+            ├── class_2
+            ├── class_3
+            └── class_4
+
+        Parameters
+        ----------
+        None.
+
+        Returns
+        -------
+        None.
+
+        """
+        
+        # get list of all classes
+        classes = os.listdir(self.data_in_path)
+        
+        # create train, val, and test directories
+        for directory in ["train", "val", "test"]:
+            os.makedirs(os.path.join(self.data_out_path, directory), exist_ok=True)
+            for class_name in classes:
+                os.makedirs(
+                    os.path.join(self.data_out_path, directory, class_name), exist_ok=True
+                )
+        
+        # loop through classes
+        for class_name in classes:
+            # get list of all files in class directory
+            files = os.listdir(os.path.join(self.data_in_path, class_name))
+            # shuffle files
+            np.random.shuffle(files)
+            # get number of files in class directory
+            num_files = len(files)
+            # get number of files for each data set
+            num_train = int(num_files * self.train_size)
+            num_val = int(num_files * self.val_size)
+            num_test = int(num_files * self.test_size)
+            # loop through files
+            for i, file in enumerate(files):
+                # copy file to train directory
+                if i < num_train:
+                    shutil.copy(
+                        os.path.join(self.data_in_path, class_name, file),
+                        os.path.join(self.data_out_path, "train", class_name, file),
+                    )
+                # copy file to val directory
+                elif i < num_train + num_val:
+                    shutil.copy(
+                        os.path.join(self.data_in_path, class_name, file),
+                        os.path.join(self.data_out_path, "val", class_name, file),
+                    )
+                # copy file to test directory
+                else:
+                    shutil.copy(
+                        os.path.join(self.data_in_path, class_name, file),
+                        os.path.join(self.data_out_path, "test", class_name, file),
+                    )   
+
+
+if __name__ == "__main__":
+    # use argparse to get command line arguments
+    import argparse
+
+    parser = argparse.ArgumentParser(
+        description="Prepare train, validation, and test data sets from raw data."
+    )
+    parser.add_argument(
+        '-i',
+        "--data_in_path",
+        type=str,
+        required=True,
+        help="Path to directory containing raw data."
+    )
+    parser.add_argument(
+        '-o',
+        "--data_out_path",
+        type=str,
+        required=True,
+        help="Path to directory to save train, validation, and test data."
+    )
+    parser.add_argument(
+        "--verbose",
+        type=bool,
+        default=False,
+        help="Enable verbose mode."
+    )
+    args = parser.parse_args()
+
+    # create instance of PrepareTrainValTest class
+
+    prepare_train_val_test = PrepareTrainValTest(
+        data_in_path=args.data_in_path,
+        data_out_path=args.data_out_path,
+        verbose=args.verbose
+    )
+
+    # prepare train, validation, and test data sets
+    prepare_train_val_test.prepare_train_val_test()
+   
+
+
+
+     
\ No newline at end of file
-- 
GitLab