├── images ├── raw │ ├── castoff │ │ └── .gitkeep │ ├── drops │ │ └── .gitkeep │ ├── spatters │ │ └── .gitkeep │ └── projected │ │ └── .gitkeep └── processed │ ├── castoff │ └── .gitkeep │ ├── drops │ └── .gitkeep │ ├── projected │ └── .gitkeep │ └── spatters │ └── .gitkeep ├── .gitignore ├── 2018_12_09 - Hack The Police - Team Splattern - Presentation.pdf ├── src ├── process.py ├── splat_predict.py └── make_training_embeddings.ipynb └── README.md /images/raw/castoff/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/raw/drops/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/raw/spatters/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /images/**/*.jpg 2 | -------------------------------------------------------------------------------- /images/processed/castoff/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/processed/drops/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/raw/projected/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/processed/projected/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/processed/spatters/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /2018_12_09 - Hack The Police - Team Splattern - Presentation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-sandall/splatterns/master/2018_12_09 - Hack The Police - Team Splattern - Presentation.pdf -------------------------------------------------------------------------------- /src/process.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import numpy as np 4 | from PIL import Image, ImageEnhance 5 | 6 | splattern_types = ['castoff', 'drops', 'projected', 'spatters'] 7 | 8 | jpg_files = [str(p) for splattern in splattern_types for p in Path(f'./images/raw/{splattern}/').glob('*.jpg')] 9 | 10 | for filename in jpg_files: 11 | print(f'Processing file {filename}') 12 | im = Image.open(filename) 13 | brightness = ImageEnhance.Brightness(im) 14 | im = brightness.enhance(2) 15 | contrast = ImageEnhance.Contrast(im) 16 | im = contrast.enhance(1.5) 17 | im 18 | im = im.convert('RGBA') 19 | data = np.array(im) 20 | rgb = data[:, :, :3] 21 | color = [50, 0, 0] 22 | black = [0, 0, 0, 255] 23 | white = [255, 255, 255, 255] 24 | mask = np.all((rgb - color) > 50, axis=-1) 25 | data[mask] = white 26 | new_im = Image.fromarray(data) 27 | grayscale = new_im.convert("L") 28 | grayscale.save(filename.replace('/raw/', '/processed/').replace('.jpg', '') + '-processed.jpg') 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bloodstain Pattern Analysis 2 | 3 | ## Hack The Police 3: Hackathon Project 4 | - [Hack The Police](https://hackthepolice.com/london) 5 | - [Team Splatterns Presentation](https://github.com/john-sandall/splatterns/blob/master/2018_12_09%20-%20Hack%20The%20Police%20-%20Team%20Splattern%20-%20Presentation.pdf) 6 | 7 | **Images:** You will need to populate the `/images/raw/` folder. We have taken the images from videos created by [Daniel Attinger et all](https://lib.dr.iastate.edu/me_pubs/) for their paper [_Fluid dynamics topics in bloodstain pattern analysis: Comparative review and research opportunities_](https://core.ac.uk/download/pdf/38936183.pdf) and cannot republish their images here due to licensing. Please contact us if you are interested in replicating our work. 8 | 9 | **Code:** 10 | 1. Pre-process images ([`process.py`](https://github.com/john-sandall/splatterns/blob/master/src/process.py)) 11 | 2. Train vector embeddings ([`make_training_embeddings.ipynb`](https://github.com/john-sandall/splatterns/blob/master/src/make_training_embeddings.ipynb)) 12 | 3. Predict new pattern ([`splat_predict.py`](https://github.com/john-sandall/splatterns/blob/master/src/splat_predict.py)) 13 | 14 | ## Team 15 | - [Shan Sun](https://github.com/bobbleoxs/) 16 | - [Gordon Blackadder](https://github.com/asos-gordon) 17 | - [John Sandall](https://twitter.com/john_sandall) 18 | - [James Curtis](https://twitter.com/jamescurtis29) 19 | 20 | --- 21 | 22 | ![BPA](http://narratively.com/wp-content/uploads/2014/02/a2EngPwQqaHRfmmwtWgt_benj_spot2.jpg) 23 | -------------------------------------------------------------------------------- /src/splat_predict.py: -------------------------------------------------------------------------------- 1 | #supress warnings 2 | import warnings 3 | warnings.filterwarnings("ignore") 4 | warnings.simplefilter(action='ignore', category=FutureWarning) 5 | import os 6 | os.environ['TF_CPP_MIN_LOG_LEVEL']='2' 7 | import tensorflow as tf 8 | 9 | #import key libraries 10 | import numpy as np 11 | import keras 12 | from keras.preprocessing import image 13 | import json 14 | import sys 15 | 16 | 17 | #import pretrained model 18 | model = keras.applications.VGG16(include_top=False, weights='imagenet', pooling="max") 19 | 20 | #import prediction labels(0, 1, etc) to human labels ("splat", "drop", etc) 21 | with open("src/codings.json", 'r') as f: 22 | id_to_label = json.loads(f.readline()) 23 | 24 | #import image 25 | def path_to_tensor(img_path): 26 | # loads RGB image as PIL.Image.Image type 27 | img = image.load_img(img_path, target_size=(224, 224)) 28 | # convert PIL.Image.Image type to 3D tensor with shape (224, 224, 3) 29 | x = image.img_to_array(img) 30 | # convert 3D tensor to 4D tensor with shape (1, 224, 224, 3) and return 4D tensor 31 | return np.expand_dims(x, axis=0) 32 | 33 | def normalize(v): 34 | norm = np.linalg.norm(v) 35 | if norm == 0: 36 | return v 37 | return v / norm 38 | 39 | 40 | def make_prediction(file_name): 41 | #get embedding for test image 42 | test_image = path_to_tensor(file_name) 43 | test_embedding = model.predict(test_image)[0] 44 | # print("test_embedding", test_embedding) 45 | 46 | #load precalculated image embeddings and labels 47 | embeddings = np.load('src/embeds.npy') 48 | labels = np.load("src/labels.npy") 49 | 50 | #take dot product of test image with saved images 51 | from scipy import spatial 52 | scores = [1 - spatial.distance.cosine(test_embedding, train_ex) for train_ex in embeddings] 53 | # print(scores) 54 | #find most similar image 55 | best_match = np.argmax(scores) 56 | # print(best_match, np.max(scores)) 57 | best_label = labels[best_match] 58 | return id_to_label[str(best_label)] 59 | 60 | print("\n\n PREDICTION: ", make_prediction(sys.argv[1])) 61 | -------------------------------------------------------------------------------- /src/make_training_embeddings.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Import Libraries" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [ 15 | { 16 | "name": "stderr", 17 | "output_type": "stream", 18 | "text": [ 19 | "/Users/gordon.blackadder/anaconda3/lib/python3.6/site-packages/h5py/__init__.py:34: FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`.\n", 20 | " from ._conv import register_converters as _register_converters\n", 21 | "Using TensorFlow backend.\n" 22 | ] 23 | } 24 | ], 25 | "source": [ 26 | "import keras\n", 27 | "from keras.preprocessing import image \n", 28 | "from keras.preprocessing.image import ImageDataGenerator\n", 29 | "from tqdm import tqdm\n", 30 | "import numpy as np\n" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "metadata": {}, 36 | "source": [ 37 | "# Get embeddings from images with known labels" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 8, 43 | "metadata": {}, 44 | "outputs": [ 45 | { 46 | "name": "stdout", 47 | "output_type": "stream", 48 | "text": [ 49 | "Found 67 images belonging to 4 classes.\n" 50 | ] 51 | } 52 | ], 53 | "source": [ 54 | "datagen = ImageDataGenerator(rescale=1. / 255)\n", 55 | "\n", 56 | "model = keras.applications.VGG16(include_top=False, weights='imagenet', pooling=\"max\")\n", 57 | "\n", 58 | "# dimensions of our images.\n", 59 | "img_width, img_height = 224, 224\n", 60 | "batch_size = 13\n", 61 | "generator = datagen.flow_from_directory(\n", 62 | " \"images/processed\",#train_data_dir,\n", 63 | " target_size=(img_width, img_height),\n", 64 | " batch_size=batch_size,\n", 65 | " shuffle=False, class_mode='sparse')\n", 66 | "\n", 67 | "label_to_id = generator.class_indices\n", 68 | "id_to_label = {val:key for key, val in label_to_id.items()}\n", 69 | "\n", 70 | "nb_train_samples = 67\n", 71 | "\n", 72 | "\n", 73 | "\n", 74 | "embeddings = []\n", 75 | "labels = []\n", 76 | "\n", 77 | "seen_samples = 0\n", 78 | "for image_batch, label_batch in generator:\n", 79 | " embs = model.predict(image_batch)\n", 80 | " for emb in embs:\n", 81 | " embeddings.append(emb)\n", 82 | " for lab in label_batch:\n", 83 | " labels.append(lab)\n", 84 | " seen_samples += len(label_batch)\n", 85 | " if seen_samples >= nb_train_samples:\n", 86 | " break\n", 87 | " \n", 88 | "embeddings = np.array(embeddings)\n", 89 | "labels = np.array(labels)" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": 9, 95 | "metadata": {}, 96 | "outputs": [ 97 | { 98 | "data": { 99 | "text/plain": [ 100 | "67" 101 | ] 102 | }, 103 | "execution_count": 9, 104 | "metadata": {}, 105 | "output_type": "execute_result" 106 | } 107 | ], 108 | "source": [ 109 | "len(embeddings)" 110 | ] 111 | }, 112 | { 113 | "cell_type": "code", 114 | "execution_count": 10, 115 | "metadata": {}, 116 | "outputs": [], 117 | "source": [ 118 | "np.save('src/embeds', embeddings)\n", 119 | "np.save('src/labels', labels)" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": 11, 125 | "metadata": {}, 126 | "outputs": [], 127 | "source": [ 128 | "import json\n", 129 | "with open(\"src/codings.json\", 'w') as f:\n", 130 | " f.write(json.dumps(id_to_label))" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "metadata": {}, 137 | "outputs": [], 138 | "source": [] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "metadata": {}, 143 | "source": [ 144 | "# Function to import a test image" 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": null, 150 | "metadata": {}, 151 | "outputs": [], 152 | "source": [ 153 | "def path_to_tensor(img_path):\n", 154 | " # loads RGB image as PIL.Image.Image type\n", 155 | " img = image.load_img(img_path, target_size=(224, 224))\n", 156 | " # convert PIL.Image.Image type to 3D tensor with shape (224, 224, 3)\n", 157 | " x = image.img_to_array(img)\n", 158 | " # convert 3D tensor to 4D tensor with shape (1, 224, 224, 3) and return 4D tensor\n", 159 | " return np.expand_dims(x, axis=0)" 160 | ] 161 | }, 162 | { 163 | "cell_type": "markdown", 164 | "metadata": {}, 165 | "source": [ 166 | "# Function to find most similar image from training set" 167 | ] 168 | }, 169 | { 170 | "cell_type": "code", 171 | "execution_count": 12, 172 | "metadata": {}, 173 | "outputs": [], 174 | "source": [ 175 | "embeddings = np.load('src/embeds.npy')\n", 176 | "labels = np.load(\"src/labels.npy\")" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": 13, 182 | "metadata": {}, 183 | "outputs": [ 184 | { 185 | "data": { 186 | "text/plain": [ 187 | "array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,\n", 188 | " 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,\n", 189 | " 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,\n", 190 | " 3], dtype=int32)" 191 | ] 192 | }, 193 | "execution_count": 13, 194 | "metadata": {}, 195 | "output_type": "execute_result" 196 | } 197 | ], 198 | "source": [ 199 | "labels" 200 | ] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": 14, 205 | "metadata": {}, 206 | "outputs": [ 207 | { 208 | "data": { 209 | "text/plain": [ 210 | "{0: 'castoff', 1: 'drops', 2: 'projected', 3: 'spatters'}" 211 | ] 212 | }, 213 | "execution_count": 14, 214 | "metadata": {}, 215 | "output_type": "execute_result" 216 | } 217 | ], 218 | "source": [ 219 | "id_to_label" 220 | ] 221 | }, 222 | { 223 | "cell_type": "code", 224 | "execution_count": 12, 225 | "metadata": {}, 226 | "outputs": [], 227 | "source": [ 228 | "def make_prediction(file_name):\n", 229 | " embeddings = np.load('src/embeds.npy')\n", 230 | " labels = np.load(\"src/labels.npy\")\n", 231 | " test_image = path_to_tensor(file_name)\n", 232 | " test_embedding = model.predict(test_image)[0]\n", 233 | " scores = [np.dot(test_embedding, train_ex) for train_ex in embeddings]\n", 234 | " best_match = np.argmax(scores)\n", 235 | " best_label = labels[best_match]\n", 236 | " return id_to_label[best_label]" 237 | ] 238 | }, 239 | { 240 | "cell_type": "markdown", 241 | "metadata": {}, 242 | "source": [ 243 | "# Example Prediction" 244 | ] 245 | }, 246 | { 247 | "cell_type": "code", 248 | "execution_count": 13, 249 | "metadata": {}, 250 | "outputs": [ 251 | { 252 | "data": { 253 | "text/plain": [ 254 | "'drops'" 255 | ] 256 | }, 257 | "execution_count": 13, 258 | "metadata": {}, 259 | "output_type": "execute_result" 260 | } 261 | ], 262 | "source": [ 263 | "import sys\n", 264 | "test_file_name = sys.argv[1]\n", 265 | "make_prediction(\"test.jpg\")" 266 | ] 267 | }, 268 | { 269 | "cell_type": "code", 270 | "execution_count": null, 271 | "metadata": {}, 272 | "outputs": [], 273 | "source": [] 274 | } 275 | ], 276 | "metadata": { 277 | "kernelspec": { 278 | "display_name": "Python 3", 279 | "language": "python", 280 | "name": "python3" 281 | }, 282 | "language_info": { 283 | "codemirror_mode": { 284 | "name": "ipython", 285 | "version": 3 286 | }, 287 | "file_extension": ".py", 288 | "mimetype": "text/x-python", 289 | "name": "python", 290 | "nbconvert_exporter": "python", 291 | "pygments_lexer": "ipython3", 292 | "version": "3.6.3" 293 | } 294 | }, 295 | "nbformat": 4, 296 | "nbformat_minor": 2 297 | } 298 | --------------------------------------------------------------------------------