{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Finding Cracks with Convolution\n",
    "\n",
    "wall_cracks.py\n",
    "\n",
    "AI project on Computer Vision: finding cracks in photos of concrete walls,\n",
    "using the CONVOLUTION operator from Unit 5.\n",
    "\n",
    "CBSE Class X, Artificial Intelligence (417), Part C project work.\n",
    "SDG 9 (Industry, Innovation and Infrastructure) and SDG 11 (Sustainable\n",
    "Cities and Communities).\n",
    "\n",
    "A crack is dark - but so is a damp stain, which is harmless. The\n",
    "difference is that a crack is THIN and SHARP, while a stain is wide and\n",
    "soft. A convolution kernel that detects edges reacts strongly to thin\n",
    "sharp lines and hardly at all to soft patches. The project compares:\n",
    "  - Model A: uses only how much of the wall is DARK\n",
    "  - Model B: uses how much of the wall the edge kernel LIGHTS UP\n",
    "Each model learns its own cut-off from the training photos.\n",
    "\n",
    "Libraries: numpy (the convolution), matplotlib (images and charts),\n",
    "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(\"FINDING CRACKS WITH CONVOLUTION\")\n",
    "print(\"=\" * 60)\n",
    "print(\"Who   : engineers who inspect buildings, bridges and school walls\")\n",
    "print(\"What  : cracks are missed, or stains are mistaken for cracks\")\n",
    "print(\"Where : old buildings, flyovers and walls after heavy rain\")\n",
    "print(\"Why   : a crack found early can be repaired before it is dangerous\")\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 = [\"cracked\", \"sound\"]\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(\"Cracked walls :\", labels.count(\"cracked\"))\n",
    "print(\"Sound walls   :\", labels.count(\"sound\"))\n",
    "print(\"Total photos  :\", len(paths))\n",
    "print()\n",
    "\n",
    "cracked_paths = []\n",
    "sound_paths = []\n",
    "for i in range(len(paths)):\n",
    "    if labels[i] == \"cracked\":\n",
    "        cracked_paths.append(paths[i])\n",
    "    else:\n",
    "        sound_paths.append(paths[i])\n",
    "\n",
    "# Chart 1: six walls of each kind.\n",
    "plt.figure(figsize=(9, 3.2))\n",
    "for i in range(6):\n",
    "    plt.subplot(2, 6, i + 1)\n",
    "    plt.imshow(plt.imread(cracked_paths[i * len(cracked_paths) // 6]))\n",
    "    plt.axis(\"off\")\n",
    "    if i == 0:\n",
    "        plt.title(\"cracked\", fontsize=9, loc=\"left\")\n",
    "    plt.subplot(2, 6, i + 7)\n",
    "    plt.imshow(plt.imread(sound_paths[i * len(sound_paths) // 6]))\n",
    "    plt.axis(\"off\")\n",
    "    if i == 0:\n",
    "        plt.title(\"sound\", fontsize=9, loc=\"left\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart1_sample_walls.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",
    "# The edge-detecting kernel. The centre pixel is weighed +4 and its four\n",
    "# neighbours -1 each. On a flat patch they cancel out to about 0; on a thin\n",
    "# line, where the centre is very different from its neighbours, the result\n",
    "# is large.\n",
    "kernel = np.array([[0, -1, 0],\n",
    "                   [-1, 4, -1],\n",
    "                   [0, -1, 0]])\n",
    "print(\"The kernel:\")\n",
    "print(kernel)\n",
    "print()\n",
    "\n",
    "# Convolution: slide the 3 x 3 kernel over every position in the image,\n",
    "# multiply each of the 9 pixels under it by the matching kernel number, and\n",
    "# add the 9 results up. Done here for all positions at once: for each of the\n",
    "# 9 kernel cells, take the part of the image under it and add it in.\n",
    "# The output is 2 pixels smaller each way, because the kernel cannot be\n",
    "# centred on the edge pixels.\n",
    "rows = []\n",
    "for i in range(len(paths)):\n",
    "    grey = plt.imread(paths[i])[:, :, :3].astype(float).mean(axis=2) * 255   # grayscale, 0-255\n",
    "    h, w = grey.shape\n",
    "    edges = np.zeros((h - 2, w - 2))\n",
    "    for dy in range(3):\n",
    "        for dx in range(3):\n",
    "            edges = edges + kernel[dy, dx] * grey[dy:dy + h - 2, dx:dx + w - 2]\n",
    "    dark_pct = (grey < np.median(grey) - 25).mean() * 100     # much darker than the wall\n",
    "    edge_pct = (np.abs(edges) > 60).mean() * 100              # lit up by the kernel\n",
    "    rows.append([paths[i], labels[i], round(dark_pct, 2), round(edge_pct, 2)])\n",
    "\n",
    "data = pd.DataFrame(rows, columns=[\"photo\", \"label\", \"dark_pct\", \"edge_pct\"])\n",
    "print(\"Size of a photo            :\", h, \"x\", w)\n",
    "print(\"Size after the convolution :\", h - 2, \"x\", w - 2)\n",
    "print()\n",
    "print(\"Average features of each kind:\")\n",
    "print(data.groupby(\"label\")[[\"dark_pct\", \"edge_pct\"]].mean().round(2))\n",
    "print()\n",
    "\n",
    "# Chart 2: what the kernel does to a cracked wall and to a stained one.\n",
    "stained = None\n",
    "for p in sound_paths:\n",
    "    g = plt.imread(p)[:, :, :3].astype(float).mean(axis=2) * 255\n",
    "    if (g < np.median(g) - 25).mean() > 0.05:\n",
    "        stained = p\n",
    "        break\n",
    "plt.figure(figsize=(7, 6.2))\n",
    "pair = [cracked_paths[0], stained]\n",
    "titles = [\"cracked wall\", \"stained wall (no crack)\"]\n",
    "for n in range(2):\n",
    "    grey = plt.imread(pair[n])[:, :, :3].astype(float).mean(axis=2) * 255\n",
    "    h, w = grey.shape\n",
    "    edges = np.zeros((h - 2, w - 2))\n",
    "    for dy in range(3):\n",
    "        for dx in range(3):\n",
    "            edges = edges + kernel[dy, dx] * grey[dy:dy + h - 2, dx:dx + w - 2]\n",
    "    plt.subplot(2, 2, n * 2 + 1)\n",
    "    plt.imshow(grey, cmap=\"gray\", vmin=0, vmax=255)\n",
    "    plt.title(titles[n], fontsize=10)\n",
    "    plt.axis(\"off\")\n",
    "    plt.subplot(2, 2, n * 2 + 2)\n",
    "    plt.imshow(np.abs(edges), cmap=\"magma\", vmin=0, vmax=150)\n",
    "    plt.title(\"after the edge kernel\", fontsize=10)\n",
    "    plt.axis(\"off\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart2_convolution.png\")\n",
    "plt.close()\n",
    "\n",
    "# Chart 3: the two features, one dot per photo.\n",
    "plt.figure(figsize=(7, 5))\n",
    "dot = [\"#e15759\", \"#4e79a7\"]\n",
    "for k in range(2):\n",
    "    part = data[data[\"label\"] == classes[k]]\n",
    "    plt.scatter(part[\"dark_pct\"], part[\"edge_pct\"], color=dot[k], label=classes[k], alpha=0.75)\n",
    "plt.title(\"Dark pixels and edge pixels in every photo\")\n",
    "plt.xlabel(\"Dark pixels (% of the photo)\")\n",
    "plt.ylabel(\"Edge pixels after convolution (%)\")\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",
    "data = data.sample(frac=1, random_state=9)\n",
    "cut = int(len(data) * 0.8)\n",
    "train = data[:cut]\n",
    "test = data[cut:]\n",
    "print(\"Training photos :\", len(train))\n",
    "print(\"Testing photos  :\", len(test))\n",
    "print()\n",
    "\n",
    "# Each model is \"cracked if <feature> is at least <cut-off>\". The cut-off is\n",
    "# learned: try every value the feature takes in the training photos, and\n",
    "# keep the one with the fewest training mistakes.\n",
    "truth = train[\"label\"] == \"cracked\"\n",
    "learned = {}\n",
    "for feature in [\"dark_pct\", \"edge_pct\"]:\n",
    "    best = None\n",
    "    for c in sorted(train[feature].unique()):\n",
    "        mistakes = ((train[feature] >= c) != truth).sum()\n",
    "        if best is None or mistakes < best[1]:\n",
    "            best = [c, mistakes]\n",
    "    learned[feature] = best[0]\n",
    "    print(\"Model using\", feature.ljust(9), \": cracked if\", feature, \">=\", best[0],\n",
    "          \" (\" + str(best[1]), \"training mistakes)\")\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Stage 5 - Evaluation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Stage 5: Evaluation ---\")\n",
    "results = {}\n",
    "for feature in [\"dark_pct\", \"edge_pct\"]:\n",
    "    tp = fn = fp = tn = 0\n",
    "    for i in range(len(test)):\n",
    "        row = test.iloc[i]\n",
    "        said = row[feature] >= learned[feature]\n",
    "        if row[\"label\"] == \"cracked\" and said:\n",
    "            tp = tp + 1\n",
    "        elif row[\"label\"] == \"cracked\":\n",
    "            fn = fn + 1\n",
    "        elif said:\n",
    "            fp = fp + 1\n",
    "        else:\n",
    "            tn = tn + 1\n",
    "    results[feature] = [tp, fn, fp, tn]\n",
    "    accuracy = (tp + tn) / len(test) * 100\n",
    "    recall = 0\n",
    "    if tp + fn > 0:\n",
    "        recall = tp / (tp + fn) * 100\n",
    "    precision = 0\n",
    "    if tp + fp > 0:\n",
    "        precision = tp / (tp + fp) * 100\n",
    "    print(\"Model using \" + feature)\n",
    "    print(\"  \" + feature + \" TP, FN, FP, TN :\", tp, fn, fp, tn)\n",
    "    print(\"  \" + feature + \" accuracy  :\", round(accuracy, 1), \"%\")\n",
    "    print(\"  \" + feature + \" precision :\", round(precision, 1), \"%\")\n",
    "    print(\"  \" + feature + \" recall    :\", round(recall, 1), \"%\")\n",
    "    print()\n",
    "\n",
    "# Chart 4: confusion matrix of the convolution model.\n",
    "tp, fn, fp, tn = results[\"edge_pct\"]\n",
    "grid = [[tp, fn], [fp, tn]]\n",
    "plt.figure(figsize=(5, 4.3))\n",
    "plt.imshow(grid, cmap=\"Reds\")\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 cracked\", \"Said sound\"])\n",
    "plt.yticks([0, 1], [\"Cracked\", \"Sound\"])\n",
    "plt.xlabel(\"What the model said\")\n",
    "plt.ylabel(\"The truth\")\n",
    "plt.title(\"Convolution model (testing photos)\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart4_matrix.png\")\n",
    "plt.close()\n",
    "\n",
    "# Chart 5: the two models compared.\n",
    "acc_dark = (results[\"dark_pct\"][0] + results[\"dark_pct\"][3]) / len(test) * 100\n",
    "acc_edge = (results[\"edge_pct\"][0] + results[\"edge_pct\"][3]) / len(test) * 100\n",
    "plt.figure(figsize=(6, 4))\n",
    "plt.bar([\"Dark pixels\", \"Edge pixels\\n(convolution)\"], [acc_dark, acc_edge], color=[\"#9c9c9c\", \"#e15759\"])\n",
    "plt.ylim(0, 100)\n",
    "plt.ylabel(\"Accuracy on testing photos (%)\")\n",
    "plt.title(\"Which feature finds cracks?\")\n",
    "plt.text(0, acc_dark + 2, str(round(acc_dark, 1)) + \"%\", ha=\"center\")\n",
    "plt.text(1, acc_edge + 2, str(round(acc_edge, 1)) + \"%\", ha=\"center\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart5_models.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.7))\n",
    "for i in range(len(new_names)):\n",
    "    grey = plt.imread(os.path.join(\"new_photos\", new_names[i]))[:, :, :3].astype(float).mean(axis=2) * 255\n",
    "    h, w = grey.shape\n",
    "    edges = np.zeros((h - 2, w - 2))\n",
    "    for dy in range(3):\n",
    "        for dx in range(3):\n",
    "            edges = edges + kernel[dy, dx] * grey[dy:dy + h - 2, dx:dx + w - 2]\n",
    "    edge_pct = (np.abs(edges) > 60).mean() * 100\n",
    "    if edge_pct >= learned[\"edge_pct\"]:\n",
    "        verdict = \"CRACK - send an engineer\"\n",
    "    else:\n",
    "        verdict = \"no crack found\"\n",
    "    print(new_names[i], \"-> edge pixels\", round(edge_pct, 2), \"% ->\", verdict)\n",
    "    plt.subplot(1, len(new_names), i + 1)\n",
    "    plt.imshow(grey, cmap=\"gray\", vmin=0, vmax=255)\n",
    "    plt.title(verdict.split(\" - \")[0], fontsize=10)\n",
    "    plt.axis(\"off\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart6_new_walls.png\")\n",
    "plt.close()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
