Skip to content

Commit

Permalink
Add comments
Browse files Browse the repository at this point in the history
  • Loading branch information
araffin committed Jan 2, 2018
1 parent 4560ff0 commit edce0e4
Show file tree
Hide file tree
Showing 15 changed files with 123 additions and 55 deletions.
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,16 @@ python main.py

0. You need a computer in addition to the raspberry pi
1. Create a Local Wifi Network (e.g. using [create ap](https://github.com/oblique/create_ap))
2. Connect the raspberrypi to this network ([Wifi on RPI](https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md))
2. Connect the raspberry pi to this network ([Wifi on RPI](https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md))
3. Launch teleoperation server (it will use the port 5556)
```
python command/python/teleop_server.py
```
4. Launch teleoperation client on your computer (you have to edit the raspberry pi Ip in the code)
4. Launch teleoperation client on your computer (you have to edit the raspberry pi `IP` in the code)
```
python command/python/teleop_client.py
```
5. Enjoy! You can now control the car with the keyboard.

## How to train the line detector ?

Expand Down Expand Up @@ -102,15 +103,22 @@ python -m train.train -f path/input/folder
```
The best model (lowest error on the validation data) will be saved as *mlp_model_tmp.npz*.


6. Test the trained neural network

```
python -m train.test -f path/input/folder -w mlp_model_tmp
```

### Installation

#### Recommended : Use an image with everything installed
#### Recommended : Use an image with everything already installed

0. You need a micro sd card (warning, all data on that card will be overwritten)

1. Download the image [here](https://drive.google.com/open?id=0Bz4VOC2vLbgPTl9LZzNNcnBCWUU)

The characteristics of the image:
Infos about the linux image:
OS: [Ubuntu MATE 16.04](https://ubuntu-mate.org/raspberry-pi/) for raspberry pi

**Username**: enstar
Expand Down
2 changes: 1 addition & 1 deletion command/python/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def clear(self):
n_received_semaphore = threading.Semaphore(n_messages_allowed)
serial_lock = threading.Lock()
command_queue = CustomQueue(2) # Must be >= 2 (motor + servo order)
rate = 1 / 1000 # 1000 fps (limit the rate of communication with the arduino)
rate = 1 / 1000 # 1000 Hz (limit the rate of communication with the arduino)


def resetCommandQueue():
Expand Down
4 changes: 3 additions & 1 deletion constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
SPLIT_SEED = 42 # For train/val/test split


# Direction and speed
THETA_MIN = 70 # value in [0, 255] sent to the servo
THETA_MAX = 150
ERROR_MAX = 1.0
MAX_SPEED_STRAIGHT_LINE = 50 # order between 0 and 100
MAX_SPEED_SHARP_TURN = 15
MIN_SPEED = 10

# PID Control
Kp_turn = 40
Kp_line = 35
Expand Down Expand Up @@ -55,4 +57,4 @@
ENTER_KEY = 10
SPACE_KEY = 32
EXIT_KEYS = [113, 27] # Escape and q
S_KEY = 115 # Save key
S_KEY = 115 # S key
Empty file added debug/.gitkeep
Empty file.
44 changes: 30 additions & 14 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
Main script for autonomous mode
It launches all the thread and does the PD control
It launches all the thread and does the PID control
"""
from __future__ import division, print_function

Expand Down Expand Up @@ -43,13 +43,17 @@ def main_control(out_queue, resolution, n_seconds=5):
:param resolution: (int, int)
:param n_seconds: (int) number of seconds to keep this script alive
"""
# Moving mean for line curve estimation
mean_h = 0
start_time = time.time()
error, errorD, errorI = 0, 0, 0
last_error = 0
initialized = False
initialized = False # compute derivative error for t > 1 only
# Neutral Angle
theta_init = (THETA_MAX + THETA_MIN) / 2
# Middle of the image
x_center = resolution[0] // 2
max_error_px = resolution[0] // 2 # max error in pixel
# Use mutable to be modified by signal handler
should_exit = [False]

Expand All @@ -64,24 +68,33 @@ def ctrl_c(signum, frame):
while time.time() - start_time < n_seconds and not should_exit[0]:
# Output of image processing
turn_percent, centroids = out_queue.get()
# print(centroids)

# Compute the error to the center of the line
# We want the line to be in the middle of the image
# Here we use the farthest centroids
error = (resolution[0] // 2 - centroids[-1, 0]) / (resolution[0] // 2)
# TODO: try with the mean of the centroids to reduce noise
error = (x_center - centroids[-1, 0]) / max_error_px

# Reduce max speed if it is a sharp turn
# Represent line curve as a number in [0, 1]
# h = 0 -> straight line
# h = 1 -> sharp turn
h = np.clip(turn_percent / 100.0, 0, 1)
# Moving mean
# Update moving mean
mean_h += ALPHA * (h - mean_h)

# print("mean_h={}".format(mean_h))
# We are using the moving mean (which is less noisy)
# for line curve estimation
h = mean_h
# Reduce max speed if it is a sharp turn
v_max = h * MAX_SPEED_SHARP_TURN + (1 - h) * MAX_SPEED_STRAIGHT_LINE

# Different Kp depending on the line curve
Kp = h * Kp_turn + (1 - h) * Kp_line

# Reduce speed if we have a high error
# Represent error as a number in [0, 1]
# t = 0 -> no error, we are perfectly on the line
# t = 1 -> maximal error
t = np.clip(error / float(ERROR_MAX), 0, 1)
# Reduce speed if we have a high error
speed_order = t * MIN_SPEED + (1 - t) * v_max

if initialized:
Expand All @@ -98,17 +111,16 @@ def ctrl_c(signum, frame):
# Update integral error
errorI += error
last_time = time.time()
# print("error={}".format(error))
# print("u_angle={}".format(u_angle))

angle_order = theta_init - u_angle
angle_order = np.clip(angle_order, THETA_MIN, THETA_MAX).astype(int)

# Send orders to Arduino
try:
common.command_queue.put_nowait((Order.MOTOR, int(speed_order)))
common.command_queue.put_nowait((Order.SERVO, angle_order))
except fullException:
print("Queue is full")
print("Command queue is full")

# SEND STOP ORDER at the end
forceStop()
Expand All @@ -118,11 +130,13 @@ def ctrl_c(signum, frame):

if __name__ == '__main__':
try:
# Open serial port (for communication with Arduino)
serial_port = get_serial_ports()[0]
serial_file = serial.Serial(port=serial_port, baudrate=BAUDRATE, timeout=0, writeTimeout=0)
except Exception as e:
raise e

# Initialize communication with Arduino
while not is_connected:
print("Waiting for arduino...")
sendOrder(serial_file, Order.HELLO.value)
Expand All @@ -136,14 +150,16 @@ def ctrl_c(signum, frame):

print("Connected to Arduino")
resolution = CAMERA_RESOLUTION
max_width = resolution[0]

# image processing queue, output centroids
# Image processing queue, output centroids
out_queue = queue.Queue()
condition_lock = threading.Lock()
exit_condition = threading.Condition(condition_lock)

print("Starting Image Processing Thread")
# It starts 2 threads:
# - one for retrieving images from camera
# - one for processing the images
image_thread = ImageProcessingThread(Viewer(out_queue, resolution, debug=False, fps=FPS), exit_condition)
# Wait for camera warmup
time.sleep(1)
Expand Down
4 changes: 4 additions & 0 deletions opencv/benchmark.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
"""
Compute time to process an image
It processes the images N_ITER times and print statistics
"""
from __future__ import print_function, with_statement, division

import time
Expand Down
19 changes: 14 additions & 5 deletions opencv/image_processing.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
"""
Main script for processing an image:
it extracts the different ROI, detect the line and estimate line curve
"""
from __future__ import print_function, with_statement, division

import argparse
Expand All @@ -6,11 +10,11 @@
import numpy as np

from constants import REF_ANGLE, MAX_ANGLE, REGIONS, EXIT_KEYS, WIDTH, HEIGHT
from train import preprocessImage, loadVanillaNet
from train import preprocessImage, loadVanillaNet, loadPytorchNetwork


# Either load network with pytorch or with numpy
# pred_fn = loadNetwork()
# pred_fn = loadPytorchNetwork()
pred_fn = loadVanillaNet()


Expand Down Expand Up @@ -44,6 +48,7 @@ def processImage(image, debug=False, regions=None, interactive=False):
if not debug:
# Batch Prediction
pred_imgs = []
# Preprocess each region of interest
for idx, r in enumerate(regions):
im_cropped = image[int(r[1]):int(r[1] + r[3]), int(r[0]):int(r[0] + r[2])]
# Preprocess the image: scaling and normalization
Expand Down Expand Up @@ -95,7 +100,10 @@ def processImage(image, debug=False, regions=None, interactive=False):
cx, cy = x_center, y_center

centroids[idx] = np.array([cx + margin_left, cy + margin_top])

# Linear Regression to fit a line
# It estimates the line curve
# TODO: test with l2 penalty to reduce noise
x = centroids[:, 0]
y = centroids[:, 1]
# Case x = cst
Expand All @@ -104,12 +112,14 @@ def processImage(image, debug=False, regions=None, interactive=False):
turn_percent = 0
else:
A = np.vstack([x, np.ones(len(x))]).T
# Linear regression using least squares method
# y = m*x + b
m, b = np.linalg.lstsq(A, y)[0]
if debug:
# Points for plotting the line
x = np.array([0, im_width], dtype=int)
pts = (np.vstack([x, m * x + b]).T).astype(int)
# Compute the angle between the reference and the fitted line
track_angle = np.arctan(m)
diff_angle = abs(REF_ANGLE) - abs(track_angle)
# Estimation of the line curvature
Expand All @@ -122,7 +132,6 @@ def processImage(image, debug=False, regions=None, interactive=False):

if debug:
if all(errors):
# print("No centroids found")
cv2.imshow('result', image)
else:
for cx, cy in centroids:
Expand All @@ -139,13 +148,13 @@ def processImage(image, debug=False, regions=None, interactive=False):

if __name__ == '__main__':

parser = argparse.ArgumentParser(description='White Line Detection')
parser = argparse.ArgumentParser(description='Line Detection')
parser.add_argument('-i', '--input_image', help='Input Image', default="", type=str)

args = parser.parse_args()
if args.input_image != "":
img = cv2.imread(args.input_image)
turn_percent, centroids = processImage(img, debug=True)
if cv2.waitKey(0) & 0xff == 27:
if cv2.waitKey(0) & 0xff in EXIT_KEYS:
cv2.destroyAllWindows()
exit()
6 changes: 4 additions & 2 deletions opencv/process_folder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Process a folder of images
Apply image processing on a folder of images
"""
from __future__ import print_function, with_statement, division

Expand Down Expand Up @@ -31,10 +31,12 @@
regions = None
if args.regions == 0:
regions = [[0, 0, img.shape[1], img.shape[0]]]

processImage(img, debug=True, regions=regions)

# Retrieve pressed key
key = cv2.waitKey(0) & 0xff

if key in EXIT_KEYS:
cv2.destroyAllWindows()
exit()
Expand Down
16 changes: 10 additions & 6 deletions opencv/process_video.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""
Process a video
Apply image processing on a video:
it processes each frame (line detection + line curve estimation)
and shows the result
"""
from __future__ import print_function, with_statement, division

Expand All @@ -11,7 +13,8 @@
from constants import RIGHT_KEY, LEFT_KEY, SPACE_KEY, EXIT_KEYS
from opencv.image_processing import processImage

play_video = False
# Pressing the space bar, it plays the video
playing_video = False

parser = argparse.ArgumentParser(description='Line Detection on a video')
parser.add_argument('-i', '--input_video', help='Input Video', default="video.mp4", type=str)
Expand Down Expand Up @@ -61,17 +64,18 @@ def nothing(x):

processImage(img, debug=True, regions=regions)

if not play_video:
if not playing_video:
key = cv2.waitKey(0) & 0xff
else:
key = cv2.waitKey(10) & 0xff

if key in EXIT_KEYS:
cv2.destroyAllWindows()
exit()
elif key in [LEFT_KEY, RIGHT_KEY] or play_video:
current_idx += 1 if key == RIGHT_KEY or play_video else -1
elif key in [LEFT_KEY, RIGHT_KEY] or playing_video:
current_idx += 1 if key == RIGHT_KEY or playing_video else -1
current_idx = np.clip(current_idx, 0, n_frames - 1)
elif key == SPACE_KEY:
play_video = not play_video
playing_video = not playing_video

cap.set(image_zero_index, current_idx)
2 changes: 1 addition & 1 deletion picam/image_analyser.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def extractInfo(self):
try:
frame = self.frame_queue.get(block=True, timeout=1)
except queue.Empty:
print("Queue empty")
print("Frame queue empty")
continue
# 1 ms per loop
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
Expand Down
3 changes: 2 additions & 1 deletion train/label_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from opencv.image_processing import processImage
from constants import REGIONS

parser = argparse.ArgumentParser(description='Split a video into a sequence of images')
parser = argparse.ArgumentParser(description='Labeling tool for line detection')
parser.add_argument('-i', '--input_folder', help='Input Folder', default="", type=str, required=True)
parser.add_argument('-o', '--output_folder', help='Output folder', default="", type=str, required=True)

Expand Down Expand Up @@ -70,6 +70,7 @@
cv2.imwrite('{}/{}.jpg'.format(output_folder, output_name), im_cropped)
# Update infos
with open('{}/infos.pkl'.format(output_folder), 'wb') as f:
# protocol=2 for python 2 compatibility
pkl.dump(infos_dict, f, protocol=2)
j += 1

Expand Down
4 changes: 4 additions & 0 deletions train/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
"""
Different neural network architectures for detecting the line
# TODO: change std of weights initialization
"""
from __future__ import print_function, division, absolute_import

import torch.nn as nn
Expand Down
Loading

0 comments on commit edce0e4

Please sign in to comment.