-
Notifications
You must be signed in to change notification settings - Fork 1
Lane Follower Systems into dev #211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
a9212b8
f8c5ad7
d0b6223
187bd1b
8c363c8
473134b
550aeec
c7d7692
a4a018f
20d1e0b
e5fd3d1
d0c80ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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]] |
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class ifResult: | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please follow PEP8 for naming. This can also be made into a |
||||||||||||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||
|
|
||||||||||||
| 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 | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you went through the effort of making |
||||||||||||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| 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 | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| 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: | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Something along those lines. |
||||||
There was a problem hiding this comment.
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
dataclassesthat have "sane" defaults.