{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# K-Means Algorithm\n",
    "\n",
    "The general idea of clustering is to group data with similar traits. The main benefit of this is the ability to extract information from new data because you know what it is most similar to, thus giving you valuable insight. In the field of machine learning, this is considered unsupervised learning because it requires no labels on the data -- the algorithm auto assigns clusters, and you infer behavior off of those clusters.\n",
    "\n",
    "Clustering has many applications such as image segmentation, preference predictions, compression, model fitting.\n",
    "\n",
    "Although you can trace the idea of k-means clustering back to 1967 with a paper by Hugo Steinhaus, James MacQueen was the first to coin the term k-means in 1956. MacQueen's paper title \"Some Methods For Classification and Analysis of Multivariate Observations\" goes over the k-means process that segments an N-dimensional population into k sets. Note: when we refer to k in the algorithm, that is the number of sets that we are dividing the population.\n",
    "\n",
    "\n",
    "A great deal of MacQueens article discusses optimality for the k-means algorithm, which is an important area to discuss, especially when considering the time at which the article got published. Back in 1967, computers were very slow and expensive. Although we had proofs that can guarantee that we could find an optimal solution, they were a NP-Hard problem. This is critical because NP-Hard problems are problems that are exponential to solve.\n",
    "\n",
    "Although the k-means algorithm did not guarantee the optimal solution, there was a subset of problems that it did guarantee an optimal solution-- the specifics of these problems got discussed later in the article. Nerveless, since this algorithm wasn't computationally expensive and generally gave good results, it was a huge breakthrough at the time. \n",
    "\n",
    "\n",
    "This algorithm can be broken down into four major segments:\n",
    "\n",
    "## Step 1:\n",
    "\n",
    "Pick k random points as cluster centers called centroids.\n",
    "\n",
    "## Step 2: \n",
    "\n",
    "Assign each point to nearest cluster by calculating its distance to each centroid.\n",
    "\n",
    "## Step 3: \n",
    "\n",
    "Find new cluster center by taking the average of the assigned points.\n",
    "\n",
    "## Step 4:\n",
    "\n",
    "Repeat steps 2 and 3 until no cluster assignments change."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Python Implementation\n",
    "\n",
    "Implementing this in python is rather straight forward. Given data, cluster it into k sets and return the cluster assignments and cluster values."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import sys\n",
    "import numpy as np\n",
    "\n",
    "def distToClust(val, clusterCenter):\n",
    "    \"\"\"\n",
    "    Distance measure to cluster, can change\n",
    "    this to be different types of distances\n",
    "    \"\"\"\n",
    "    return np.linalg.norm(val-clusterCenter)\n",
    "\n",
    "\n",
    "def closestCenter(val, clusters):\n",
    "    \"\"\"\n",
    "    Finds the cluster closest to the presented\n",
    "    value\n",
    "    \"\"\"\n",
    "    curMin = sys.maxsize\n",
    "    curIndex = 0    \n",
    "    for k in range(0, len(clusters)):\n",
    "        d = distToClust(val, clusters[k])\n",
    "        if d < curMin:\n",
    "            curIndex = k\n",
    "            curMin = d\n",
    "    return curIndex\n",
    "\n",
    "\n",
    "def kmeansAlgo(k, data):\n",
    "    \"\"\"\n",
    "    k: number of clusters\n",
    "    data: nxd numpy matrix where n is number of elements\n",
    "          and d is the number of dimensions in the data.  \n",
    "    \n",
    "    return: tuple of assignments and clusters where clusters\n",
    "    is a list of clusteroids and assignments maps each value\n",
    "    to a single cluster\n",
    "    \"\"\"\n",
    "    \n",
    "    n = data.shape[0] # length of data to cluster\n",
    "    d = data.shape[1] # dimensionality of data\n",
    "    \n",
    "    # maps each element in data to a cluster\n",
    "    assignments = np.zeros(n, dtype=np.int) \n",
    "    \n",
    "    clusters = []\n",
    "    for i in range(0, k):\n",
    "        clusters.append(data[i])\n",
    "    \n",
    "    reAssigned = True\n",
    "    generations = 0\n",
    "    while reAssigned:\n",
    "        reAssigned = False\n",
    "        \n",
    "        # assign clusters\n",
    "        for i in range(0, n):\n",
    "            c = closestCenter(data[i], clusters)\n",
    "            if c != assignments[i]:\n",
    "                reAssigned = True\n",
    "                assignments[i] = c\n",
    "                \n",
    "        # re-compute centers\n",
    "        clusterValues = []\n",
    "        for _ in range(0, k):\n",
    "            clusterValues.append([])\n",
    "        for i in range(0, n):\n",
    "            clusterValues[assignments[i]].append(data[i])\n",
    "        for i in range(0, k):\n",
    "            clusters[i] = np.average(clusterValues[i], axis=0)\n",
    "        generations = generations + 1\n",
    "    print(\"Clustering took \" + str(generations) + \" generations\")\n",
    "    return assignments, clusters"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Image Segmentation\n",
    "\n",
    "Using our k-means algorithm we can cluster the pixels in an image together.\n",
    "\n",
    "## Clustering on Color\n",
    "\n",
    "When we cluster the pixels of an image based on color, we map pixels with similar color to the same cluster. Since an image is a three dimensional matrix, we can do this just fine. When clustering our data, the input is going to just be the list of pixel values. Note: as far as the k-means algorithm is conserned, there is no coordinates, just a list of pixels. The RGB values of the pixels gets clustered together. \n",
    "\n",
    "To make things run a bit faster, we are going to be using the k-means implementation from sklearn."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "def segmentImgClrRGB(imgFilename, k):\n",
    "\n",
    "    #1. Load the image \n",
    "    img = cv2.imread(imgFilename)\n",
    "    \n",
    "    h = img.shape[0]\n",
    "    w = img.shape[1]\n",
    "    \n",
    "    img.shape = (img.shape[0] * img.shape[1], 3)\n",
    "    \n",
    "    #5. Run k-means on the vectorized reponses X to get a vector of labels (the clusters); \n",
    "    #  \n",
    "    kmeans = KMeans(n_clusters=k, random_state=0).fit(img).labels_\n",
    "    \n",
    "    #6. Reshape the label results of k-means so that it has the same size as the input image\n",
    "    #   Return the label image which we call idx\n",
    "    kmeans.shape = (h, w)\n",
    "\n",
    "    return kmeans"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "After we have our pixel assignment we, want some useful way to display it. In this algorithm we are coloring in each pixel with the assignments's clusteroid center. IE: if our algorithm ran with three clusters, the new image would only have three colors in it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "import skimage\n",
    "from sklearn.cluster import KMeans\n",
    "from numpy import linalg as LA\n",
    "\n",
    "def colorClustering(idx, imgFilename, k):\n",
    "    img = cv2.imread(imgFilename)\n",
    "    \n",
    "    clusterValues = []\n",
    "    for _ in range(0, k):\n",
    "        clusterValues.append([])\n",
    "    \n",
    "    for r in range(0, idx.shape[0]):\n",
    "        for c in range(0, idx.shape[1]):\n",
    "            clusterValues[idx[r][c]].append(img[r][c])\n",
    "\n",
    "    imgC = np.copy(img)\n",
    "\n",
    "    clusterAverages = []\n",
    "    for i in range(0, k):\n",
    "#         print(len(clusterValues[i])/(idx.shape[1]*idx.shape[0]))\n",
    "        clusterAverages.append(np.average(clusterValues[i], axis=0))\n",
    "    \n",
    "    for r in range(0, idx.shape[0]):\n",
    "        for c in range(0, idx.shape[1]):\n",
    "            imgC[r][c] = clusterAverages[idx[r][c]]\n",
    "            \n",
    "    return imgC"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next we need a way of printing the images, I usually use matplot lib. I'm also displaying the image that we are going to be using for the clutering."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 152464 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "import cv2\n",
    "import matplotlib.pyplot as plt \n",
    "\n",
    "def printI(img):\n",
    "    rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n",
    "    plt.imshow(rgb)\n",
    "\n",
    "printI(cv2.imread(\"bg.jpg\"))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "***Segmentation done***\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 14892 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "idx = segmentImgClrRGB(\"bg.jpg\", 5)\n",
    "res = colorClustering(idx, \"bg.jpg\", 5)\n",
    "printI(res)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Just like you can segment with RGB values, you can also try different color spaces such as HSV. Playing around with the distance measure would also give you different results.\n",
    "\n",
    "\n",
    "\n",
    "## Texture Clustering\n",
    "\n",
    "Similarly to color segmentation, you can cluster based on texture detection. The premis of this method is that you generate a bunch of filters of different shapes, sizes, and scales. When you apply this filter bank to a pixel, it will have similar responses as pixels in regions with a similar texture.\n",
    "\n",
    "\n",
    "The first step is to construct a filter bank."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''\n",
    "The Leung-Malik (LM) Filter Bank, implementation in python\n",
    "\n",
    "T. Leung and J. Malik. Representing and recognizing the visual appearance of\n",
    "materials using three-dimensional textons. International Journal of Computer\n",
    "Vision, 43(1):29-44, June 2001.\n",
    "\n",
    "Reference: http://www.robots.ox.ac.uk/~vgg/research/texclass/filters.html\n",
    "'''\n",
    "\n",
    "def gaussian1d(sigma, mean, x, ord):\n",
    "    x = np.array(x)\n",
    "    x_ = x - mean\n",
    "    var = sigma**2\n",
    "\n",
    "    # Gaussian Function\n",
    "    g1 = (1/np.sqrt(2*np.pi*var))*(np.exp((-1*x_*x_)/(2*var)))\n",
    "\n",
    "    if ord == 0:\n",
    "        g = g1\n",
    "        return g\n",
    "    elif ord == 1:\n",
    "        g = -g1*((x_)/(var))\n",
    "        return g\n",
    "    else:\n",
    "        g = g1*(((x_*x_) - var)/(var**2))\n",
    "        return g\n",
    "\n",
    "def gaussian2d(sup, scales):\n",
    "    var = scales * scales\n",
    "    shape = (sup,sup)\n",
    "    n,m = [(i - 1)/2 for i in shape]\n",
    "    x,y = np.ogrid[-m:m+1,-n:n+1]\n",
    "    g = (1/np.sqrt(2*np.pi*var))*np.exp( -(x*x + y*y) / (2*var) )\n",
    "    return g\n",
    "\n",
    "def log2d(sup, scales):\n",
    "    var = scales * scales\n",
    "    shape = (sup,sup)\n",
    "    n,m = [(i - 1)/2 for i in shape]\n",
    "    x,y = np.ogrid[-m:m+1,-n:n+1]\n",
    "    g = (1/np.sqrt(2*np.pi*var))*np.exp( -(x*x + y*y) / (2*var) )\n",
    "    h = g*((x*x + y*y) - var)/(var**2)\n",
    "    return h\n",
    "\n",
    "def makefilter(scale, phasex, phasey, pts, sup):\n",
    "\n",
    "    gx = gaussian1d(3*scale, 0, pts[0,...], phasex)\n",
    "    gy = gaussian1d(scale,   0, pts[1,...], phasey)\n",
    "\n",
    "    image = gx*gy\n",
    "\n",
    "    image = np.reshape(image,(sup,sup))\n",
    "    return image\n",
    "\n",
    "def makeLMfilters():\n",
    "    sup     = 49\n",
    "    scalex  = np.sqrt(2) * np.array([1,2,3])\n",
    "    norient = 6\n",
    "    nrotinv = 12\n",
    "\n",
    "    nbar  = len(scalex)*norient\n",
    "    nedge = len(scalex)*norient\n",
    "    nf    = nbar+nedge+nrotinv\n",
    "    F     = np.zeros([sup,sup,nf])\n",
    "    hsup  = (sup - 1)/2\n",
    "\n",
    "    x = [np.arange(-hsup,hsup+1)]\n",
    "    y = [np.arange(-hsup,hsup+1)]\n",
    "\n",
    "    [x,y] = np.meshgrid(x,y)\n",
    "\n",
    "    orgpts = [x.flatten(), y.flatten()]\n",
    "    orgpts = np.array(orgpts)\n",
    "\n",
    "    count = 0\n",
    "    for scale in range(len(scalex)):\n",
    "        for orient in range(norient):\n",
    "            angle = (np.pi * orient)/norient\n",
    "            c = np.cos(angle)\n",
    "            s = np.sin(angle)\n",
    "            rotpts = [[c+0,-s+0],[s+0,c+0]]\n",
    "            rotpts = np.array(rotpts)\n",
    "            rotpts = np.dot(rotpts,orgpts)\n",
    "            F[:,:,count] = makefilter(scalex[scale], 0, 1, rotpts, sup)\n",
    "            F[:,:,count+nedge] = makefilter(scalex[scale], 0, 2, rotpts, sup)\n",
    "            count = count + 1\n",
    "\n",
    "    count = nbar+nedge\n",
    "    scales = np.sqrt(2) * np.array([1,2,3,4])\n",
    "\n",
    "    for i in range(len(scales)):\n",
    "        F[:,:,count]   = gaussian2d(sup, scales[i])\n",
    "        count = count + 1\n",
    "\n",
    "    for i in range(len(scales)):\n",
    "        F[:,:,count] = log2d(sup, scales[i])\n",
    "        count = count + 1\n",
    "\n",
    "    for i in range(len(scales)):\n",
    "        F[:,:,count] = log2d(sup, 3*scales[i])\n",
    "        count = count + 1\n",
    "\n",
    "    return F\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now that we have our filters we are going to display them to verify that they are what we expect."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 176048 characters]\n",
      "text/plain": [
       "<Figure size 1440x1440 with 48 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "def displayFilterBank():\n",
    "    \n",
    "    fig, axs = plt.subplots(6, 8, figsize=(20,20))\n",
    "    F = makeLMfilters()\n",
    "    for row in range(0, 6):\n",
    "        for col in range(0, 8):\n",
    "            f = F[:, :, row * 6 + col]\n",
    "            axs[row][col].imshow(f, cmap='hot', interpolation='nearest')\n",
    "            axs[row][col].grid(True)\n",
    "#     plt.show()\n",
    "    plt.savefig('filters.png')\n",
    "displayFilterBank()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "With the filter bank we can now start clustering based on the responses from each filter response. When we clustered with color, we had three dimensions, with our filter bank we now have 48 dimensions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "def generateGrayScale(image):\n",
    "    w = np.array([[[ 0.25, 0.5,  0.25]]])\n",
    "    gray2 = cv2.convertScaleAbs(np.sum(image*w, axis=2))\n",
    "    return gray2\n",
    "\n",
    "def segmentImg(imgFilename, clusters):\n",
    "    #1. Load and display the image from which you want to segment the foreground from the background\n",
    "    #  Make sure to convert your image to grayscale after loading\n",
    "    img = cv2.imread(imgFilename)\n",
    "    h = img.shape[0]\n",
    "    w = img.shape[1]\n",
    "    gImg =  generateGrayScale(img)\n",
    "    printI(gImg)\n",
    "    \n",
    "    #2. Create an overcomplete bank of filters F\n",
    "    F = makeLMfilters()\n",
    "    \n",
    "    s = F.shape\n",
    "    \n",
    "    #3. Convolve the input image with every filter in the bank of filters \n",
    "    responses = [None] * s[2]\n",
    "    for k in range(0, s[2]):\n",
    "        fb = F[:, :,k]\n",
    "        response = cv2.filter2D(gImg,-1, fb)\n",
    "        responses[k] = response\n",
    "    \n",
    "        \n",
    "    #4.Take the absolute values of the responses and \n",
    "    #  reshape the reponse tensor into a matrix of size [row*cols, num_filters]\n",
    "    a = np.zeros((h*w, s[2]), dtype=np.float)\n",
    "    \n",
    "    for r in range(0, h):\n",
    "        for c in range(0, w):\n",
    "            for f in range(0, s[2]):\n",
    "                t = abs(responses[f][r][c])\n",
    "                a[r * w + c][f] = abs(responses[f][r][c])\n",
    "\n",
    "    #5. Run k-means on the vectorized reponses X to get a vector of labels (the clusters); \n",
    "    kmeans = KMeans(n_clusters=clusters, random_state=0).fit(a).labels_    \n",
    "    \n",
    "    #6. Reshape the label results of k-means so that it has the same size as the input image\n",
    "    #   Return the label image which we call idx\n",
    "    kmeans.shape = (h, w)\n",
    "\n",
    "    return kmeans\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 15984 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "idx1 = segmentImg(\"bg.jpg\",6)\n",
    "res1 = colorClustering(idx1, \"bg.jpg\", 6)\n",
    "printI(res1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "With the texture clustering, you can see that part of the ocean was in the same cluster as part of the sky or beach. This is to be expected because they were clustered on texture and not color. When doing texture  detection a common thing to pick up is blurryness. This maxes texture clustering useful for things like background removal when your foreground is in focus and the background is blurred. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Takaways\n",
    "\n",
    "Due to the k-means algorithm not always converting on an optimum answer and being profoundly affected by outlines, it is rarely used by itself. However, it frequently used to bootstrap and influence other algorithms.\n",
    "As the MacQueen stated initially in his article, \"there is no feasible, general method which always yield an optimal partition.\" Due to the nature of NP-Hard problems, this fact is unlikely to change anytime soon. More recently, people have rebooted k-means to include a beam search to avoid converging on local maxima-- this process is called \"k-means++\" and Vassilvitskii outlines it in his  2007 paper titled \"K-means++: the advantages of careful seeding\".\n",
    "\n",
    "Another major question in this field of research research is: how do we choose k? Newer work with k-means and other clustering techniques look into how we can automatically select a k using the elbow technique or other techniques like GVF.\n",
    "\n",
    "K-means has had a lasting effect on the field of machine learning. Most textbooks and AI classes cover k-means clustering as a starting point when teaching people about unsupervised learning. Moreover, algorithms to this day are still using k-means as a tool behind the scenes to pre-process data before it gets fed to the next step in the data pipeline. \n"
   ]
  }
 ],
 "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.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
