Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions components/lane_follower/src/Follower_Consts.toml
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably better to just have these as dataclasses that have "sane" defaults.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Constants for the individual follower module

[Individual_Follower]
NWINDOWS = 9
SEARCH_WIDTH = 100
MINPIXELS = 40
LANE_POLY_SIZE = 2
BOX_COLOR = [0, 255, 0]
LANE_COLOR = [255, 0, 0]
DRAW_THICKNESS = 2

[Lane_Follower]
GUI = true
# TODO: Move these into a conf
KERNEL = 31
LANE_TOLERANCE = 10
MISSING_IMAGE_TOLERANCE = 100
EMPTY_WINDOWS_THRESHOLD = 3
OVERFLOW = 1000.0
FORMAT = [640, 480]
TOLERANCE = 100
PIXELS_TO_METERS = 260.8269125
lower_hsv = [100, 0, 220]
upper_hsv = [255, 255, 255]
# Alignment points
bottom_coordinates = [[12, 472], [499, 475]]
top_coordinates = [[90, 8], [435, 24]]
150 changes: 150 additions & 0 deletions components/lane_follower/src/individual_follower.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import math

import cv2
import numpy as np
import toml
from einops import repeat
Comment on lines +5 to +6
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import toml
from einops import repeat
import toml
from einops import repeat



class ifResult:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please follow PEP8 for naming. This can also be made into a dataclass.

def __init__(self):
self.result_img = None
self.empty_windows = 0
self.heading = 0
self.slope = 0

pass


class IndividualFollower:

def __init__(self, toml_path='Follower_Consts.toml'):

self.consts = toml.load(toml_path)["Individual_Follower"]
Comment on lines +21 to +23
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the vision, but the execution can be better. IMO TOML should be unmarshaled outside the class and passed as a constants struct.

# self.fit is set in Plot_Line
self.fit = None
# These two are set below
self._binary_warped = None
self._histogram = None
Comment on lines +24 to +28
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think these comments add much value to the reader. I'd also separate public and private members of the class.


# Need to determine if these should remain properties
# Or if they should be passed as arguments to Plot_Line
@property
def _binary_warped(self, value: np.ndarray):
Comment on lines +30 to +33
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@property should only get used for public members and without arguments so that they just act as a variable access.

