├── Generative Adversial Networks ├── Build and Deploy a GAN Streamlit App on Heroku │ ├── 1.png │ ├── Procfile │ ├── app.py │ ├── horse.jpg │ ├── readme.md │ ├── requirements.txt │ ├── setup.sh │ ├── white horse.jpg │ ├── zebra 2.jpg │ └── zebra.jpg ├── readme.md └── requirements.txt ├── Readme.md ├── Tracking Objects in Video with Particle Filters ├── Readme.md ├── Tracking Objects in Video with Particle Filters.ipynb └── walking.mp4 ├── Video Analysis └── Readme.md └── image-classification ├── Multiclass_Image_Classification_Project.ipynb └── readme.md /Generative Adversial Networks/Build and Deploy a GAN Streamlit App on Heroku/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youssefHosni/Practical-Computer-Vision-In-Python/cb72d148da8d4d47be06c9e244e9b5e4c211ed25/Generative Adversial Networks/Build and Deploy a GAN Streamlit App on Heroku/1.png -------------------------------------------------------------------------------- /Generative Adversial Networks/Build and Deploy a GAN Streamlit App on Heroku/Procfile: -------------------------------------------------------------------------------- 1 | web: sh setup.sh && streamlit run app.py -------------------------------------------------------------------------------- /Generative Adversial Networks/Build and Deploy a GAN Streamlit App on Heroku/app.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from PIL import Image 3 | from tensorflow.keras.models import load_model 4 | from keras_contrib.layers.normalization.instancenormalization import InstanceNormalization 5 | import numpy as np 6 | from numpy import vstack 7 | 8 | #loading pretrained GAN models 9 | cust = {'InstanceNormalization':InstanceNormalization} 10 | model_horse2zebra = load_model('./g_model_AtoB_023740.h5',cust) 11 | model_zebra2horse = load_model('./g_model_BtoA_023740.h5',cust) 12 | 13 | 14 | def load_image(image_path): 15 | """ 16 | Function to load image given image path 17 | :param image_path: image path 18 | :return: loaded image 19 | """ 20 | image = Image.open(image_path) 21 | newsize = (256, 256) 22 | image = image.resize(newsize) 23 | image = np.array(image) 24 | image = image[np.newaxis, ...] # convert the array into 3D array 25 | return image 26 | 27 | def generate_image(model, image): 28 | """ 29 | Function to generate image 30 | :param model: the GAN model to generate new image 31 | :param image: the image to 32 | :return: generated images 33 | """ 34 | generated_image = model.predict(image) 35 | images = vstack(generated_image) 36 | images = (images+1)/2.0 37 | return images 38 | 39 | st.title("Horse Zebra GAN Web APP") # Set the title 40 | st.image(Image.open('./1.png')) 41 | 42 | pick = st.selectbox("Please select a GAN model to use:", ["Horse 2 Zebra GAN", "Zebra 2 Horse GAN"]) 43 | 44 | if pick == "Horse 2 Zebra GAN": 45 | st.write("This is a GAN model for Generating Zebra images from Horses") 46 | st.write("Try out the GAN model with a default images of a horse or simply upload an image") 47 | 48 | if st.button("Try with Default Image"): 49 | image = load_image('./horse.jpg') 50 | st.subheader("Horse Image") 51 | st.image(image) 52 | st.subheader("Generated Zebra Image") 53 | st.image(generate_image(model_horse2zebra, image)) 54 | 55 | st.subheader("Upload an image file of a horse to convert it to a Zebra") 56 | uploaded_file = st.file_uploader("Upload JPG image file of a horse only", type=["jpg","jpeg"]) 57 | 58 | if uploaded_file: 59 | image = load_image(uploaded_file) 60 | st.image(generate_image(model_horse2zebra, image)) 61 | 62 | elif pick == "Zebra 2 Horse GAN" : 63 | st.write("This is a GAN model for Generating Horse images from Zebras") 64 | st.write("Try out the GAN model with a default images of a zebra or simply upload an image") 65 | 66 | if st.button("Try with Default Image"): 67 | image = load_image('./zebra.jpg') 68 | st.subheader("Zebra Image") 69 | st.image(image) 70 | st.subheader("Generated Horse Image") 71 | st.image(generate_image(model_zebra2horse, image)) 72 | 73 | st.subheader("Upload an image file of a zebra to convert it to a horse") 74 | uploaded_file = st.file_uploader("Upload JPG image file of a zebra only", type=["jpg","jpeg"]) 75 | 76 | if uploaded_file: 77 | image = load_image(uploaded_file) 78 | st.image(generate_image(model_zebra2horse, image)) 79 | 80 | 81 | -------------------------------------------------------------------------------- /Generative Adversial Networks/Build and Deploy a GAN Streamlit App on Heroku/horse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youssefHosni/Practical-Computer-Vision-In-Python/cb72d148da8d4d47be06c9e244e9b5e4c211ed25/Generative Adversial Networks/Build and Deploy a GAN Streamlit App on Heroku/horse.jpg -------------------------------------------------------------------------------- /Generative Adversial Networks/Build and Deploy a GAN Streamlit App on Heroku/readme.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Generative Adversial Networks/Build and Deploy a GAN Streamlit App on Heroku/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youssefHosni/Practical-Computer-Vision-In-Python/cb72d148da8d4d47be06c9e244e9b5e4c211ed25/Generative Adversial Networks/Build and Deploy a GAN Streamlit App on Heroku/requirements.txt -------------------------------------------------------------------------------- /Generative Adversial Networks/Build and Deploy a GAN Streamlit App on Heroku/setup.sh: -------------------------------------------------------------------------------- 1 | mkdir -p ~/.streamlit/ 2 | echo "\ 3 | [server]\n\ 4 | headless = true\n\ 5 | port = $PORT\n\ 6 | enableCORS = false\n\ 7 | \n\ 8 | " > ~/.streamlit/config.toml -------------------------------------------------------------------------------- /Generative Adversial Networks/Build and Deploy a GAN Streamlit App on Heroku/white horse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youssefHosni/Practical-Computer-Vision-In-Python/cb72d148da8d4d47be06c9e244e9b5e4c211ed25/Generative Adversial Networks/Build and Deploy a GAN Streamlit App on Heroku/white horse.jpg -------------------------------------------------------------------------------- /Generative Adversial Networks/Build and Deploy a GAN Streamlit App on Heroku/zebra 2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youssefHosni/Practical-Computer-Vision-In-Python/cb72d148da8d4d47be06c9e244e9b5e4c211ed25/Generative Adversial Networks/Build and Deploy a GAN Streamlit App on Heroku/zebra 2.jpg -------------------------------------------------------------------------------- /Generative Adversial Networks/Build and Deploy a GAN Streamlit App on Heroku/zebra.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youssefHosni/Practical-Computer-Vision-In-Python/cb72d148da8d4d47be06c9e244e9b5e4c211ed25/Generative Adversial Networks/Build and Deploy a GAN Streamlit App on Heroku/zebra.jpg -------------------------------------------------------------------------------- /Generative Adversial Networks/readme.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Generative Adversial Networks/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youssefHosni/Practical-Computer-Vision-In-Python/cb72d148da8d4d47be06c9e244e9b5e4c211ed25/Generative Adversial Networks/requirements.txt -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Practical Computer Vision In Python # 2 | Computer vision projects that covers most of the computer vision applications and tasks. 3 | 4 | This will be a series of computer vision projects that will cover most of the computer vision tasks and applications as dissced in this article [Computer Vision Tasks and Applications](https://pub.towardsai.net/overview-of-the-computer-vision-tasks-applications-647f63e66e9f?sk=c91f0f20aa48a82fd710744258d82d3d). 5 | 6 | ## 1. Content Recognition 7 | 8 | ### 1.1. Image Classification Projects 9 | * [Multiclass Image Classifcation Project](https://github.com/youssefHosni/Practical-Computer-Vision-In-Python/blob/main/image-classification/Multiclass_Image_Classification_Project.ipynb) 10 | 11 | 12 | ### 1.2. Object Identification Projects 13 | 14 | ### 1.3. Object Detection and Localization Projects 15 | * [Detecting Car Plates Number using Python]() [[Code]() | [Article]() ] 16 | 17 | 18 | ### 1.4. Object and Instance Segmentation Projects 19 | 20 | ### 1.5. Pose Estimation Projects 21 | 22 | ## [2. Video Analysis](https://github.com/youssefHosni/Practical-Computer-Vision-In-Python/tree/main/Video%20Analysis) 23 | 24 | ### 2.1. Object Tracking Projects 25 | * [Tracking Object in Videos Using Particle Filters](https://github.com/youssefHosni/Practical-Computer-Vision-In-Python/tree/main/Tracking%20Objects%20in%20Video%20with%20Particle%20Filters) [[Code](https://github.com/youssefHosni/Practical-Computer-Vision-In-Python/tree/main/Tracking%20Objects%20in%20Video%20with%20Particle%20Filters) | [Article](https://pub.towardsai.net/object-tracking-with-particle-filters-in-python-77a61bb4fd91?sk=a0be61dc1c3609cbc6d7515d306355f3)] 26 | 27 | 28 | ### 2.2. Action Recognition Projects 29 | 30 | ### 2.3. Motion Estimation Projects 31 | 32 | ## 3. Content-aware Image Editing Projects 33 | 34 | ## 4. Scene Reconstruction Projects 35 | 36 | ## 5. Generative Adversial Networks 37 | 38 | * [Build and Deploy a GAN Streamlit App on Heroku]() 39 | 40 | 41 | -------------------------------------------------------------------------------- /Tracking Objects in Video with Particle Filters/Readme.md: -------------------------------------------------------------------------------- 1 | # Tracking Objects in Video with Particle Filters In Python 2 | 3 | ## Introduction 4 | 5 | Computer vision has made rapid progress in the last few years, thanks to improvements in training data and algorithms, as well as the availability of cheap GPUs and abundant labeled training datasets. One of the primary computer vision tasks is object tracking. Object tracking is used in the vast majority of applications such as video surveillance, car tracking, people detection, and tracking, etc. We will use a particle filter to track a moving object. Particle filters are powerful and efficient solutions to problems in robotics, artificial intelligence, and even finance. 6 | 7 | ![1_QV0wBEVUua2p3f1qnnylNw](https://user-images.githubusercontent.com/72076328/204287563-c7a54fd5-dfc6-4fcc-88b4-1b45f2eb7115.gif) 8 | 9 | A particle filter is a generic algorithm for function optimization where the solution search space is searched using particles (sampling). So what does this mean? In our case, each particle incorporates tests on whether how it is likely that the object is at the position where the particle is. After the particles have been evaluated, the weights are assigned according to how good the particles are. Then the good particles are multiplied and the bad particles are removed through the re-sampling process. 10 | 11 | The next particle generation then predicts where the object might be. Then this generation is evaluated, and the cycle repeats. 12 | 13 | Opposed to the Kalman filter the particle filter can model non-linear object motion because the motion model should not be written as a state transition matrix like in the Discrete Kalman filter. Moreover, the particle filter is fairly easy to understand, but there is a negative thing: the performance of the filter depends on the particle number where the higher number of particles will lead to a better estimate, but it is more costly. Nevertheless, the particle filter is largely used for generic function optimization including object tracking. The figure below shows the two main steps of the particle filter: predict and correct. 14 | 15 | ![1_JNhxikwpmN1aMZOxFr7yUA](https://user-images.githubusercontent.com/72076328/204287766-43f94dde-0fe7-4722-8563-84c065246db5.png) 16 | 17 | The cycle of a particle filter starts with the general probability densities. First, the filter predicts the next state from the provided state transition (e.g. motion model), then if applicable, the noisy measurement information is incorporated in the correction phase and the cycle is repeated after that. 18 | 19 | ## Methodology 20 | 21 | * Loading & Displaying video frames using OpenCV 22 | * Initializing a Particle Filter 23 | * Moving Particles According to Their Velocity State 24 | * Prevent Particles from Falling Off the Edges 25 | * Measure Each Particle’s Quality 26 | * Assign Weights to the Particles 27 | * Resample Particles According to Their Weights 28 | * Fuzz the Particles 29 | 30 | ## Final Output 31 | 32 | The final output will be particle cloud surround the tracked object which in this case is the person's elbows as shown below: 33 | 34 | ![Untitled video - Made with Clipchamp](https://user-images.githubusercontent.com/72076328/204291399-7faf8c6f-e516-4fa3-b8f4-dd6e7aaa6df9.gif) 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Tracking Objects in Video with Particle Filters/Tracking Objects in Video with Particle Filters.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Tracking Objects in Video with Particle Filters" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "Computer vision has made rapid progress in the last few years, thanks to improvements in training data and algorithms, as well as the availability of cheap GPUs and abundant labeled training datasets.\n", 15 | "\n", 16 | "One of the primary computer vision tasks is object tracking. Object tracking is used in the vast majority of applications such as video surveillance, car tracking, people detection, and tracking, etc. We will use a particle filter to track a moving object. \n", 17 | "\n", 18 | "Particle filters are powerful and efficient solutions to problems in robotics, artificial intelligence, and even finance." 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "## Table of Content:\n", 26 | "1. Introduction to Particle Filter\n", 27 | "2. Loading & Displaying video frames using OpenCV\n", 28 | "3. Initializing a Particle Filter\n", 29 | "4. Moving Particles According to Their Velocity State\n", 30 | "5. Prevent Particles from Falling Off the Edges\n", 31 | "6. Measure Each Particle's Quality\n", 32 | "7. Assign Weights to the Particles\n", 33 | "8. Resample Particles According to Their Weights\n", 34 | "9. Fuzz the Particles" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "## 1. Introduction to Particle Filter\n", 42 | "A particle filter is a generic algorithm for function optimization where the solution search space is searched using particles (sampling). So what does this mean? In our case, each particle incorporates tests on whether how it is likely that the object is at the position where the particle is. After the particles have been evaluated, the weights are assigned according to how good the particles are. Then the good particles are multiplied and the bad particles are removed through the re-sampling process.\n", 43 | "\n", 44 | "The next particle generation then predicts where the object might be. Then this generation is evaluated, and the cycle repeats.\n", 45 | "Opposed to the Kalman filter the particle filter can model non-linear object motion because the motion model should not be written as a state transition matrix like in the Discrete Kalman filter. \n", 46 | "\n", 47 | "Moreover, the particle filter is fairly easy to understand, but there is a negative thing: the performance of the filter depends on the particle number where the higher number of particles will lead to a better estimate, but it is more costly. Nevertheless, the particle filter is largely used for generic function optimization including object tracking. The figure below shows the two main steps of the particle filter: predict and correct." 48 | ] 49 | }, 50 | { 51 | "attachments": { 52 | "1_JNhxikwpmN1aMZOxFr7yUA.png": { 53 | "image/png": "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" 54 | } 55 | }, 56 | "cell_type": "markdown", 57 | "metadata": {}, 58 | "source": [ 59 | "![1_JNhxikwpmN1aMZOxFr7yUA.png](attachment:1_JNhxikwpmN1aMZOxFr7yUA.png)" 60 | ] 61 | }, 62 | { 63 | "cell_type": "markdown", 64 | "metadata": {}, 65 | "source": [ 66 | "The cycle of a particle filter starts with the general probability densities. First, the filter predicts the next state from the provided state transition (e.g. motion model), then if applicable, the noisy measurement information is incorporated in the correction phase and the cycle is repeated after that." 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "metadata": {}, 72 | "source": [ 73 | "## 1. Load video frames from file" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "Let's first import the NumPy math library and the OpenCV computer vision library as they will be used extensively throughout this project. Then we set the random seed so that every time we run the code, we get consistent results." 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": 1, 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "import numpy as np\n", 90 | "import cv2\n", 91 | "from IPython.display import clear_output, Image, display, HTML\n", 92 | "import ipywidgets as widgets\n", 93 | "import threading\n", 94 | "import numpy as np\n", 95 | "\n", 96 | "# Repeatability\n", 97 | "np.random.seed(0)" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 2, 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "VFILENAME = \"Man.mp4\"\n", 107 | "\n", 108 | "frame_height = 720\n", 109 | "frame_width = 1280\n" 110 | ] 111 | }, 112 | { 113 | "cell_type": "markdown", 114 | "metadata": {}, 115 | "source": [ 116 | "We can now load the video frames and display them using OpenCV using the code below:" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": null, 122 | "metadata": {}, 123 | "outputs": [], 124 | "source": [ 125 | "import cv2\n", 126 | "import numpy as np\n", 127 | "\n", 128 | "\n", 129 | "# Create a VideoCapture object and read from input file\n", 130 | "# If the input is the camera, pass 0 instead of the video file name\n", 131 | "cap = cv2.VideoCapture('walking.mp4')\n", 132 | "\n", 133 | "# Check if camera opened successfully\n", 134 | "if (cap.isOpened()== False): \n", 135 | " print(\"Error opening video stream or file\")\n", 136 | " \n", 137 | "# Read until video is completed\n", 138 | "while(cap.isOpened()):\n", 139 | " # Capture frame-by-frame\n", 140 | " ret, frame = cap.read()\n", 141 | "\n", 142 | " if ret == True:\n", 143 | " # Display the resulting frame\n", 144 | " cv2.imshow('Frame',frame)\n", 145 | " \n", 146 | " # Press Q on keyboard to exit\n", 147 | " if cv2.waitKey(30) & 0xFF == ord('q'):\n", 148 | " break\n", 149 | " \n", 150 | " # Break the loop\n", 151 | " else: \n", 152 | " break\n", 153 | " \n", 154 | "# When everything done, release the video capture object\n", 155 | "cap.release()\n", 156 | " \n", 157 | "# Closes all the frames\n", 158 | "cv2.destroyAllWindows()" 159 | ] 160 | }, 161 | { 162 | "cell_type": "markdown", 163 | "metadata": {}, 164 | "source": [ 165 | "In the code above we first call a function from OpenCV called VideoCapture which will return a video capture object. And while the video is opened, we want to read the frames. So we’re going to call the read method of this video object, and it’s going to return, like, a status flag and then the frame, which is actually an array of pixel color values.\n", 166 | "\n", 167 | "As long as this flag is valid, we’re going to yield the frame. So this turns that the function into a Python generator that we’re going to call over and over again, and it’s going to keep looping and yielding frames. And if the status is not good, we break out of the loop and release the resources and yield None. Then we will display each frame consecutively for 3o ms and the loop will break if the frame has finished or if you press the q key.\n", 168 | "\n" 169 | ] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "metadata": {}, 174 | "source": [ 175 | "Since we will be using the frame values in different functions as will be seen shortly, it is better to create a function that takes the video path and return the value for each frame:\n", 176 | "\n" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": 3, 182 | "metadata": {}, 183 | "outputs": [], 184 | "source": [ 185 | "def get_frames(filename):\n", 186 | " video = cv2.VideoCapture(filename)\n", 187 | " while video.isOpened():\n", 188 | " ret, frame = video.read()\n", 189 | " if ret:\n", 190 | " yield frame \n", 191 | " else:\n", 192 | " break\n", 193 | " video.release() \n", 194 | " yield None" 195 | ] 196 | }, 197 | { 198 | "cell_type": "markdown", 199 | "metadata": {}, 200 | "source": [ 201 | "Finally, let's create a display function to display the particles and the tracked particle. So the first thing we want to do is check if there are any particles to display. And if so, we’re going to iterate through them and we’re going to use their values as pixel coordinates. So if you want to do that, we’ll have to actually cast them to an integer type. Next, we draw these particles as a tiny circle on top of our video frame. We can use OpenCV’s circle function, hand it the video frame, and the circle center and the radius of one, and then we’ll set the color.\n", 202 | "\n", 203 | "Every time we use OpenCV, the color convention is BGR instead of RGB. So we’ll go with green this time and set a really small curve thickness of one pixel. So that takes care of the particles.\n", 204 | "\n", 205 | "Next, if we get a location for the particle we are trying to track throughout the video. We want to draw a nice circle there. So the location is actually going to be an (x,y) tuple by itself so we can use it directly. And we’ll go with a slightly larger circle, 15 pixels, and go with red this time and a curve thickness of 5 pixels. So next we want to actually display the video frame. There’s an image show function from OpenCV.\n", 206 | "\n" 207 | ] 208 | }, 209 | { 210 | "cell_type": "code", 211 | "execution_count": 4, 212 | "metadata": {}, 213 | "outputs": [], 214 | "source": [ 215 | "def display(frame, particles, location):\n", 216 | " if len(particles)> 0:\n", 217 | " for i in range(NUM_PARTICLES):\n", 218 | " x = int(particles[i,0])\n", 219 | " y = int(particles[i,1])\n", 220 | " cv2.circle(frame, (x,y), 1 , (0,255,0) ,1)\n", 221 | " \n", 222 | " if len(location) > 0:\n", 223 | " cv2.circle(frame,location,15,(0,0,255),5)\n", 224 | " \n", 225 | " cv2.imshow('frame', frame)\n", 226 | " #stop the video if pressing q button\n", 227 | " if cv2.waitKey(30) & 0xFF == ord('q'):\n", 228 | " return True \n", 229 | "\n", 230 | " return False" 231 | ] 232 | }, 233 | { 234 | "cell_type": "markdown", 235 | "metadata": {}, 236 | "source": [ 237 | "### 3. Initializing a Particle Filter" 238 | ] 239 | }, 240 | { 241 | "cell_type": "markdown", 242 | "metadata": {}, 243 | "source": [ 244 | "To initialize the particles that we will show at the top of the video we need to estimate the state of the target, meaning its position and velocity within the video frames. At the beginning of the video, we don’t know that state. All we know is that the position should lie within the frame somewhere and the velocity could be in any direction but not moving too, too fast.\n", 245 | "\n", 246 | "We are going to express our estimate of the target state with a set of particles. Each particle has its own position and velocity. Since we have no information yet about the target, our particles will be scattered uniformly in the frame and they’ll have random velocities.\n", 247 | "\n", 248 | "We can start by initializing the number of particles to 5000 particles and the initial velocity range to be a pixel per frame. Then we will create the particle array. It will be filled with random numbers it will have one row per particle and four columns. The first two will be the coordinates of the particles and the last two will be their velocity. The first two columns will have values between zero and the length and the height of the frame.\n", 249 | "\n", 250 | "The velocity will take the initialized value of 0.5 and we will center it to zero so as to have the possibility to move in both directions. We’re going to decrement that by half the velocity range. And what that will do is shift the velocities down so that everything is centered around zero. You’ll notice that the particles are spread out uniformly at random. Each row here represents one particle and the first two columns that are in the range of hundreds, here they are the x and y position, and then we have the velocity components, which are smaller than one.\n", 251 | "\n" 252 | ] 253 | }, 254 | { 255 | "cell_type": "code", 256 | "execution_count": 23, 257 | "metadata": {}, 258 | "outputs": [], 259 | "source": [ 260 | "NUM_PARTICLES = 500\n", 261 | "VEL_RANGE = 0.5\n", 262 | "\n", 263 | "def initialize_particles():\n", 264 | " particles = np.random.rand(NUM_PARTICLES,4)\n", 265 | " particles = particles * np.array((frame_width,frame_height, VEL_RANGE,VEL_RANGE))\n", 266 | " particles[:,2:4] -= VEL_RANGE/2.0\n", 267 | " return particles" 268 | ] 269 | }, 270 | { 271 | "cell_type": "markdown", 272 | "metadata": {}, 273 | "source": [ 274 | "We will display the video using the code below:" 275 | ] 276 | }, 277 | { 278 | "cell_type": "code", 279 | "execution_count": 7, 280 | "metadata": {}, 281 | "outputs": [], 282 | "source": [ 283 | "location =[]\n", 284 | "particles = initialize_particles()\n", 285 | "\n", 286 | "\n", 287 | "for frame in get_frames(VFILENAME):\n", 288 | " if frame is None: break\n", 289 | " terminate = display(frame, particles, location)\n", 290 | " if terminate:\n", 291 | " break\n", 292 | "\n", 293 | "cv2.destroyAllWindows() " 294 | ] 295 | }, 296 | { 297 | "cell_type": "markdown", 298 | "metadata": {}, 299 | "source": [ 300 | "## 4. Moving Particles According to Their Velocity State\n" 301 | ] 302 | }, 303 | { 304 | "cell_type": "markdown", 305 | "metadata": {}, 306 | "source": [ 307 | "You might have noticed during video playback that the particles were not moving, even though they have a velocity. The reason for that is we are not updating their position according to the velocities.\n", 308 | "\n", 309 | "We can solve this with the code below. We are going to increment the particle's x-coordinates with the x-direction velocity component that’s in the third column ( index 2). And we do something similar for the y. So let’s run the code that will prevent particles from falling off the edge of the video frame and see if it worked.\n", 310 | "\n" 311 | ] 312 | }, 313 | { 314 | "cell_type": "code", 315 | "execution_count": 7, 316 | "metadata": {}, 317 | "outputs": [], 318 | "source": [ 319 | "def apply_velocity(particles):\n", 320 | " particles[:,0] += particles[:,2]\n", 321 | " particles[:,1] += particles[:,3]\n", 322 | "\n", 323 | " return particles" 324 | ] 325 | }, 326 | { 327 | "cell_type": "markdown", 328 | "metadata": {}, 329 | "source": [ 330 | "Now you see are particles are moving according to their velocity using the code below:\n" 331 | ] 332 | }, 333 | { 334 | "cell_type": "code", 335 | "execution_count": 11, 336 | "metadata": {}, 337 | "outputs": [], 338 | "source": [ 339 | "location = []\n", 340 | "particles = initialize_particles()\n", 341 | "\n", 342 | "\n", 343 | "for frame in get_frames(VFILENAME):\n", 344 | " if frame is None: break\n", 345 | " particles = apply_velocity(particles)\n", 346 | "\n", 347 | " terminate = display(frame, particles, location)\n", 348 | " if terminate:\n", 349 | " break\n", 350 | "\n", 351 | "cv2.destroyAllWindows() " 352 | ] 353 | }, 354 | { 355 | "cell_type": "markdown", 356 | "metadata": {}, 357 | "source": [ 358 | "## 5. Prevent Particles from Falling Off the Edges" 359 | ] 360 | }, 361 | { 362 | "cell_type": "markdown", 363 | "metadata": {}, 364 | "source": [ 365 | "Next, we will add some logic to keep them from falling off the edge of the frame and also decide which particles are more relevant than others. We will do this by putting a limit on the particles’ (x,y) coordinates. To do so we will loop over all the particles and add upper and lower boundaries on both x and y coordinates. The new value for the x coordinate will be the minimum of Width— 1 and the current value. For the y coordinate will be the minimum of Height— 1 and the current value.\n", 366 | "\n", 367 | "By doing this we will prevent the x value from getting greater than WIDTH — 1 and we subtract one as the frame coordinates are zero-based. So if you had a frame with 100 pixels width, you would want the coordinate to go from 0 to 99. Then we are going to place a lower bound on that value. We’ll take the maximum of zero and the upper bounded value. So this will prevent x and y from going below zero.\n", 368 | "\n" 369 | ] 370 | }, 371 | { 372 | "cell_type": "code", 373 | "execution_count": 8, 374 | "metadata": {}, 375 | "outputs": [], 376 | "source": [ 377 | "def enforce_edges(particles):\n", 378 | " for i in range(NUM_PARTICLES):\n", 379 | " particles[i,0] = max(0,min(frame_width-1, particles[i,0]))\n", 380 | " particles[i,1] = max(0,min(frame_height-1, particles[i,1]))\n", 381 | " return particles" 382 | ] 383 | }, 384 | { 385 | "cell_type": "markdown", 386 | "metadata": {}, 387 | "source": [ 388 | "We will display the video using the code below:" 389 | ] 390 | }, 391 | { 392 | "cell_type": "code", 393 | "execution_count": 13, 394 | "metadata": {}, 395 | "outputs": [], 396 | "source": [ 397 | "location = []\n", 398 | "particles = initialize_particles()\n", 399 | "\n", 400 | "\n", 401 | "for frame in get_frames(VFILENAME):\n", 402 | " if frame is None: break\n", 403 | " particles = apply_velocity(particles)\n", 404 | " particles = enforce_edges(particles)\n", 405 | " terminate = display(frame, particles, location)\n", 406 | " if terminate:\n", 407 | " break\n", 408 | "\n", 409 | "cv2.destroyAllWindows() " 410 | ] 411 | }, 412 | { 413 | "cell_type": "markdown", 414 | "metadata": {}, 415 | "source": [ 416 | "# 6. Measure Each Particle’s Quality" 417 | ] 418 | }, 419 | { 420 | "cell_type": "markdown", 421 | "metadata": {}, 422 | "source": [ 423 | "In order to improve our state estimation, we want to check the color of the pixel sitting under each particle and compare it to the target color. So now, to get the precise values of that target color, we can pause the video, take a screenshot and open it in gimp or other similar software then with the eyedropper tool we can find the BGR values for some pixels.\n", 424 | "\n", 425 | "In this case, we would like to track the elbow of the person. So if we check it you will find it to be 105,63, 66. You can choose another pixel that can be kind of representative of the whole target. Not too bright, not too dark.\n", 426 | "\n", 427 | "We will create a NumPy array to store those color differences and call them errors, and we’ll fill it with zeros to start with. Then we are going to loop through all of the particles and we want to use their x, and y positions as pixel coordinates. Since we need those to be integers we are going to cast the X and Y positions. So to pull out the pixel values at that position, it’s all about NumPy indexing. Here we index the video frame. So the frames are stored by row and column. So we use y and then x, and then we pull out all three-pixel values.\n", 428 | "Finally, we want a single value to represent the color difference at that pixel. One way to do so is to take the mean square difference between the two colors.\n", 429 | "\n" 430 | ] 431 | }, 432 | { 433 | "cell_type": "code", 434 | "execution_count": 9, 435 | "metadata": {}, 436 | "outputs": [], 437 | "source": [ 438 | "TARGET_COLOR = np.array((66,63, 105))\n", 439 | "\n", 440 | "def compute_errors(particles, frame):\n", 441 | " \n", 442 | " errors = np.zeros(NUM_PARTICLES)\n", 443 | " for i in range(NUM_PARTICLES):\n", 444 | " x = int(particles[i,0])\n", 445 | " y= int(particles[i,1])\n", 446 | " pixel_color = frame[y, x, :]\n", 447 | " errors[i] = np.sum((TARGET_COLOR - pixel_color)**2)\n", 448 | " \n", 449 | " return errors" 450 | ] 451 | }, 452 | { 453 | "cell_type": "markdown", 454 | "metadata": {}, 455 | "source": [ 456 | "# 7. Assign Weights to the Particles" 457 | ] 458 | }, 459 | { 460 | "cell_type": "markdown", 461 | "metadata": {}, 462 | "source": [ 463 | "In this section, we will compute particle weights and re-sample the particle filter. We will use the errors we calculated to compute a weight for each particle. When the error is low, we want the weight to be high. This means that a particle is at a location where the pixel color is a good match for the target.\n", 464 | "\n", 465 | "The simplest thing we can do is invert the errors, In this sense: if we take the highest error that was found and then we subtract off the errors array, this will be done element-wise, and the weights array will then have as many elements as the errors array have. Next, we want to prevent the particles from piling up along the edge. So we’d like to set the weight for particles on the edge to zero.\n", 466 | "\n", 467 | "We can set a condition, x is equal to zero. That’s along the left-hand edge of the frame. We can do a logical OR and set the condition where, if the x particle value is equal to WIDTH — 1. So this would be the right edge of the frame, and we do the same thing for y, so y will be zero at the top edge of the frame, and at the bottom edge, it will be equal to HEIGHT-1.\n", 468 | "\n" 469 | ] 470 | }, 471 | { 472 | "cell_type": "code", 473 | "execution_count": 21, 474 | "metadata": {}, 475 | "outputs": [], 476 | "source": [ 477 | "def compute_weights(errors):\n", 478 | " weights = np.max(errors) - errors\n", 479 | " \n", 480 | " weights[\n", 481 | " (particles[:,0]==0) |\n", 482 | " (particles[:,0]==frame_width-1) |\n", 483 | " (particles[:,1]==0) |\n", 484 | " (particles[:,1]==frame_height-1) ] = 0 \n", 485 | " \n", 486 | " weights = weights**8\n", 487 | " \n", 488 | " return weights" 489 | ] 490 | }, 491 | { 492 | "cell_type": "markdown", 493 | "metadata": {}, 494 | "source": [ 495 | "# 8. Resample Particles According to Their Weights\n" 496 | ] 497 | }, 498 | { 499 | "cell_type": "markdown", 500 | "metadata": {}, 501 | "source": [ 502 | "Now we’re going to do something really cool with these weights. If we normalize them so that they sum to one, we can use them as a probability distribution over the particles. So in other words, we’re going to build a new particle array by sampling from the current particles. The ones that have a high weight will get chosen many times and those with a low weight may not be chosen at all.\n", 503 | "\n", 504 | "\n", 505 | "NumPy has a very useful function for this kind of re-sampling. We’re going to call the choice function.\n", 506 | "\n", 507 | "* The first argument for this function is the sampling range. So if we just give it a single integer, it’s clever enough to know that we want to re-sample in the range from zero to that maximum value.\n", 508 | "* The second argument we need to pass the function is how many samples to take. Since we’re replacing the whole particle array, we need as many samples as we have particles.\n", 509 | "* Finally, we just tell it the probability distribution.\n", 510 | "\n", 511 | "So once we’re done with this re-sampling, we have our new index numbers that are pointing to the current particles we’ve sampled. Then we just rebuild a particle array according to these index numbers. Finally, it would be great if we could come up with a single x,y position, which is our best guess for where is the target particle.\n", 512 | "\n", 513 | "So far, we’ve expressed our state estimation using all the particles. But we would like to have a single best guess. This can be done easily by just taking the average x and y positions over all the particles that we have. We will return the particles array and we would like to return a tuple with the x and y cast to integers. So this will allow us to use this tuple directly to an index\n", 514 | "as pixel coordinates.\n", 515 | "\n" 516 | ] 517 | }, 518 | { 519 | "cell_type": "code", 520 | "execution_count": 11, 521 | "metadata": {}, 522 | "outputs": [], 523 | "source": [ 524 | "def resample(particles, weights):\n", 525 | " probabilities = weights / np.sum(weights)\n", 526 | " index_numbers = np.random.choice(\n", 527 | " NUM_PARTICLES,\n", 528 | " size=NUM_PARTICLES,\n", 529 | " p=probabilities)\n", 530 | " particles = particles[index_numbers, :]\n", 531 | " \n", 532 | " x = np.mean(particles[:,0])\n", 533 | " y= np.mean(particles[:,1])\n", 534 | " return particles, [int(x), int(y)]" 535 | ] 536 | }, 537 | { 538 | "cell_type": "markdown", 539 | "metadata": {}, 540 | "source": [ 541 | "As you can see at the end of the video below generated by the code below, it looks like the particle cloud collapsed onto one single pixel and stopped moving.\n", 542 | "\n" 543 | ] 544 | }, 545 | { 546 | "cell_type": "markdown", 547 | "metadata": {}, 548 | "source": [ 549 | "The code also gives the following error: Probabilities contain NaN, which stands for Not a Number. So basically the program crashed.\n", 550 | "\n" 551 | ] 552 | }, 553 | { 554 | "cell_type": "code", 555 | "execution_count": 13, 556 | "metadata": {}, 557 | "outputs": [ 558 | { 559 | "name": "stderr", 560 | "output_type": "stream", 561 | "text": [ 562 | "C:\\Users\\youss\\anaconda3\\envs\\new_enviroment\\lib\\site-packages\\ipykernel_launcher.py:2: RuntimeWarning: invalid value encountered in true_divide\n", 563 | " \n" 564 | ] 565 | }, 566 | { 567 | "ename": "ValueError", 568 | "evalue": "probabilities contain NaN", 569 | "output_type": "error", 570 | "traceback": [ 571 | "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", 572 | "\u001b[1;31mValueError\u001b[0m Traceback (most recent call last)", 573 | "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 8\u001b[0m \u001b[0merrors\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mcompute_errors\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mparticles\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mframe\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 9\u001b[0m \u001b[0mweights\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mcompute_weights\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0merrors\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 10\u001b[1;33m \u001b[0mparticles\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mlocation\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mresample\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mparticles\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mweights\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 11\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 12\u001b[0m \u001b[0mterminate\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mdisplay\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mframe\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mparticles\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mlocation\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", 574 | "\u001b[1;32m\u001b[0m in \u001b[0;36mresample\u001b[1;34m(particles, weights)\u001b[0m\n\u001b[0;32m 4\u001b[0m \u001b[0mNUM_PARTICLES\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 5\u001b[0m \u001b[0msize\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mNUM_PARTICLES\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 6\u001b[1;33m p=probabilities)\n\u001b[0m\u001b[0;32m 7\u001b[0m \u001b[0mparticles\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mparticles\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mindex_numbers\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m:\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 8\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", 575 | "\u001b[1;32mmtrand.pyx\u001b[0m in \u001b[0;36mnumpy.random.mtrand.RandomState.choice\u001b[1;34m()\u001b[0m\n", 576 | "\u001b[1;31mValueError\u001b[0m: probabilities contain NaN" 577 | ] 578 | } 579 | ], 580 | "source": [ 581 | "particles = initialize_particles()\n", 582 | "\n", 583 | "\n", 584 | "for frame in get_frames(VFILENAME):\n", 585 | " if frame is None: break\n", 586 | " particles = apply_velocity(particles)\n", 587 | " particles = enforce_edges(particles)\n", 588 | " errors = compute_errors(particles, frame)\n", 589 | " weights = compute_weights(errors)\n", 590 | " particles, location = resample(particles, weights)\n", 591 | "\n", 592 | " terminate = display(frame, particles, location)\n", 593 | " if terminate:\n", 594 | " break\n", 595 | "\n", 596 | "cv2.destroyAllWindows() " 597 | ] 598 | }, 599 | { 600 | "cell_type": "markdown", 601 | "metadata": {}, 602 | "source": [ 603 | "So what happened is when all the particles were sitting on top of the same pixel, they all had the same error. And because of the way that we calculated the weights, the weights are all zero and then we divided them by the sum of all the weights. So division by zero is not a good idea and we should fix it.\n", 604 | "\n" 605 | ] 606 | }, 607 | { 608 | "cell_type": "markdown", 609 | "metadata": {}, 610 | "source": [ 611 | "# 9. Fuzz the Particles\n" 612 | ] 613 | }, 614 | { 615 | "cell_type": "markdown", 616 | "metadata": {}, 617 | "source": [ 618 | "In the previous task, our particle filter did a great job of converging on one pixel. But it wasn’t quite a pixel on the target. We need it to locate the target and keep tracking the target, even if it moves around the frame or the lighting conditions change. The solution for this is to just add noise. In a particle filter, we can use noise to express our uncertainty about the target state.\n", 619 | "\n", 620 | "We will generate some Gaussian noise and add it to each particle. If the target changes in the next frame, some of the particles will have changed in the same way, thanks to the variations from the noise we added, so they will move along with the target. The other particles that did not move with the target will have more color errors and won’t get re-sampled.\n", 621 | "Here is the code that will do this:" 622 | ] 623 | }, 624 | { 625 | "cell_type": "code", 626 | "execution_count": 12, 627 | "metadata": {}, 628 | "outputs": [], 629 | "source": [ 630 | "POS_SIGMA = 0.75\n", 631 | "VEL_SIGMA = 0.1\n", 632 | "\n", 633 | "def apply_noise(particles):\n", 634 | " noise= np.concatenate(\n", 635 | " (\n", 636 | " np.random.normal(0.0, POS_SIGMA, (NUM_PARTICLES,1)),\n", 637 | " np.random.normal(0.0, POS_SIGMA, (NUM_PARTICLES,1)),\n", 638 | " np.random.normal(0.0, VEL_SIGMA, (NUM_PARTICLES,1)),\n", 639 | " np.random.normal(0.0, VEL_SIGMA, (NUM_PARTICLES,1))\n", 640 | " \n", 641 | " ),\n", 642 | " axis=1)\n", 643 | " \n", 644 | " particles += noise\n", 645 | " return particles" 646 | ] 647 | }, 648 | { 649 | "cell_type": "markdown", 650 | "metadata": {}, 651 | "source": [ 652 | "We will be using Gaussian noise, so we’re going to specify the standard deviations for the position. We can go with a standard deviation of one pixel and for velocity, maybe half a pixel per frame. So next we create the noise: we’re going to create one column at a time, and then we’ll concatenate all the columns into one array. The output video generated by the code below is shown below:" 653 | ] 654 | }, 655 | { 656 | "cell_type": "code", 657 | "execution_count": 26, 658 | "metadata": {}, 659 | "outputs": [], 660 | "source": [ 661 | "particles = initialize_particles()\n", 662 | "\n", 663 | "for frame in get_frames(VFILENAME):\n", 664 | " if frame is None: break\n", 665 | "\n", 666 | " particles = apply_velocity(particles)\n", 667 | " particles = enforce_edges(particles)\n", 668 | " errors = compute_errors(particles, frame)\n", 669 | " weights = compute_weights(errors)\n", 670 | " particles, location = resample(particles, weights)\n", 671 | " particles = apply_noise(particles)\n", 672 | " terminate = display(frame, particles, location)\n", 673 | " if terminate:\n", 674 | " break\n", 675 | "cv2.destroyAllWindows()" 676 | ] 677 | }, 678 | { 679 | "cell_type": "markdown", 680 | "metadata": {}, 681 | "source": [ 682 | "It looks like the particle cloud is distributed along different subjects and not drawn to the target. It turns out that just tweaking the parameters did not really improve the behavior of the particle filter; changing the standard deviations or the number of particles. What we’d really like to do is to make the weights more sensitive to color difference so that only the ones on top of the target get resampled. One possible solution is to square the weights since when we square numbers that are larger than one, the largest ones will increase by more than the smaller ones do. So the larger weights get even larger, and those particles will get re-sampled even more so this could make the weights more sensitive to color. Let’s run the code again after squaring the weight and see the output below:\n", 683 | "\n" 684 | ] 685 | }, 686 | { 687 | "cell_type": "code", 688 | "execution_count": null, 689 | "metadata": {}, 690 | "outputs": [], 691 | "source": [ 692 | "def compute_weights(errors):\n", 693 | " weights = np.max(errors) - errors\n", 694 | " \n", 695 | " weights[\n", 696 | " (particles[:,0]==0) |\n", 697 | " (particles[:,0]==frame_width-1) |\n", 698 | " (particles[:,1]==0) |\n", 699 | " (particles[:,1]==frame_height-1) ] = 0 \n", 700 | " \n", 701 | " weights = weights**2\n", 702 | " \n", 703 | " return weights" 704 | ] 705 | }, 706 | { 707 | "cell_type": "markdown", 708 | "metadata": {}, 709 | "source": [ 710 | "As you can see the particles are more attracted to the t-shirt and the bricks and it seems that it cannot differentiate the skin so we can increase the power of the weights to 8.\n", 711 | "\n" 712 | ] 713 | }, 714 | { 715 | "cell_type": "code", 716 | "execution_count": null, 717 | "metadata": {}, 718 | "outputs": [], 719 | "source": [ 720 | "def compute_weights(errors):\n", 721 | " weights = np.max(errors) - errors\n", 722 | " \n", 723 | " weights[\n", 724 | " (particles[:,0]==0) |\n", 725 | " (particles[:,0]==frame_width-1) |\n", 726 | " (particles[:,1]==0) |\n", 727 | " (particles[:,1]==frame_height-1) ] = 0 \n", 728 | " \n", 729 | " weights = weights**8\n", 730 | " \n", 731 | " return weights" 732 | ] 733 | }, 734 | { 735 | "cell_type": "markdown", 736 | "metadata": {}, 737 | "source": [ 738 | "As you can see from the video it is now much better but there’s still some spreading of the particle cloud and it takes a long time to go to the target, but it has locked onto the target and it stays locked on. So what if we raise the weights to a higher power like 16?\n", 739 | "\n" 740 | ] 741 | }, 742 | { 743 | "cell_type": "code", 744 | "execution_count": null, 745 | "metadata": {}, 746 | "outputs": [], 747 | "source": [ 748 | "def compute_weights(errors):\n", 749 | " weights = np.max(errors) - errors\n", 750 | " \n", 751 | " weights[\n", 752 | " (particles[:,0]==0) |\n", 753 | " (particles[:,0]==frame_width-1) |\n", 754 | " (particles[:,1]==0) |\n", 755 | " (particles[:,1]==frame_height-1) ] = 0 \n", 756 | " \n", 757 | " weights = weights**16\n", 758 | " \n", 759 | " return weights" 760 | ] 761 | }, 762 | { 763 | "cell_type": "markdown", 764 | "metadata": {}, 765 | "source": [ 766 | "Now it works even quicker and better. So we don’t want to increase it more. If the weights become too sensitive to color, then the filter may have problems if the lighting conditions change a bit and it can no longer find a very close match to the original target color. This is pretty good as it is now. You can try also to increase the power of the weights to 32 and sees what happens.\n", 767 | "\n" 768 | ] 769 | }, 770 | { 771 | "cell_type": "code", 772 | "execution_count": null, 773 | "metadata": {}, 774 | "outputs": [], 775 | "source": [] 776 | } 777 | ], 778 | "metadata": { 779 | "kernelspec": { 780 | "display_name": "Python 3", 781 | "language": "python", 782 | "name": "python3" 783 | }, 784 | "language_info": { 785 | "codemirror_mode": { 786 | "name": "ipython", 787 | "version": 3 788 | }, 789 | "file_extension": ".py", 790 | "mimetype": "text/x-python", 791 | "name": "python", 792 | "nbconvert_exporter": "python", 793 | "pygments_lexer": "ipython3", 794 | "version": "3.7.9" 795 | } 796 | }, 797 | "nbformat": 4, 798 | "nbformat_minor": 4 799 | } 800 | -------------------------------------------------------------------------------- /Tracking Objects in Video with Particle Filters/walking.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youssefHosni/Practical-Computer-Vision-In-Python/cb72d148da8d4d47be06c9e244e9b5e4c211ed25/Tracking Objects in Video with Particle Filters/walking.mp4 -------------------------------------------------------------------------------- /Video Analysis/Readme.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /image-classification/readme.md: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------