{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Spotting Coral Bleaching\n",
    "\n",
    "coral_bleaching.py\n",
    "\n",
    "AI project on Computer Vision: telling healthy coral from bleached coral in\n",
    "underwater photographs, so that bleaching can be spotted early.\n",
    "\n",
    "CBSE Class X, Artificial Intelligence (417), Part C project work.\n",
    "SDG 14 (Life below Water) and SDG 13 (Climate Action).\n",
    "\n",
    "A computer sees a photo as a grid of pixels, and each pixel as three\n",
    "numbers from 0 to 255 - how much red, green and blue it holds. The program:\n",
    "  1. reads every photo into those numbers,\n",
    "  2. finds which pixels are coral and which are water (segmentation),\n",
    "  3. measures two FEATURES of the coral: how bright it is, and how\n",
    "     colourful it is, and\n",
    "  4. learns the average healthy coral and the average bleached coral from\n",
    "     the training photos, and labels a new photo by whichever it is closer to.\n",
    "\n",
    "Libraries: numpy (the pixel arithmetic), matplotlib (reading and showing\n",
    "images, and the charts), pandas (the table of features), os (the folders)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Stage 1 - Problem scoping"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"=\" * 60)\n",
    "print(\"SPOTTING CORAL BLEACHING\")\n",
    "print(\"=\" * 60)\n",
    "print(\"Who   : marine scientists and reef divers\")\n",
    "print(\"What  : thousands of reef photos, too many to check by eye\")\n",
    "print(\"Where : coral reefs, such as those around Lakshadweep\")\n",
    "print(\"Why   : bleaching caught early shows where reefs need protection\")\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Stage 2 - Data acquisition"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "pd.set_option(\"display.width\", 120)\n",
    "\n",
    "classes = [\"healthy\", \"bleached\"]\n",
    "paths = []\n",
    "labels = []\n",
    "for c in classes:\n",
    "    for name in sorted(os.listdir(os.path.join(\"images\", c))):\n",
    "        paths.append(os.path.join(\"images\", c, name))\n",
    "        labels.append(c)\n",
    "\n",
    "print(\"--- Stage 2: Data acquisition ---\")\n",
    "print(\"Healthy photos  :\", labels.count(\"healthy\"))\n",
    "print(\"Bleached photos :\", labels.count(\"bleached\"))\n",
    "print(\"Total photos    :\", len(paths))\n",
    "\n",
    "# Read one photo and look at what the computer actually sees.\n",
    "first = plt.imread(paths[0])\n",
    "print(\"One photo is stored as an array of shape :\", first.shape)\n",
    "print(\"Height x width x colours                 : 64 x 64 x 3 = 12288 numbers\")\n",
    "pixel = (first[40, 32] * 255).round()\n",
    "print(\"The pixel at row 40, column 32 (R, G, B) :\", int(pixel[0]), int(pixel[1]), int(pixel[2]))\n",
    "print()\n",
    "\n",
    "# Chart 1: eight photos of each kind, spread through each folder.\n",
    "healthy_paths = []\n",
    "bleached_paths = []\n",
    "for i in range(len(paths)):\n",
    "    if labels[i] == \"healthy\":\n",
    "        healthy_paths.append(paths[i])\n",
    "    else:\n",
    "        bleached_paths.append(paths[i])\n",
    "plt.figure(figsize=(9, 2.6))\n",
    "for i in range(8):\n",
    "    plt.subplot(2, 8, i + 1)\n",
    "    plt.imshow(plt.imread(healthy_paths[i * len(healthy_paths) // 8]))\n",
    "    plt.axis(\"off\")\n",
    "    if i == 0:\n",
    "        plt.title(\"healthy\", fontsize=9, loc=\"left\")\n",
    "    plt.subplot(2, 8, i + 9)\n",
    "    plt.imshow(plt.imread(bleached_paths[i * len(bleached_paths) // 8]))\n",
    "    plt.axis(\"off\")\n",
    "    if i == 0:\n",
    "        plt.title(\"bleached\", fontsize=9, loc=\"left\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart1_sample_photos.png\")\n",
    "plt.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Stage 3 - Data exploration"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Stage 3: Data exploration ---\")\n",
    "\n",
    "# Chart 2: one photo split into its red, green and blue layers.\n",
    "plt.figure(figsize=(9, 2.8))\n",
    "plt.subplot(1, 4, 1)\n",
    "plt.imshow(first)\n",
    "plt.title(\"Photo\")\n",
    "plt.axis(\"off\")\n",
    "channel_names = [\"Red\", \"Green\", \"Blue\"]\n",
    "channel_maps = [\"Reds\", \"Greens\", \"Blues\"]\n",
    "for ch in range(3):\n",
    "    plt.subplot(1, 4, ch + 2)\n",
    "    plt.imshow(first[:, :, ch], cmap=channel_maps[ch], vmin=0, vmax=1)\n",
    "    plt.title(channel_names[ch])\n",
    "    plt.axis(\"off\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart2_rgb_layers.png\")\n",
    "plt.close()\n",
    "\n",
    "# Feature extraction. Water is blue-green with very little red, and coral -\n",
    "# healthy or bleached - has a lot of red. So a pixel with red above 0.3\n",
    "# (about 77 out of 255) is counted as coral. This is segmentation: deciding\n",
    "# which pixels belong to the object.\n",
    "rows = []\n",
    "for i in range(len(paths)):\n",
    "    img = plt.imread(paths[i])[:, :, :3].astype(float) * 255\n",
    "    coral = img[:, :, 0] > 77\n",
    "    pixels = img[coral]                                  # only the coral pixels\n",
    "    brightness = pixels.mean()                            # 0 = black, 255 = white\n",
    "    colourful = (pixels.max(axis=1) - pixels.min(axis=1)).mean()   # grey/white = 0\n",
    "    rows.append([paths[i], labels[i], coral.sum(), round(brightness, 1), round(colourful, 1)])\n",
    "\n",
    "features = pd.DataFrame(rows, columns=[\"photo\", \"label\", \"coral_pixels\", \"brightness\", \"colourfulness\"])\n",
    "print(\"Features measured from each photo (first 5 of each kind):\")\n",
    "print(features[features[\"label\"] == \"healthy\"].head(5).to_string(index=False))\n",
    "print(features[features[\"label\"] == \"bleached\"].head(5).to_string(index=False))\n",
    "print()\n",
    "print(\"Average features of each kind:\")\n",
    "print(features.groupby(\"label\")[[\"coral_pixels\", \"brightness\", \"colourfulness\"]].mean().round(1))\n",
    "print()\n",
    "\n",
    "# Chart 3: the two features, one dot per photo.\n",
    "plt.figure(figsize=(7, 5))\n",
    "colours = [\"#b5651d\", \"#7fb3d5\"]\n",
    "for k in range(2):\n",
    "    part = features[features[\"label\"] == classes[k]]\n",
    "    plt.scatter(part[\"brightness\"], part[\"colourfulness\"], color=colours[k], label=classes[k], alpha=0.75)\n",
    "plt.title(\"Two features measured from every photo\")\n",
    "plt.xlabel(\"Brightness of the coral (0-255)\")\n",
    "plt.ylabel(\"Colourfulness of the coral\")\n",
    "plt.legend()\n",
    "plt.grid(True, linestyle=\"--\", alpha=0.4)\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart3_features.png\")\n",
    "plt.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Stage 4 - Modelling"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Stage 4: Modelling ---\")\n",
    "use = [\"brightness\", \"colourfulness\"]\n",
    "\n",
    "features = features.sample(frac=1, random_state=8)\n",
    "cut = int(len(features) * 0.8)\n",
    "train = features[:cut]\n",
    "test = features[cut:]\n",
    "print(\"Training photos :\", len(train))\n",
    "print(\"Testing photos  :\", len(test))\n",
    "\n",
    "low = train[use].min()\n",
    "high = train[use].max()\n",
    "averages = ((train[use] - low) / (high - low)).groupby(train[\"label\"]).mean()\n",
    "print()\n",
    "print(\"What the model learned - the average of each kind (rescaled 0 to 1):\")\n",
    "print(averages.round(2))\n",
    "print()\n",
    "\n",
    "test_scaled = (test[use] - low) / (high - low)\n",
    "predictions = []\n",
    "for i in range(len(test)):\n",
    "    distance = ((averages - test_scaled.iloc[i]) ** 2).sum(axis=1)\n",
    "    predictions.append(distance.idxmin())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Stage 5 - Evaluation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Stage 5: Evaluation ---\")\n",
    "actual = list(test[\"label\"])\n",
    "\n",
    "# \"bleached\" is the positive class: it is what the scientists are looking for.\n",
    "tp = fn = fp = tn = 0\n",
    "for i in range(len(actual)):\n",
    "    if actual[i] == \"bleached\" and predictions[i] == \"bleached\":\n",
    "        tp = tp + 1\n",
    "    elif actual[i] == \"bleached\":\n",
    "        fn = fn + 1\n",
    "    elif predictions[i] == \"bleached\":\n",
    "        fp = fp + 1\n",
    "    else:\n",
    "        tn = tn + 1\n",
    "accuracy = (tp + tn) / len(actual) * 100\n",
    "precision = 0\n",
    "if tp + fp > 0:\n",
    "    precision = tp / (tp + fp) * 100\n",
    "recall = 0\n",
    "if tp + fn > 0:\n",
    "    recall = tp / (tp + fn) * 100\n",
    "f1 = 0\n",
    "if precision + recall > 0:\n",
    "    f1 = 2 * precision * recall / (precision + recall)\n",
    "print(\"TP, FN, FP, TN :\", tp, fn, fp, tn)\n",
    "print(\"Accuracy       :\", round(accuracy, 1), \"%\")\n",
    "print(\"Precision      :\", round(precision, 1), \"%\")\n",
    "print(\"Recall         :\", round(recall, 1), \"%\")\n",
    "print(\"F1 score       :\", round(f1, 1), \"%\")\n",
    "print()\n",
    "\n",
    "# Looking at the mistakes is the most useful check in computer vision.\n",
    "wrong = []\n",
    "for i in range(len(actual)):\n",
    "    if predictions[i] != actual[i]:\n",
    "        wrong.append(i)\n",
    "print(\"Photos the model got wrong :\", len(wrong))\n",
    "for i in wrong:\n",
    "    row = test.iloc[i]\n",
    "    print(\"  \" + os.path.basename(row[\"photo\"]).ljust(18), \"really\", row[\"label\"].ljust(8),\n",
    "          \" brightness\", row[\"brightness\"], \" colourfulness\", row[\"colourfulness\"])\n",
    "print()\n",
    "\n",
    "# Chart 5: the photos it got wrong.\n",
    "if len(wrong) > 0:\n",
    "    plt.figure(figsize=(1.8 * len(wrong) + 0.5, 2.4))\n",
    "    for n in range(len(wrong)):\n",
    "        row = test.iloc[wrong[n]]\n",
    "        plt.subplot(1, len(wrong), n + 1)\n",
    "        plt.imshow(plt.imread(row[\"photo\"]))\n",
    "        plt.title(\"really \" + row[\"label\"], fontsize=9)\n",
    "        plt.axis(\"off\")\n",
    "    plt.tight_layout()\n",
    "    plt.savefig(\"chart5_mistakes.png\")\n",
    "    plt.close()\n",
    "\n",
    "# Chart 4: confusion matrix.\n",
    "grid = [[tp, fn], [fp, tn]]\n",
    "plt.figure(figsize=(5, 4.3))\n",
    "plt.imshow(grid, cmap=\"Blues\")\n",
    "names = [[\"TP\", \"FN\"], [\"FP\", \"TN\"]]\n",
    "for r in range(2):\n",
    "    for c in range(2):\n",
    "        if grid[r][c] > max(max(grid)) / 2:\n",
    "            ink = \"white\"\n",
    "        else:\n",
    "            ink = \"black\"\n",
    "        plt.text(c, r, names[r][c] + \"\\n\" + str(grid[r][c]), ha=\"center\", va=\"center\", fontsize=14, color=ink)\n",
    "plt.xticks([0, 1], [\"Said bleached\", \"Said healthy\"])\n",
    "plt.yticks([0, 1], [\"Bleached\", \"Healthy\"])\n",
    "plt.xlabel(\"What the model said\")\n",
    "plt.ylabel(\"The truth\")\n",
    "plt.title(\"Confusion matrix (testing photos)\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart4_matrix.png\")\n",
    "plt.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Stage 6 - Deployment"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Stage 6: Deployment ---\")\n",
    "new_names = sorted(os.listdir(\"new_photos\"))\n",
    "plt.figure(figsize=(8, 2.6))\n",
    "for i in range(len(new_names)):\n",
    "    img = plt.imread(os.path.join(\"new_photos\", new_names[i]))[:, :, :3].astype(float) * 255\n",
    "    pixels = img[img[:, :, 0] > 77]\n",
    "    point = pd.Series({\n",
    "        \"brightness\": pixels.mean(),\n",
    "        \"colourfulness\": (pixels.max(axis=1) - pixels.min(axis=1)).mean(),\n",
    "    })\n",
    "    distance = ((averages - (point - low) / (high - low)) ** 2).sum(axis=1)\n",
    "    answer = distance.idxmin()\n",
    "    print(new_names[i], \"-> brightness\", round(point[\"brightness\"]), \" colourfulness\",\n",
    "          round(point[\"colourfulness\"]), \"->\", answer.upper())\n",
    "    plt.subplot(1, len(new_names), i + 1)\n",
    "    plt.imshow(img.astype(\"uint8\"))\n",
    "    plt.title(answer, fontsize=10)\n",
    "    plt.axis(\"off\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart6_new_photos.png\")\n",
    "plt.close()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
