from scipy.ndimage import gaussian_filter
from scipy.signal import convolve2d
from tqdm.notebook import tqdm
import numpy as np
import imageio
import cv2
Lucas Kanade Optical Flow
Optical Flow
Optical flow is the pattern of apparent motion of image objects between two consecutive frames caused by the movement of object or camera. It is vector field where each vector denotes the movement of points from first frame to second. Generally, optical flow is used to track the motion of objects in a video. Lets assume you have captured two frames with a small time difference \(\Delta t\). You wish to track the motion of a pixel located at \((x, y)\) in first frame. In the second frame, the pixel has moved to \((x + \Delta x, y + \Delta y)\). The vector \((\Delta x, \Delta y)\) is the optical flow vector.
\[I(x, y, t) = I(x + \Delta x, y + \Delta y, t + \Delta t)\]
The above equation is the basic assumption in optical flow. It assumes that intensity of an object does not change between two consecutive frames. This constraint is called brightness constancy constraint. This equation can be expanded using taylor series to get the optical flow equation.
\[I(x + \Delta x, y + \Delta y, t + \Delta t) \approx I(x, y, t) + \frac{\partial I}{\partial x} \Delta x + \frac{\partial I}{\partial y} \Delta y + \frac{\partial I}{\partial t} \Delta t = I(x, y, t)\]
\[\frac{\partial I}{\partial x} \Delta x + \frac{\partial I}{\partial y} \Delta y + \frac{\partial I}{\partial t} \Delta t = 0\]
\[\frac{\partial I}{\partial x} u + \frac{\partial I}{\partial y} v + \frac{\partial I}{\partial t} = 0\]
where \(u = \frac{\Delta x}{\Delta t}\) and \(v = \frac{\Delta y}{\Delta t}\) are the optical flow velocities in x and y directions respectively.
The above equation is called optical flow equation. It is a single equation with two unknowns \(u\) and \(v\). Hence, it is an ill-posed problem. To solve this problem, we need to make some more assumptions. One of the assumption to arrive here was brightness constancy assumption. It assumes that the intensity of an object does not change between two consecutive frames. The other assumption made was given by lucas and kanade. They assumed that the optical flow is same for all the pixels in a neighborhood. This assumption is called spatial coherence assumption.
Lucas Kanade Optical Flow
The Lucas-Kanade method is a widely used differential method for optical flow estimation developed by Bruce D. Lucas and Takeo Kanade. It assumes that the flow is essentially constant in a local neighbourhood of the pixel under consideration, and solves the basic optical flow equations for all the pixels in that neighbourhood by the least squares criterion.
\[\frac{\partial I}{\partial x} u + \frac{\partial I}{\partial y} v + \frac{\partial I}{\partial t} = 0\]
\[E_x = \frac{\partial I}{\partial x}, E_y = \frac{\partial I}{\partial y}, E_t = \frac{\partial I}{\partial t}\]
\[E_x u + E_y v + E_t = 0\]
\[\begin{bmatrix} E_x & E_y \end{bmatrix} \begin{bmatrix} u \\ v \end{bmatrix} = -E_t\]
\[ \begin{bmatrix} E_x(i-1,j-1) & E_y(i-1,j-1) \\ E_x(i-1,j) & E_y(i-1,j) \\ E_x(i-1,j+1) & E_y(i-1,j+1) \\ E_x(i,j-1) & E_y(i,j-1) \\ E_x(i,j) & E_y(i,j) \\ E_x(i,j+1) & E_y(i,j+1) \\ E_x(i+1,j-1) & E_y(i+1,j-1) \\ E_x(i+1,j) & E_y(i+1,j) \\ E_x(i+1,j+1) & E_y(i+1,j+1) \end{bmatrix} \begin{bmatrix} u \\ v \end{bmatrix} = \begin{bmatrix} -E_t(i-1,j-1) \\ -E_t(i-1,j) \\ -E_t(i-1,j+1) \\ -E_t(i,j-1) \\ -E_t(i,j) \\ -E_t(i,j+1) \\ -E_t(i+1,j-1) \\ -E_t(i+1,j) \\ -E_t(i+1,j+1) \end{bmatrix}\]
\[A_{(9,2)} x_{(2,1)} = b_{(9,1)}\]
\[A \overrightarrow{x} = \overrightarrow{b}\]
\[x = (A^T A)^{-1} A^T b\]
So for a chosen patch we can estimate the optical flow using above equation. This could also be written as
\[\begin{bmatrix} u \\ v \end{bmatrix} = \begin{bmatrix} \sum E_x E_x & \sum E_x E_y \\ \sum E_y E_x & \sum E_y E_y \end{bmatrix}^{-1} \begin{bmatrix} -\sum E_x E_t \\ -\sum E_y E_t \end{bmatrix}\]
This is the final equation used to estimate the optical flow using Lucas Kanade method. The above equation is solved for each patch in the image to get the optical flow vectors. Optical flow algorithm could be summarized as follows:
- Compute image gradients \(E_x\) and \(E_y\).
- Compute temporal gradient \(E_t\).
- Compute the matrix \(A\) and vector \(b\) for each patch.
- Solve the equation \(A \overrightarrow{x} = \overrightarrow{b}\) to get the optical flow vectors.
Lucas Kanade Implementation
UNDER CONSTRUCTION !!
def compute_derivatives(frame1, frame2):
"""
Compute spatial and temporal derivatives between two consecutive frames for optical flow estimation.
This function calculates the following derivatives:
- Ix: Spatial derivative in x direction (horizontal)
- Iy: Spatial derivative in y direction (vertical)
- It: Temporal derivative between frames
The spatial derivatives are computed using Sobel operators on the average of both frames.
The temporal derivative is computed as the difference between frames.
Args:
frame1 (numpy.ndarray): First frame (earlier time point)
frame2 (numpy.ndarray): Second frame (later time point)
Returns:
tuple: A tuple containing three numpy.ndarray:
- Ix: Spatial derivative in x direction
- Iy: Spatial derivative in y direction
- It: Temporal derivative
Note:
Both input frames should have the same dimensions and be in grayscale format.
"""
# Define Sobel operator for x direction (horizontal)
= np.array([[1, 0, -1],
kernel_x 2, 0, -2],
[1, 0, -1]]) /8.0
[
# Create y direction kernel by transposing x direction kernel
= kernel_x.T
kernel_y
# Calculate average frame to reduce noise in derivative estimation
= (frame1 + frame2)//2
calc_frame
# Compute spatial derivatives using convolution with Sobel operators
# 'same' mode maintains original dimensions, 'symm' handles boundaries by reflection
= convolve2d(calc_frame, kernel_x, mode='same', boundary='symm')
Ix = convolve2d(calc_frame, kernel_y, mode='same', boundary='symm')
Iy
# Compute temporal derivative as difference between frames
# Convert to float to avoid integer overflow
= frame2.astype(float) - frame1.astype(float)
It
return Ix, Iy, It
def lk_flow(frame1, frame2, window_size=16, tau=1e-2):
"""
Compute optical flow using the Lucas-Kanade method with improved robustness features.
This function implements the Lucas-Kanade algorithm for computing optical flow between
two consecutive frames. It includes several improvements for robustness:
- Gaussian smoothing for noise reduction
- Eigenvalue-based condition checking
- Flow magnitude thresholding
- Optional outlier removal (commented out in post-processing)
Args:
frame1 (numpy.ndarray): First frame (earlier time point)
frame2 (numpy.ndarray): Second frame (later time point)
window_size (int, optional): Size of the local window for flow computation.
Should be odd. Defaults to 16.
tau (float, optional): Threshold for eigenvalue condition checking.
Higher values mean stricter filtering. Defaults to 1e-2.
Returns:
tuple: Two numpy.ndarray containing:
- u: Horizontal component of the optical flow
- v: Vertical component of the optical flow
Note:
- Frames should be in grayscale format
- The function includes eigenvalue-based filtering to handle aperture problem
- Flow vectors with magnitude > 50 are filtered out
"""
# Apply Gaussian smoothing to reduce noise
# Higher sigma (2) for more aggressive smoothing
= gaussian_filter(frame1, sigma=2)
frame1 = gaussian_filter(frame2, sigma=2)
frame2
# Compute spatial and temporal derivatives
= compute_derivatives(frame1, frame2)
Ix, Iy, It
# Initialize flow fields with zeros
= np.zeros_like(frame1, dtype=float)
u = np.zeros_like(frame1, dtype=float)
v
# Pad images to handle border pixels
# Using edge padding to avoid border artifacts
= window_size // 2
pad = np.pad(Ix, ((pad, pad), (pad, pad)), mode='edge')
Ix_pad = np.pad(Iy, ((pad, pad), (pad, pad)), mode='edge')
Iy_pad = np.pad(It, ((pad, pad), (pad, pad)), mode='edge')
It_pad
# Iterate through each pixel in the frame
for y in range(pad, frame1.shape[0] + pad):
for x in range(pad, frame1.shape[1] + pad):
# Extract local windows for derivatives
# Flatten to create system of equations
= Ix_pad[y-pad:y+pad+1, x-pad:x+pad+1].flatten()
Ix_win = Iy_pad[y-pad:y+pad+1, x-pad:x+pad+1].flatten()
Iy_win = It_pad[y-pad:y+pad+1, x-pad:x+pad+1].flatten()
It_win
# Construct system of equations Av = b
# A: spatial gradients, b: negative temporal gradient
= np.vstack((Ix_win, Iy_win)).T
A = -It_win
b
# Compute ATA for solving least squares
= np.dot(A.T, A)
ATA
# Check eigenvalues for aperture problem
= np.linalg.eigvals(ATA)
eigenvalues
# Skip if eigenvalues indicate ill-conditioned system
# Two conditions: minimum eigenvalue threshold and condition number check
if np.min(eigenvalues) < tau or np.max(eigenvalues) / (np.min(eigenvalues) + 1e-10) > 100:
continue
# Solve the system of equations
try:
= np.linalg.solve(ATA, np.dot(A.T, b))
flow
# Apply flow magnitude threshold to filter out large motions
if np.sqrt(flow[0]**2 + flow[1]**2) < 50: # Threshold can be adjusted
-pad, x-pad] = flow[0]
u[y-pad, x-pad] = flow[1]
v[y
except np.linalg.LinAlgError:
continue
return u, v
file = 0
= "../../images/blogs/Lucas-Kanade/"+str(file)+".gif"
gif_path = np.array(imageio.mimread(gif_path)) # Read GIF file
gif_frames
= cv2.VideoWriter("../../images/blogs/Lucas-Kanade/Flow"+str(file)+".AVI",
writer *'XVID'), 10, (2*gif_frames[0].shape[1], 2*gif_frames[0].shape[0]))
cv2.VideoWriter_fourcc(
for i in range(1, len(gif_frames)):
# Convert frames to grayscale
= cv2.cvtColor(gif_frames[i-1], cv2.COLOR_RGB2GRAY)
frame1_gray = cv2.cvtColor(gif_frames[i], cv2.COLOR_RGB2GRAY)
frame2_gray
# Compute optical flow using Lucas-Kanade method.
= lk_flow(frame1_gray, frame2_gray, window_size=16, tau=1e-2)
u, v = u*2, v*2
u,v
# Display optical flow vectors on frame.
= gif_frames[i-1].copy()
flow_img = cv2.cvtColor(flow_img, cv2.COLOR_RGB2BGR)
flow_img = 8
step
for y in range(0, flow_img.shape[0], step):
for x in range(0, flow_img.shape[1], step):
int(x+u[y, x]), int(y+v[y, x])), (0, 0, 255), 1)
cv2.arrowedLine(flow_img, (x, y), (
= cv2.resize(flow_img, (2*flow_img.shape[1], 2*flow_img.shape[0]))
flow_img # Display frame with optical flow vectors.
'Optical Flow', flow_img)
cv2.imshow(
# Write frame to output video
writer.write(flow_img)
# Press 'q' to quit
if cv2.waitKey(1) & 0xFF == ord('q'):
cv2.destroyAllWindows()1)
cv2.waitKey(
cv2.destroyAllWindows()1) cv2.waitKey(