self._binary_warped = value
self._histogram = np.sum(
self.binary_warped[self.binary_warped.shape[0] // 2 :, :], axis=0
)

def get_white_pixels(self):
# Returns an output image
NWINDOWS = self.consts["NWINDOWS"]
SEARCH_WIDTH = self.consts['SEARCH_WIDTH']
Comment on lines +40 to +42
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See, if this was a config class, we wouldn't need these intermediate variables that need to be made. We could also give default values to the config class so that we don't need to always pass it to __init__()


window_height = np.int32(self._binary_warped.shape[0] / NWINDOWS)
lane_inds = []

nonzero_pixels = self._binary_warped.nonzero()
nonzero_y = np.array(nonzero_pixels[0])
nonzero_x = np.array(nonzero_pixels[1])

empty_windows = 0
lane_base = np.argmax(self.histogram[:])

# Image to visualize output
out_img = repeat(self._binary_warped, 'h w -> h w c', repeat=3) * 255
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still have magic numbers. I'd also give more descriptive names to the dimensions like height, width, and channels.

##These outputs need to be confirmed compatible
# out_img = (
# np.dstack(
# self._binary_warped, self._binary_warped, self._binary_warped
# )
# * 255
# )

for window in range(NWINDOWS):
window_dims = window * window_height
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you went through the effort of making window_height an np.int32, I think you should do the same with window, or make them both Python ints.

win_y_upper = self._binary_warped.shape[0] - window_dims
# One window height lower than win_y_higher
win_y_lower = win_y_upper - window_height
win_x_lower = lane_base - SEARCH_WIDTH
win_x_upper = lane_base + SEARCH_WIDTH

lower_coords = (win_x_lower, win_y_lower)
upper_coords = (win_x_upper, win_y_lower)
cv2.rectangle(
Comment on lines +73 to +74
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
upper_coords = (win_x_upper, win_y_lower)
cv2.rectangle(
upper_coords = (win_x_upper, win_y_lower)
cv2.rectangle(

out_img,
lower_coords,
upper_coords,
self.consts["BOX_COLOR"],
self.consts["DRAW_THICKNESS"],
)

white_pix_inds = (
(nonzero_y >= win_y_lower)
& (nonzero_y < win_y_upper)
& (nonzero_x >= win_x_lower)
& (nonzero_x >= win_x_upper)
).nonzero_pixels()[0]

# This should likely be moved into the if statement: leaving for now
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For stuff like this, turn it into a Python warning so that we don't forget about it.

lane_inds.append(white_pix_inds)
if len(white_pix_inds) > self.consts["MINPIXELS"]:
# np.mean will return a float: We need an exact value
lane_base = np.int32(np.mean(nonzero_x[white_pix_inds]))
Comment on lines +92 to +93
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# np.mean will return a float: We need an exact value
lane_base = np.int32(np.mean(nonzero_x[white_pix_inds]))
lane_base = np.int32(np.mean(nonzero_x[white_pix_inds]))

else:
empty_windows += 1
if len(lane_inds) == 0:
return None
lane_array = np.concatenate(lane_inds)
lane_x_pos = nonzero_x[lane_array]
lane_y_pos = nonzero_y[lane_array]
# TODO: test if this statement is necessary
if lane_x_pos.any() and lane_y_pos.any():
self._fit = np.polyfit(
lane_y_pos, lane_x_pos, self.consts["LANE_POLY_SIZE"]
)

out_img[nonzero_y[lane_array], nonzero_x[lane_array]] = (
self.consts["LANE_COLOR"]
)
return out_img

def plot_line(self) -> ifResult:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you search online how other projects might add code to generate plots for certain things? There has to be a cleaner way to do this.

if not self._binary_warped:
raise Exception("no binary warp specified")

result = ifResult()

NWINDOWS = self.consts["NWINDOWS"]
window_height = np.int32(self._binary_warped.shape[0] / NWINDOWS)
out_img = self.get_white_pixels()
if out_img is None:
return result

##Generates the search window area
window_img = np.zeros_like(out_img)
# Create linearly spaced points at each height, evaluate polynomial, create coordinates
x_val = np.arange(0, window_height, step=5).T
y_val = np.polyval(self.fit, x_val)
coords = np.concatenate((x_val, y_val), axis=0)
##TODO: Test if this approach works
cv2.polylines(
out_img,
coords,
isClosed=False,
color=self.consts["LANE_COLOR"],
thickness=self.consts["DRAW_THICKNESS"],
)

# Calculating heading error by converting lane polynomial into line
##TODO: Make this use the furthest found box. This way, we'll use the most accurate heading
y_lower = 0
y_upper = (NWINDOWS + 1) * window_height

result.slope = np.polyval(self.fit, y_lower) - np.polyval(
self.fit, y_upper
) / (y_upper - y_lower)
result.result_img = cv2.addWeighted(out_img, 1, window_img, 0.3, 0)
result.heading = math.atan(result.slope)

return result
20 changes: 20 additions & 0 deletions components/lane_follower/src/lane_follower.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code should just not be committed as it's accessible in Git history.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import toml
import cv2

# import numpy as np


class LaneFollower:

def __init__(
self, odom_sub, cam_left, cam_right, toml_path='Follower_Consts.toml'
):
self.vidcap_left = cv2.VideoCapture(cam_left)
self.vidcap_right = cv2.VideoCapture(cam_right)
self.const = toml.load(toml_path)["Lane_Follower"]
# LOWER = np.array(self.const['lower_hsv'])
# UPPER = np.array(self.const['upper_hsv'])
# PTS1 = np.float32([tl, bl, tr, br])
# PTS2 = np.float32([[0, 0], [0, 480], [640, 0], [640, 480]])
# # Matrix to warp the image for birdseye window
# UNWARP = cv2.getPerspectiveTransform(pts1, pts2)
8 changes: 8 additions & 0 deletions components/lane_follower/src/pyproject.toml
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we haven't discussed this yet, but here's the current schema for a component:

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[project]
version = '1.0'
name = "individual-follower"
authors = [
{name = "Vaibhav Hariani"},
{name = "Isaiah Rivera"},
]
description = "IGVC lane follower module for a single camera"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description = "IGVC lane follower module for a single camera"
description = "lane follower for a video feed"

Something along those lines.