diff --git a/src/Active-Lane-Keeping-Assistant/.gitignore b/src/Active-Lane-Keeping-Assistant/.gitignore
new file mode 100644
index 0000000000..f58035a579
--- /dev/null
+++ b/src/Active-Lane-Keeping-Assistant/.gitignore
@@ -0,0 +1,5 @@
+__pycache__/
+assets/*
+!assets/.keep
+img/examples/*
+!img/examples/.keep
\ No newline at end of file
diff --git a/src/Active-Lane-Keeping-Assistant/README.md b/src/Active-Lane-Keeping-Assistant/README.md
index 4f80bd0eea..ebd89fdb3e 100644
--- a/src/Active-Lane-Keeping-Assistant/README.md
+++ b/src/Active-Lane-Keeping-Assistant/README.md
@@ -1 +1,205 @@
-车道辅助系统的完成
\ No newline at end of file
+# Active Lane Keeping Assistant
+
+> Please note that the code and contributions in this GitHub repository were created through a collaborative effort by [@Irish-77](https://github.com/Irish-77) and [@Ronho](https://github.com/Ronho). Both contributors acknowledge that their work is of equal merit and that they each have a deep understanding of the other's contributions.
+
+The aim of this project is to control a vehicle. Computer vision methods are used for lane recognition. The final control of the vehicle is done with the help of different variants of the PID controller. The variants are also investigated and compared.
+
+## Table of contents
+1. [Installation](#installation)
+1. [Description](#description)
+ 1. [Introductory Questions](#introductory-questions)
+ 1. [Computer Vision to Identify the Track](#computer-vision-to-identify-the-track)
+ 1. [Control Technology for Tracking](#control-technology-for-tracking)
+1. [Closing Words](#closing-words)
+1. [Sources](#sources)
+
+## Installation
+
+First, the project must be cloned via git using the following command.
+```sh
+git clone https://github.com/C2G-BR/Active-Lane-Keeping-Assistant.git
+```
+To be able to test the project, you have to install [CARLA](https://github.com/carla-simulator/carla/releases) next. This project was created and tested with version 0.9.13 of CARLA.
+
+Additionally, a Python environment is required. Due to dependencies it is necessary to use Python 3.8. All necessary packages can be installed with the command
+```sh
+pip install -r requirements.txt
+```
+that must be executed inside the project folder.
+
+To run the program, CARLA must be started at the beginning. Afterwards a run can be started with
+```sh
+python src/main.py -id "test" -c pid -s 1000
+```
+within the project's folder. Within the CARLA interface, a car should now appear that starts to move. To find the vehicle, it may be necessary to look around within the world. All possible configuration settings can be viewed by
+```sh
+python main.py -h
+```
+If the error
+```sh
+RuntimeError: time-out of 5000ms while waiting for the simulator, make sure the simulator is ready and connected to 127.0.0.1:2000
+```
+occurs, it can help to execute the command multiple times and restart CARLA.
+
+## Description
+
+### Introductory Questions
+In order to implement the system, the following questions first arose:
+
+What metric can we use to obtain information about the current deviation from the track?
+
+>Since the focus of this project is on image-based control of the vehicle, several transformation steps were applied to the image to find the track, which are described below. These transformations do not use machine learning techniques in the process. The deviation value from the center point can ultimately be derived from the detected line.
+
+Which algorithm can be used to keep the track optimal?
+
+>Different control engineering methods were selected for this project. The following algorithms were evaluated:
+>1. Simple countersteering with fixed steering angle
+>1. P controller
+>1. PD controller
+>1. PID controller
+
+### Computer Vision to Identify the Track
+As described earlier, no machine learning methods were used for track finding. There are two reasons for this:
+- many machine learning methods often still work according to a black box principle, i.e. the exact reason for prediction is usually not necessarily comprehensible.
+- on the other hand it is also interesting to see how only image transformations can already detect the roadway.
+
+**Approach**
+
+To extract the trace through image-based transformations, several steps were performed, which are described below.
+
+
+
+
+Original image that we get from the simulator. We get a new frame every 0.01 seconds.
+
+
+
+
+
+The image shows the original image in black and white. All colors that are within a defined color space are marked with white. All other pixels are replaced by black pixels. Additionally, the blur of the image was increased.
+
+
+
+
+
+The region of interest (RoI, outlined in pink here) indicates the area to be viewed. This means that only the current lane is considered. The RoI should be large enough to include curves, but small enough not to include other lane lines.
+
+
+
+
+
+Here the angle is changed to a bird's eye view. Only the region of interest is taken into account. You can already see the track better. Through the vanishing point the lines approach each other.
+
+
+
+
+
+The figure shows the transformed image in binary representation and the corresponding histogram. The peaks of the histogram correspond to the gradients (i.e. the change from black to white and vice versa) from the image above. From the histogram, the outer limits of the lane can be read. The center of the roadway corresponds to the center of the two histogram peaks.
+
+
+
+
+
+During this step, a small window is slid over the image. Based on the non-black pixels inside the sliding window, important line points are extracted.
+
+
+
+
+
+Using the calculated coordinates of the line points (from the previous image), a polynomial is approximated for each side.
+
+
+
+
+
+Original image with detected track.
+
+
+**Challenges during Implementation**
+
+> No or Incorrectly Detected Track
+
+Based on the detected tracks, the center point could be determined afterwards. However, there is a problem here. On the test track "Town04" (see below), which we have selected here, there are no solid lines (only the separating lane on the far left and right), which is why our method has difficulty recognizing a reasonable lane. The second figure below shows what such an incorrectly detected trace looks like. In the code, we caught the error by calculating the area of the detected trace each time. If the area falls below a predefined threshold, the detected trace is ignored and the center of the last valid trace is used as a starting point for further decisions.
+
+
+
+
+Town04 in CARLA
+
+
+
+
+
+The illustration shows a misidentified track. Within the red box (used here only as a visual aid), a narrow green stripe can be seen. However, the green stripe does not correspond to any lane and is therefore an error.
+
+
+> Highway Exit
+
+In the CARLA simulation, the vehicle is spawned on the right lane. However, the exits are also linked to the right lane. As soon as an exit comes up, our program has problems to detect a lane, because for a short time the right lane is missing (see below). To avoid such errors, a lane change to the left lane is performed at the beginning of each simulation.
+
+
+
+
+On the right lane, the dividing line is missing for a short time.
+
+
+> Collision Detection
+
+Another problem was the collision with the concrete sliding wall. If the vehicle does not detect a lane or the parameters are configured too sensitively, the vehicle leaves the lane and collides with a concrete sliding wall. Sometimes, however, a phenomenon occurs that shortly before the vehicle collides, it detects a lane for a short time. Accordingly, the vehicle stores the last deviation of the last valid time for the current error (see solution from chapter No or Incorrectly Detected Lane). During optimization, this phenomenon can cause problems. As a solution we have implemented a CollisionDetector. As soon as the vehicle collides with an object and therefore comes to a stop, the previous error is stored for the remaining time.
+
+### Control Technology for Tracking
+
+As already described in the introduction, four procedures were tested. The simple method countersteers with a constant steering angle as soon as it exceeds a threshold value. In this way, the agent manages to keep the vehicle on track without any problems. However, the abrupt and uneven steering movements also reduce driving comfort. Programming a simple procedure was nevertheless helpful, as it allowed the test environment to be tested initially.
+
+Based on the observations of the driving behavior controlled by a simple controller, a more complex controller is needed that blocks strong "wobble" and compensates instead by precise steering movements.
+
+A pure P-controller like the simple controller was already able to drive the track with confidence. In addition, the proportional part made the steering movements smoother than with just a constant factor. However, the vehicle only countersteers as soon as the error occurs from the other direction (vehicle must now approach the center point from the other side). The differential component counteracts overshooting of the track center point, thus reducing extreme overshoots. If, for example, further deviations or distortions occur due to the environment, which prevent the vehicle from reaching the center point, these influences must be taken into account. The integrating controller monitors the sum of all errors and influences the steering movement unless the errors have been reduced during a certain time.
+
+The parameters for the P and PD controllers can still be determined manually, since a maximum of two parameters have to be adjusted to each other. With three parameters, however, the adaptation effort increases, which is why the use of automated processes is the logical consequence. There are different methods with advantages and disadvantages. The random search offers the possibility to test many parameter variations, which normally would not be tested in this form. In order to find meaningful parameters, however, many iterations must be executed, which also requires many resources. A method that does not have this problem is the Twiddle algorithm, which was also used in the context of this project. The twiddle algorithm makes it possible to calculate reasonable values for the proportional, integrating and differentiating parts using a simple implementation.
+
+The twiddle algorithm proceeds as follows: At the beginning, the parameters to be optimized are initially defined in a list and the corresponding deltas are set up. The parameters from the parameter list are adjusted one after the other. For this purpose, it is first examined whether an increase of the current parameter results in a reduction of the error, which in this case corresponds to the deviation from the center of the track. If this is the case, the corresponding delta value is increased. Otherwise, the search is performed in the other direction by reducing the current parameter by twice the delta value and then recalculating the error. If there was an improvement with the reduced parameter value, the delta value is increased. Otherwise, the delta values and parameter values are updated. To determine the parameters of the PID controller, the combinations were tested to a maximum of 5000 steps using the twiddle method.
+
+Due to the simulation environment, we were not able to optimize the parameters outside the simulation, which is why we were bound to the real-time simulation and could not "fast-forward" the optimization.
+
+The final parameters are:
+| Method | Value |
+| ----------- | ----------- |
+| Simple Lane Detection | Constant steering angle: 0.75 |
+| PID-Controller | P: 0.65003358 I: 0.00000002 D: 0.03399347 |
+
+**Examples of Runs of the Simplified Procedure**
+
+[](https://youtu.be/WlGPokl0ZK8)
+
+(Video) Simulation with the simple method over 10k steps.
+
+
+
+
+
+Error depending on the time step for the simple method. It can be seen from step 1000 that the error occurs cyclically in similarity to the sinusoid. This can be explained by the change from side relative to the center of the detected trace. The straight sections connecting the curves show the moments when no trace was detected. Here, false detections occur again and again due to the small white lines in the spaces.
+
+
+**Examples of Runs of the PID Controller**
+
+[](https://youtu.be/YVZafMtOxzY)
+
+
+(Video) Simulation with a PID controller over 10k steps.
+
+
+
+
+
+Error depending on the time step for the PID controller. Larger deviations occur at the beginning, which, however, do not occur from step 2000 onwards. After that the controller shows a constant stable deviation to the center point.
+
+
+## Closing Words
+
+The vision of self-driving vehicles poses a great challenge both in science and in practice, which is why there is a great deal of interest in this topic, even from many newcomers. Accordingly, there are some sources on the Internet and in the literature about this, which have already implemented similar projects. Furthermore, there are also different approaches. For example, some projects encode the images in HSL or HSV color space, whereas other projects leave it at RGB encoding. It should be mentioned that we did not "invent" the performed transformation steps during the track detection, rather we tested different combinations of approaches and parameters that are most suitable for our specific use case.
+
+## Sources
+- https://towardsdatascience.com/lane-detection-with-deep-learning-part-1-9e096f3320b7
+- https://www.analyticsvidhya.com/blog/2020/05/tutorial-real-time-lane-detection-opencv/
+
+[Go back to table of contents](#table-of-contents)
diff --git a/src/Active-Lane-Keeping-Assistant/assets/.keep b/src/Active-Lane-Keeping-Assistant/assets/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/Active-Lane-Keeping-Assistant/docs/error_detection.png b/src/Active-Lane-Keeping-Assistant/docs/error_detection.png
new file mode 100644
index 0000000000..5e94c28c11
Binary files /dev/null and b/src/Active-Lane-Keeping-Assistant/docs/error_detection.png differ
diff --git a/src/Active-Lane-Keeping-Assistant/docs/highway_exit.png b/src/Active-Lane-Keeping-Assistant/docs/highway_exit.png
new file mode 100644
index 0000000000..15ac052fb3
Binary files /dev/null and b/src/Active-Lane-Keeping-Assistant/docs/highway_exit.png differ
diff --git a/src/Active-Lane-Keeping-Assistant/docs/lane_overlay.jpg b/src/Active-Lane-Keeping-Assistant/docs/lane_overlay.jpg
new file mode 100644
index 0000000000..09bf3922e3
Binary files /dev/null and b/src/Active-Lane-Keeping-Assistant/docs/lane_overlay.jpg differ
diff --git a/src/Active-Lane-Keeping-Assistant/docs/original.jpg b/src/Active-Lane-Keeping-Assistant/docs/original.jpg
new file mode 100644
index 0000000000..b3f8cf9179
Binary files /dev/null and b/src/Active-Lane-Keeping-Assistant/docs/original.jpg differ
diff --git a/src/Active-Lane-Keeping-Assistant/docs/pid_error.jpg b/src/Active-Lane-Keeping-Assistant/docs/pid_error.jpg
new file mode 100644
index 0000000000..dc95d25991
Binary files /dev/null and b/src/Active-Lane-Keeping-Assistant/docs/pid_error.jpg differ
diff --git a/src/Active-Lane-Keeping-Assistant/docs/pid_video.mp4 b/src/Active-Lane-Keeping-Assistant/docs/pid_video.mp4
new file mode 100644
index 0000000000..ce844ec9c5
Binary files /dev/null and b/src/Active-Lane-Keeping-Assistant/docs/pid_video.mp4 differ
diff --git a/src/Active-Lane-Keeping-Assistant/docs/region_of_interest.jpg b/src/Active-Lane-Keeping-Assistant/docs/region_of_interest.jpg
new file mode 100644
index 0000000000..f82bf78505
Binary files /dev/null and b/src/Active-Lane-Keeping-Assistant/docs/region_of_interest.jpg differ
diff --git a/src/Active-Lane-Keeping-Assistant/docs/search_window.jpg b/src/Active-Lane-Keeping-Assistant/docs/search_window.jpg
new file mode 100644
index 0000000000..a3f9191b75
Binary files /dev/null and b/src/Active-Lane-Keeping-Assistant/docs/search_window.jpg differ
diff --git a/src/Active-Lane-Keeping-Assistant/docs/simple_error.jpg b/src/Active-Lane-Keeping-Assistant/docs/simple_error.jpg
new file mode 100644
index 0000000000..57aeb3cd3e
Binary files /dev/null and b/src/Active-Lane-Keeping-Assistant/docs/simple_error.jpg differ
diff --git a/src/Active-Lane-Keeping-Assistant/docs/simple_video.mp4 b/src/Active-Lane-Keeping-Assistant/docs/simple_video.mp4
new file mode 100644
index 0000000000..011123ad7e
Binary files /dev/null and b/src/Active-Lane-Keeping-Assistant/docs/simple_video.mp4 differ
diff --git a/src/Active-Lane-Keeping-Assistant/docs/sliding_windows.jpg b/src/Active-Lane-Keeping-Assistant/docs/sliding_windows.jpg
new file mode 100644
index 0000000000..ed6e11bcf5
Binary files /dev/null and b/src/Active-Lane-Keeping-Assistant/docs/sliding_windows.jpg differ
diff --git a/src/Active-Lane-Keeping-Assistant/docs/threshold.jpg b/src/Active-Lane-Keeping-Assistant/docs/threshold.jpg
new file mode 100644
index 0000000000..d4148b9805
Binary files /dev/null and b/src/Active-Lane-Keeping-Assistant/docs/threshold.jpg differ
diff --git a/src/Active-Lane-Keeping-Assistant/docs/track.png b/src/Active-Lane-Keeping-Assistant/docs/track.png
new file mode 100644
index 0000000000..7c9a1293d9
Binary files /dev/null and b/src/Active-Lane-Keeping-Assistant/docs/track.png differ
diff --git a/src/Active-Lane-Keeping-Assistant/docs/warped_hist.jpg b/src/Active-Lane-Keeping-Assistant/docs/warped_hist.jpg
new file mode 100644
index 0000000000..4ca250a58e
Binary files /dev/null and b/src/Active-Lane-Keeping-Assistant/docs/warped_hist.jpg differ
diff --git a/src/Active-Lane-Keeping-Assistant/docs/warped_perspective.jpg b/src/Active-Lane-Keeping-Assistant/docs/warped_perspective.jpg
new file mode 100644
index 0000000000..d16397def7
Binary files /dev/null and b/src/Active-Lane-Keeping-Assistant/docs/warped_perspective.jpg differ
diff --git a/src/Active-Lane-Keeping-Assistant/img/examples/.keep b/src/Active-Lane-Keeping-Assistant/img/examples/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/Active-Lane-Keeping-Assistant/requirements.txt b/src/Active-Lane-Keeping-Assistant/requirements.txt
new file mode 100644
index 0000000000..effa44305c
--- /dev/null
+++ b/src/Active-Lane-Keeping-Assistant/requirements.txt
@@ -0,0 +1,4 @@
+matplotlib==3.7.1
+numpy==1.20.0
+opencv-python==4.7.0.72
+carla==0.9.13
\ No newline at end of file
diff --git a/src/Active-Lane-Keeping-Assistant/src/agent.py b/src/Active-Lane-Keeping-Assistant/src/agent.py
new file mode 100644
index 0000000000..091c3df28d
--- /dev/null
+++ b/src/Active-Lane-Keeping-Assistant/src/agent.py
@@ -0,0 +1,198 @@
+from __future__ import annotations
+
+import matplotlib.pyplot as plt
+import numpy as np
+import os
+
+class Agent():
+ """Agent that outputs the desired behaviour given
+ """
+
+ def __init__(self, tau_p:float = 0, tau_d:float = 0, tau_i:float = 0,
+ surface_lower_threshold:float = 20e6, throttle:float = 0.3,
+ suface_upper_threshold=30e6, controller:str = 'simple') -> None:
+ """Constructor
+
+ Args:
+ tau_p (float, optional): Parameter for the proportional part of the
+ PID-Controller. Defaults to 0.
+ tau_d (float, optional): Parameter for the derivative part of the
+ PID-Controller. Defaults to 0.
+ tau_i (float, optional): Parameter for the integral part of the
+ PID-Controller. Defaults to 0.
+ surface_lower_threshold (float, optional): Minimum amount of surface
+ that has to be detected in order to calculate new output. This
+ helps to find suitable lanes. Defaults to 20e6.
+ throttle (float, optional): Throttle to return at each time step.
+ Defaults to 0.3.
+ suface_upper_threshold (_type_, optional): Maximum amount of surface
+ that has to be detected in order to calculate new output. This
+ helps to find suitable lanes. Defaults to 30e6.
+ controller (str, optional): Identifies the controller to be used.
+ This can be one of the following:
+ - 'simple': hard coded controller that does not use any of
+ the tau parameters
+ - 'p': controller that only uses the proportional part
+ - 'pd': controller that only uses the proportional and
+ derivative part
+ - 'pid': pid-controller
+ Defaults to 'simple'.
+ """
+ self.tau_p = tau_p
+ self.tau_d = tau_d
+ self.tau_i = tau_i
+ self.surface_lower_threshold = surface_lower_threshold
+ self.surface_upper_threshold = suface_upper_threshold
+ self.prev_error = None
+ self.throttle = throttle
+ self.func = None
+ self.errors = []
+ self.controller_name = controller
+ self._select_controller_method(name=self.controller_name)
+
+ def _select_controller_method(self, name:str) -> None:
+ """Helper method to select the controller
+
+ Args:
+ name (str): Name of the controller.
+
+ Raises:
+ Exception: If the given name does not map to a controller method.
+ """
+ name = name.lower()
+ if name == 'simple':
+ self.func = Agent._simple_controller
+ elif name == 'p':
+ self.func = self._p_controller
+ elif name == 'pd':
+ self.func = self._pd_controller
+ elif name == 'pid':
+ self.func = self._pid_controller
+ else:
+ raise Exception(f'Controller name \'{name}\' is not applicable.')
+
+ def check_surface_area(self, detection_surface_area:float) -> bool:
+ """Checks Surface Area
+
+ Args:
+ detection_surface_area (float): _description_
+
+ Returns:
+ bool: Whether the given surface area is not inbetween the selected
+ interval.
+ """
+ lower = (detection_surface_area < self.surface_lower_threshold)
+ upper = (detection_surface_area > self.surface_upper_threshold)
+ return lower or upper
+
+ def show_error(self) -> None:
+ """Displays Error
+
+ Displays the difference to the center of all past steps.
+ """
+ plt.figure(1)
+ plt.clf()
+ x = range(len(self.errors))
+ y = self.errors
+ plt.plot(x, y, 'g', label=self.controller_name)
+ plt.plot(x, np.zeros(len(self.errors)), 'r', label='baseline')
+ plt.xlabel('Step')
+ plt.ylabel('Difference to Center')
+ plt.legend()
+ plt.pause(1e-10)
+
+ def save_error_fig(self, path:str, id:str) -> None:
+ """Save the Figure of the Error Plot
+
+ Args:
+ path (str): Folder to place the file in.
+ id (str): Unique identifier to prevent overwriting files.
+ """
+ plt.figure(1)
+ file_name = os.path.join(path, f'{id}_error.jpg')
+ plt.savefig(file_name)
+
+ def get_actions(self, detection_surface_area:float,
+ error:float) -> tuple[float, float]:
+ """Retrieve action based on Observation
+
+ Args:
+ error (float): Difference to the center of the detected lane.
+ detection_surface_area (float): Detected surface area.
+
+ Returns:
+ tuple[float, float]:
+ [0]: Steering angle to use.
+ [1]: Throttle to apply.
+ """
+ if (self.check_surface_area(detection_surface_area)):
+ steer = 0
+ if len(self.errors) > 0:
+ self.errors.append(self.errors[-1])
+ else:
+ self.errors.append(0)
+ else:
+ self.errors.append(error)
+ steer = self.func(error=error)
+ return steer, self.throttle
+
+ @staticmethod
+ def _simple_controller(error:float) -> float:
+ """Hard Coded Controller
+
+ Args:
+ error (float): Difference to the center of the detected lane.
+
+ Returns:
+ float: Steering angle to use.
+ """
+ if (abs(error) < 0.1):
+ steer = 0
+ elif error > 0:
+ steer = -0.75
+ else:
+ steer = 0.75
+ return steer
+
+ def _p_controller(self, error:float) -> float:
+ """Proportional Controller
+
+ Args:
+ error (float): Difference to the center of the detected lane.
+
+ Returns:
+ float: Steering angle to use.
+ """
+ steer = - self.tau_p * error
+ return steer
+
+ def _pd_controller(self, error:float) -> float:
+ """Proportional and Derivative Controller
+
+ Args:
+ error (float): Difference to the center of the detected lane.
+
+ Returns:
+ float: Steering angle to use.
+ """
+ if len(self.errors) > 2:
+ self.prev_error = self.errors[-2]
+ else:
+ self.prev_error = self.errors[-1]
+
+ deviation = error - self.prev_error
+ steer = - self.tau_d * deviation/0.1 + self._p_controller(error)
+ return steer
+
+ def _pid_controller(self, error:float) -> float:
+ """PID-Controller
+
+ Args:
+ error (float): Difference to the center of the detected lane.
+
+ Returns:
+ float: Steering angle to use.
+ """
+ sum_error = sum(self.errors)
+ steer = - self.tau_i * sum_error + self._pd_controller(error)
+ return steer
\ No newline at end of file
diff --git a/src/Active-Lane-Keeping-Assistant/src/lane.py b/src/Active-Lane-Keeping-Assistant/src/lane.py
new file mode 100644
index 0000000000..53f6211ac6
--- /dev/null
+++ b/src/Active-Lane-Keeping-Assistant/src/lane.py
@@ -0,0 +1,512 @@
+from __future__ import annotations
+
+import cv2
+import matplotlib.pyplot as plt
+import numpy as np
+
+from os.path import join
+
+class Lane:
+ """Detects Lanes in a given Image
+ """
+
+ def __init__(self, height:int, width:int, save:bool=False,
+ save_folder:str=join('img', 'examples')) -> None:
+ """Constructor
+
+ Args:
+ height (int): Height of the Image.
+ width (int): Width of the Image.
+ save (bool, optional): Whether to save the Images that will be
+ generated during the process. Defaults to False.
+ save_folder (str, optional): Folder to save the Images. This only
+ applies if save is true. Defaults to join('img', 'examples').
+ """
+ self.save = save
+ self.save_folder = save_folder
+ self.img_height = height
+ self.img_width = width
+ self.margin = int((1/12) * self.img_width)
+ self.DEGREE = 2
+
+ # Variables to store for lines
+ self.x_left = None
+ self.x_right = None
+ self.y_left = None
+ self.y_right = None
+
+ # Variables for fill
+ self.x_left_fill = None
+ self.x_right_fill = None
+ self.y_left_fill = None
+ self.y_right_fill = None
+
+ def get_lines(self, img:np.ndarray) -> np.ndarray:
+ """Get white Lines
+
+ Filters out lighter parts of the image.
+
+ Args:
+ img (np.ndarray): Image on which the changes are to be applied.
+
+ Returns:
+ np.ndarray: Image with changes applied.
+ """
+ hls = cv2.cvtColor(img, cv2.COLOR_BGR2HLS)
+
+ # Create threshold matrix to differentiate black and white based on the
+ # lightness (of HSL).
+ _, binary = cv2.threshold(hls[:, :, 1], 150, 255, cv2.THRESH_BINARY)
+ binary_blured = cv2.GaussianBlur(binary, (3, 3), 0)
+
+ if self.save:
+ cv2.imwrite(join(self.save_folder, 'lines.jpg'), binary_blured)
+
+ return binary_blured
+
+ def extract_roi(self, img:np.ndarray, bottom:int=350, top:int=260,
+ left:int=200, right:int=440) -> tuple[np.ndarray, np.ndarray]:
+ """Extract the Region of Interest
+
+ Extracts the region of interest and transforms it to fit the original
+ image size.
+
+ Args:
+ img (np.ndarray): Image on which the changes are to be applied.
+ Should generally be returned by get_lines.
+ bottom (int, optional): Bottom of the trapezoid. Defaults to 350.
+ top (int, optional): Top of the trapezoid. Defaults to 260.
+ left (int, optional): Top-Left of the trapezoid. Defaults to 200.
+ right (int, optional): Top-Right of the trapezoid. Defaults to 440.
+
+ Returns:
+ np.ndarray: Image with changes applied.
+ tuple[np.ndarray, np.ndarray]:
+ [0]: Image showing the warped perspective.
+ [1]: Matrix representing the inverse operation to undo the
+ transformation.
+ """
+ # Corners of the trapezoidal relevant area.
+ roi_points = np.float32([
+ [left, top],
+ [0, bottom], # Bottom-Left
+ [self.img_width, bottom], # Bottom-Right
+ [right, top]
+ ])
+
+ # Padding to apply on the transformed image.
+ padding = int(0.25 * self.img_width)
+ # Corners of the trapezoidal relevant area after transformation.
+ desired_roi_points = np.float32([
+ [padding, 0], # Top-left corner
+ [padding, self.img_height], # Bottom-left corner
+ [self.img_width-padding, self.img_height], # Bottom-right corner
+ [self.img_width-padding, 0] # Top-right corner
+ ])
+
+ perspective_transform = cv2.getPerspectiveTransform(roi_points,
+ desired_roi_points)
+ inverse_perspective_transform = cv2 \
+ .getPerspectiveTransform(desired_roi_points, roi_points)
+ perspective = cv2.warpPerspective(img, perspective_transform,
+ (self.img_width, self.img_height), flags=cv2.INTER_LINEAR)
+ _, warped_perspective_binary = cv2.threshold(perspective, 127, 255,
+ cv2.THRESH_BINARY)
+
+ if self.save:
+ COLOR_TRAPEZE = (147,20,255)
+ warped_perspective = cv2.polylines(warped_perspective_binary.copy(),
+ np.int32([desired_roi_points]), True, COLOR_TRAPEZE, 3)
+ cv2.imwrite(join(self.save_folder, 'warped_perspective.jpg'),
+ warped_perspective)
+
+ return warped_perspective_binary, inverse_perspective_transform
+
+ def get_hist(self,
+ img:np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+ """Generates Historgramm of light Parts inside the image.
+
+ Args:
+ img (np.ndarray): Image on which the changes are to be applied.
+ Should generally be returned by extract_roi.
+
+ Returns:
+ tuple[np.ndarray, np.ndarray, np.ndarray]:
+ [0]: Values of the histogram.
+ [1]: Width-index of the location of the left lane.
+ [2]: Width-index of the location of the right lane.
+ """
+ hist = np.sum(img[int(img.shape[0]/2):,:], axis=0)
+ midpoint = np.int(hist.shape[0]/2)
+ left_lane = np.argmax(hist[:midpoint])
+ right_lane = np.argmax(hist[midpoint:]) + midpoint
+
+ if self.save:
+ _, (ax1, ax2) = plt.subplots(2, figsize=(8,8))
+ plt.subplots_adjust(hspace=0.25)
+ ax1.imshow(img, cmap='gray', aspect='auto')
+ ax1.set_title('Transformation of the Binary Representation')
+ ax2.plot(hist)
+ ax2.set_title('Histogram of the Deflections')
+ plt.savefig(join(self.save_folder, 'warped_hist.jpg'))
+
+ return hist, left_lane, right_lane
+
+ def get_line_fits(self, img:np.ndarray, max_left_idx:np.ndarray,
+ max_right_idx:np.ndarray,
+ number_windows:int=10) -> tuple[np.ndarray, np.ndarray]:
+ """Apply Sliding Windows and Retrieve Lines
+
+ Args:
+ img (np.ndarray): Image on which the changes are to be applied.
+ Should generally be the original image.
+ max_left_idx (np.ndarray): Index of the detected left line as
+ returned by get_hist.
+ max_right_idx (np.ndarray): Index of the detected right line as
+ returned by get_hist.
+ number_windows (int, optional): Number of windows to create.
+ Defaults to 10.
+
+ Returns:
+ tuple[np.ndarray, np.ndarray]:
+ [0]: Second order polynomial curve to represent the left line.
+ [1]: Second order polynomial curve to represent the right line.
+ """
+ # Number of pixels to identify line.
+ MINIMAL_AREA = int((1/24) * self.img_width)
+ WINDOW_HEIGHT = np.int(img.shape[0]/number_windows)
+
+ # Retrive x and y coordinates of white pixels (non-zero).
+ nonzero_y, nonzero_x = img.nonzero()
+
+ # Store the pixel indices for the left and right lane lines
+ left_lane_idxs = []
+ right_lane_idxs = []
+
+ WINDOW_COLOR = (255,255,255)
+ WINDOW_THICKNESS = 2
+
+ for window in range(number_windows):
+ y_low = img.shape[0] - (window + 1) * WINDOW_HEIGHT
+ y_high = img.shape[0] - window * WINDOW_HEIGHT
+ x_low_left = max_left_idx - self.margin
+ x_high_left = max_left_idx + self.margin
+ x_low_right = max_right_idx - self.margin
+ x_high_right = max_right_idx + self.margin
+
+ # Draw windows.
+ cv2.rectangle(img, (x_low_left, y_low), (x_high_left, y_high),
+ WINDOW_COLOR, WINDOW_THICKNESS)
+ cv2.rectangle(img, (x_low_right, y_low),(x_high_right, y_high),
+ WINDOW_COLOR, WINDOW_THICKNESS)
+
+ # Find the white pixels in x and y inside the window.
+ non_zero_left_idxs = ((nonzero_y >= y_low) & (nonzero_y < y_high)
+ & (nonzero_x >= x_low_left) & (nonzero_x < x_high_left)) \
+ .nonzero()[0]
+ non_zero_right_idxs = ((nonzero_y >= y_low) & (nonzero_y < y_high)
+ & (nonzero_x >= x_low_right) & (nonzero_x < x_high_right)) \
+ .nonzero()[0]
+ left_lane_idxs.append(non_zero_left_idxs)
+ right_lane_idxs.append(non_zero_right_idxs)
+
+ # Recenter next window on mean position.
+ if len(non_zero_left_idxs) > MINIMAL_AREA:
+ max_left_idx = np.int(np.mean(nonzero_x[non_zero_left_idxs]))
+ if len(non_zero_right_idxs) > MINIMAL_AREA:
+ max_right_idx = np.int(np.mean(nonzero_x[non_zero_right_idxs]))
+
+ left_lane_idxs = np.concatenate(left_lane_idxs)
+ right_lane_idxs = np.concatenate(right_lane_idxs)
+
+ # If valid lane has been found, update values for next image. This
+ # ensures that if no lane has been found, the detection within the next
+ # image are still possible. However, this requires to detect a lane
+ # within the first image after the lane object has been created.
+ is_invalid = (sum(nonzero_x[left_lane_idxs]) == 0) or \
+ (sum(nonzero_y[left_lane_idxs]) == 0)
+ is_false = (nonzero_x[left_lane_idxs].size == 0) or \
+ (nonzero_x[left_lane_idxs] is None)
+ if not (is_false or is_invalid):
+ self.x_left = nonzero_x[left_lane_idxs].copy()
+
+ is_false = (nonzero_y[left_lane_idxs].size == 0) or \
+ (nonzero_y[left_lane_idxs] is None)
+ if not (is_false or is_invalid):
+ self.y_left = nonzero_y[left_lane_idxs].copy()
+
+ is_invalid = (sum(nonzero_x[right_lane_idxs]) == 0) or \
+ (sum(nonzero_y[right_lane_idxs]) == 0)
+ is_false = (nonzero_x[right_lane_idxs].size == 0) or \
+ (nonzero_x[right_lane_idxs] is None)
+ if not (is_false or is_invalid):
+ self.x_right = nonzero_x[right_lane_idxs].copy()
+
+ is_false = (nonzero_y[right_lane_idxs].size == 0) or \
+ (nonzero_y[right_lane_idxs] is None)
+ if not (is_false or is_invalid):
+ self.y_right = nonzero_y[right_lane_idxs].copy()
+
+ # Fit a second order polynomial curve to represent the lines.
+ left_line = np.polyfit(self.y_left, self.x_left, self.DEGREE)
+ right_line = np.polyfit(self.y_right, self.x_right, self.DEGREE)
+
+ # Retrieve x and y coordinates by applying the ploynomical curves.
+ y = np.linspace(0, img.shape[0]-1, img.shape[0])
+ left_fit_x = left_line[0]*y**2 + left_line[1]*y + \
+ left_line[2]
+ right_fit_x = right_line[0]*y**2 + right_line[1]*y + \
+ right_line[2]
+
+ if self.save:
+ out_img = np.dstack((img, img, img)) * 255
+
+ left_lane_idxs = ((nonzero_x > (left_line[0]*(nonzero_y**2) +
+ left_line[1]*nonzero_y + left_line[2] - self.margin)) &
+ (nonzero_x < (left_line[0]*(nonzero_y**2) +
+ left_line[1]*nonzero_y + left_line[2] + self.margin)))
+ right_lane_idxs = ((nonzero_x > (right_line[0]*(nonzero_y**2) +
+ right_line[1]*nonzero_y + right_line[2] - self.margin)) &
+ (nonzero_x < (right_line[0]*(nonzero_y**2) +
+ right_line[1]*nonzero_y + right_line[2] + self.margin)))
+
+ # Apply color to the left and right line pixels.
+ out_img[nonzero_y[left_lane_idxs], nonzero_x[left_lane_idxs]] = \
+ [255, 0, 0]
+ out_img[nonzero_y[right_lane_idxs], nonzero_x[right_lane_idxs]] = \
+ [0, 0, 255]
+
+ figure, (ax1, ax2) = plt.subplots(2)
+ figure.tight_layout(pad=3.0)
+ ax1.imshow(img, cmap='gray')
+ ax2.imshow(out_img)
+ ax2.plot(left_fit_x, y, color='yellow')
+ ax2.plot(right_fit_x, y, color='yellow')
+ ax1.set_title('Sliding Windows of the Transformed Image')
+ ax2.set_title('Detected Track Lines')
+ plt.savefig(join(self.save_folder, 'sliding_windows.jpg'))
+
+ return left_line, right_line
+
+ def get_search_window(self, img:np.ndarray, left_line:np.ndarray,
+ right_line:np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+ """Retrieve the Search Window for Lines
+
+ Args:
+ img (np.ndarray): Image on which the changes are to be applied.
+ Should generally be the original image.
+ left_line (np.ndarray): Second order polynomial curve to represent
+ the left line.
+ right_line (np.ndarray): Second order polynomial curve to represent
+ the right line.
+
+ Returns:
+ tuple[np.ndarray, np.ndarray, np.ndarray]:
+ [0]: Values returned by applying the polynomial curve to using
+ the y values for the left line.
+ [1]: Values returned by applying the polynomial curve to using
+ the y values for the right line.
+ [2]: Generated y values to get a continuous look.
+ """
+ # Retrive x and y coordinates of white pixels (non-zero).
+ nonzero_y, nonzero_x = img.nonzero()
+
+ # Get indices for he left and right line pixels.
+ left_lane_idxs = ((nonzero_x > (left_line[0]*(nonzero_y**2) +
+ left_line[1]*nonzero_y + left_line[2] - self.margin)) &
+ (nonzero_x < (left_line[0]*(nonzero_y**2) + left_line[1]*nonzero_y +
+ left_line[2] + self.margin)))
+ right_lane_idxs = ((nonzero_x > (right_line[0]*(nonzero_y**2) +
+ right_line[1]*nonzero_y + right_line[2] - self.margin)) &
+ (nonzero_x < (right_line[0]*(nonzero_y**2) +
+ right_line[1]*nonzero_y + right_line[2] + self.margin)))
+
+ # If valid lane has been found, update values for next image. This
+ # ensures that if no lane has been found, the detection within the next
+ # image are still possible. However, this requires to detect a lane
+ # within the first image after the lane object has been created.
+ is_invalid = (sum(nonzero_x[left_lane_idxs]) == 0) or \
+ (sum(nonzero_y[left_lane_idxs]) == 0)
+ is_false = (nonzero_x[left_lane_idxs].size == 0) or \
+ (nonzero_x[left_lane_idxs] is None)
+ if not (is_false or is_invalid):
+ self.x_left_fill = nonzero_x[left_lane_idxs].copy()
+
+ is_false = (nonzero_y[left_lane_idxs].size == 0) or \
+ (nonzero_y[left_lane_idxs] is None)
+ if not (is_false or is_invalid):
+ self.y_left_fill = nonzero_y[left_lane_idxs].copy()
+
+ is_invalid = (sum(nonzero_x[right_lane_idxs]) == 0) or \
+ (sum(nonzero_y[right_lane_idxs]) == 0)
+ is_false = (nonzero_x[right_lane_idxs].size == 0) or \
+ (nonzero_x[right_lane_idxs] is None)
+ if not (is_false or is_invalid):
+ self.x_right_fill = nonzero_x[right_lane_idxs].copy()
+
+ is_false = (nonzero_y[right_lane_idxs].size == 0) or \
+ (nonzero_y[right_lane_idxs] is None)
+ if not (is_false or is_invalid):
+ self.y_right_fill = nonzero_y[right_lane_idxs].copy()
+
+ # Fit a second order polynomial curve to represent the outer lines.
+ left_fit = np.polyfit(self.y_left_fill, self.x_left_fill, self.DEGREE)
+ right_fit = np.polyfit(self.y_right_fill, self.x_right_fill,
+ self.DEGREE)
+
+ # Retrieve x and y coordinates by applying the ploynomical curves.
+ y = np.linspace(0, img.shape[0]-1, img.shape[0])
+ left_fit_x = left_fit[0]*y**2 + left_fit[1]*y + left_fit[2]
+ right_fit_x = right_fit[0]*y**2 + right_fit[1]*y + right_fit[2]
+
+ if self.save:
+ out_img = np.dstack((img, img, img))*255
+ window_img = np.zeros_like(out_img)
+
+ # Apply color to the left and right line pixels.
+ out_img[nonzero_y[left_lane_idxs], nonzero_x[left_lane_idxs]] = \
+ [255, 0, 0]
+ out_img[nonzero_y[right_lane_idxs], nonzero_x[right_lane_idxs]] = \
+ [0, 0, 255]
+
+ # Create a polygon to represent the search window area and convert
+ # the x and y points into a usable format for cv2.fillPoly().
+ left_line_window1 = np.array([np.transpose(
+ np.vstack([left_fit_x-self.margin, y]))])
+ left_line_window2 = np.array([np.flipud(np.transpose(
+ np.vstack([left_fit_x+self.margin, y])))])
+ left_line_pts = np.hstack((left_line_window1, left_line_window2))
+ right_line_window1 = np.array([np.transpose(
+ np.vstack([right_fit_x-self.margin, y]))])
+ right_line_window2 = np.array([np.flipud(np.transpose(
+ np.vstack([right_fit_x+self.margin, y])))])
+ right_line_pts = np.hstack((right_line_window1, right_line_window2))
+
+ # Draw the lane onto the warped blank image
+ cv2.fillPoly(window_img, np.int_([left_line_pts]), (0, 255, 0))
+ cv2.fillPoly(window_img, np.int_([right_line_pts]), (0, 255, 0))
+ result = cv2.addWeighted(out_img, 1, window_img, 0.3, 0)
+
+ _, ax= plt.subplots(1)
+ ax.imshow(result)
+ ax.plot(left_fit_x, y, color='yellow')
+ ax.plot(right_fit_x, y, color='yellow')
+ ax.set_title('Search Window on the Transformed Image')
+ plt.savefig(join(self.save_folder, 'search_window.jpg'))
+
+ return left_fit_x, right_fit_x, y
+
+ def show_lane(self, orig_img:np.ndarray, img:np.ndarray,
+ left_fit_x:np.ndarray, right_fit_x:np.ndarray, y:np.ndarray,
+ inverse_perspective_transform:np.ndarray) -> tuple[np.ndarray, int]:
+ """Draw Lane on original Image
+
+ Args:
+ orig_img (np.ndarray): Original image before any changes.
+ img (np.ndarray): Image after warping.
+ left_fit_x (np.ndarray): Values returned by applying the polynomial
+ curve to using the y values for the left line.
+ right_fit_x (np.ndarray): Values returned by applying the polynomial
+ curve to using the y values for the right line.
+ y (np.ndarray): Generated y values to get a continuous look.
+ inverse_perspective_transform (np.ndarray): Matrix representing the
+ inverse operation to undo the transformation.
+
+ Returns:
+ tuple[np.ndarray, int]:
+ [0]: Original image with lane drawn on.
+ [1]: Detected surface area multiplied by 255.
+ """
+
+ # Generate image to draw on.
+ warp_zero = np.zeros_like(img).astype(np.uint8)
+ color_warp = np.dstack((warp_zero, warp_zero, warp_zero))
+
+ # Converting the x and y points into a usable format for cv2.fillPoly().
+ pts_left = np.array([np.transpose(np.vstack([left_fit_x, y]))])
+ pts_right = np.array([np.flipud(np.transpose(
+ np.vstack([right_fit_x, y])))])
+ pts = np.hstack((pts_left, pts_right))
+
+ # Draw lane on warped image.
+ cv2.fillPoly(color_warp, np.int_([pts]), (0, 255, 0))
+
+ # Undo warp transformation.
+ newwarp = cv2.warpPerspective(color_warp, inverse_perspective_transform,
+ (orig_img.shape[1], orig_img.shape[0]))
+
+ # Draw lane on original image.
+ result = cv2.addWeighted(orig_img, 1, newwarp, 0.3, 0)
+
+ if self.save:
+ _, ax = plt.subplots(1)
+ ax.imshow(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
+ ax.set_title('Original Image with Recognised Track')
+ plt.savefig(join(self.save_folder, 'lane_overlay.jpg'))
+
+ return result, np.sum(color_warp)
+
+ def get_car_position(self, img:np.ndarray, left_line:np.ndarray,
+ right_line:np.ndarray) -> tuple[np.ndarray, float]:
+ """Retrieve the Error
+
+ Args:
+ img (np.ndarray): Original image with lane drawn on.
+ left_line (np.ndarray): Second order polynomial curve to represent
+ the left line.
+ right_line (np.ndarray): Second order polynomial curve to represent
+ the right line.
+
+ Returns:
+ tuple[np.ndarray, float]:
+ [0]: Original image with lane and offset drawn on.
+ [1]: Calculated offset to the center of the detected lane.
+ """
+ M_PER_PIX = 3.7 / 781 # Meters per pixel
+
+ # Retrieve the position of the car assuming it is the center of the
+ # image.
+ car_location = self.img_width / 2
+
+ # Retrieve x coordinates of bottom from the lane.
+ bottom_left = left_line[0]*self.img_height**2 + \
+ left_line[1]*self.img_height + left_line[2]
+ bottom_right = right_line[0]*self.img_height**2 + \
+ right_line[1]*self.img_height + right_line[2]
+
+ center_lane = (bottom_right - bottom_left)/2 + bottom_left
+ center_offset = (np.abs(car_location) - np.abs(center_lane)) * \
+ M_PER_PIX * 100
+
+ cv2.putText(img, (f'Difference to Center: '
+ f'{str(round(center_offset, 5))} cm'), (int((5/600)*self.img_width),
+ int((20/338)*self.img_height)), cv2.FONT_HERSHEY_SIMPLEX,
+ (float((0.5/600)*self.img_width)), (255,255,255),2,cv2.LINE_AA)
+
+ return img, center_offset
+
+ def pipe(self, img:np.ndarray) -> tuple[np.ndarray, float, int]:
+ """Pipes Image through every Step in right order.
+
+ Args:
+ img (np.ndarray): Original image to detect lane on.
+
+ Returns:
+ tuple[np.ndarray, float, int]:
+ [0]: Original image with lane and offset drawn on.
+ [1]: Calculated offset to the center of the detected lane.
+ [2]: Detected surface area multiplied by 255.
+ """
+ original_image = img.copy()
+ img = self.get_lines(img)
+ img, inverse_perspective_transform = self.extract_roi(img)
+ _, max_left_idx, max_right_idx = self.get_hist(img)
+ left_line, right_line = self.get_line_fits(img.copy(),
+ max_right_idx, max_left_idx)
+ left_fit_x, right_fit_x, y = self.get_search_window(img.copy(),
+ left_line, right_line)
+ img, detection_surface_area = self.show_lane(original_image,
+ img.copy(), left_fit_x, right_fit_x, y,
+ inverse_perspective_transform)
+ img, error = self.get_car_position(img, left_line, right_line)
+ return img, error, detection_surface_area
\ No newline at end of file
diff --git a/src/Active-Lane-Keeping-Assistant/src/main.py b/src/Active-Lane-Keeping-Assistant/src/main.py
new file mode 100644
index 0000000000..72f3c002ed
--- /dev/null
+++ b/src/Active-Lane-Keeping-Assistant/src/main.py
@@ -0,0 +1,297 @@
+from __future__ import annotations
+
+import argparse
+import logging
+import numpy as np
+import os
+import warnings
+
+from itertools import count
+from os.path import join
+from typing import Iterable
+
+from agent import Agent
+from recorder import Recorder
+from world import World
+
+def log_parameters(id:str, logger:logging.Logger, parameters: Iterable[float],
+ delta_parameters: Iterable[float], error:float, steps:int) -> None:
+ """Log one set of Parameters
+
+ Args:
+ id (str): An unique identifier used to identify the run.
+ logger (logging.Logger): Logger that writes logs.
+ parameters (Iterable[float]): Parameters of the controller.
+ delta_parameters (Iterable[float]): Delta parameters for adaption of the
+ parameters.
+ error (float): Summed error received by the environment.
+ steps (int): Number of steps taken within the run.
+ """
+ logger.info((f'id: {id} | parameters: {parameters} | delta_parameters: '
+ f'{delta_parameters} | error: {error:.5f} | steps: {steps:5d}'))
+
+def run(id:str, path:str, world:World, agent:Agent, steps:int = 100,
+ save:bool=False) -> tuple[float, int]:
+ """One Epsisode inside the World
+
+ Performs one run inside the world until the maximum steps is reached or a
+ collision was detected.
+
+ Args:
+ id (str): An unique identifier used to identify the run.
+ path (str): Path where all files for a run will be stored.
+ world (World): Environment for the simulation.
+ agent (Agent): Agent to decide on actions.
+ steps (int, optional): Number of maximum amount of steps to take until
+ termination. Defaults to 100.
+ save (bool, optional): Whether to create a video of the run. Defaults to
+ False.
+
+ Returns:
+ tuple[float, int]:
+ [0]: Sum of errors as returned by the world.
+ [1]: Number of steps taken.
+ """
+ error, detection_surface_area, _, _ = world.reset()
+
+ if save:
+ recorder = Recorder(folder=path)
+ recorder.init_new_video(id=id)
+
+ for i in range(steps):
+ steer, throttle = agent.get_actions(detection_surface_area, error)
+ error, detection_surface_area, img, collision_detected = world.step(
+ steer=steer, throttle=throttle)
+ agent.show_error()
+
+ if save:
+ recorder.add_image(img=img)
+
+ if collision_detected:
+ agent.errors = agent.errors + [max(agent.errors)] * (steps-i)
+ agent.show_error()
+ break
+
+ agent.save_error_fig(path, id)
+ if save:
+ recorder.close_recording()
+
+ return sum(agent.errors), i
+
+def check_supported_controller(name:str) -> str:
+ """Check supported controller
+
+ Checks if the provided controller string is allowed for the adaption mode.
+
+ Args:
+ name (str): Identifies the controller to be used for the run.
+
+ Returns:
+ str: Identifies the controller to be used for the run. This may be
+ different from the input name.
+ """
+ if name.lower() != 'pid':
+ warnings.warn(('Twiddle does not support other controllers than '
+ '\'pid\'. Setting controller to \'pid\''))
+ return 'pid'
+ return name
+
+def get_logger(id:str, path:str, debug:bool=False) -> logging.Logger:
+ """Retrieve logger
+
+ The returned logger 'pacman_rl' writes to a file inside the 'logs' folder
+ and streams the logs to the console.
+
+ Args:
+ id (str): An unique identifier used to identify the log file.
+ path (str): Path where all files for a run will be stored.
+ debug (bool, optional): Whether to use the debug mode for logging. If
+ False, only infos will be logged. Defaults to False.
+
+ Returns:
+ logging.Logger: Logger that streams and writes logs.
+ """
+ if debug:
+ level = logging.DEBUG
+ else:
+ level = logging.INFO
+
+ formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')
+ logger = logging.getLogger('active_lane_keeping_assistant')
+ logger.setLevel(level=level)
+ fh = logging.FileHandler(join(path, f'alka_{id}.log'), mode='a')
+ sh = logging.StreamHandler()
+ sh.setFormatter(formatter)
+ fh.setFormatter(formatter)
+ logger.addHandler(fh)
+ logger.addHandler(sh)
+
+ return logger
+
+def twiddle(id:str, path:str, tolerance:float=0.2, controller:str='pid',
+ steps:int=100, debug:bool=False) -> None:
+ """Twiddle-Algorithm
+
+ Perform the twiddle algorithm to receive near optimal parameters for the
+ PID-controller.
+
+ Args:
+ id (str): An unique identifier used to identify the run.
+ path (str): Path where all files for a run will be stored.
+ tolerance (float, optional): Value that must be undershot for the
+ algorithm to be terminated. Defaults to 0.2.
+ controller (str, optional): Identifies the controller to be used for the
+ run. Defaults to 'pid'.
+ steps (int, optional): Number of maximum amount of steps to take until
+ termination. Defaults to 100.
+ debug (bool, optional): Whether to use the debug mode for logging.
+ Defaults to False.
+ """
+ logger = get_logger(id=id, path=path, debug=debug)
+ controller = check_supported_controller(name=controller)
+
+ logger.info(f'twiddle: {id}')
+ logger.info((f'tolerance: {tolerance} | controller: {controller} | steps: '
+ f'{steps}'))
+
+ parameters = np.zeros(3)
+ delta_parameters = np.ones(3)
+
+ world = World()
+ agent = Agent(controller=controller, tau_p=parameters[0],
+ tau_i=parameters[1], tau_d=parameters[2])
+
+ best_err, steps_taken = run(id, path, world, agent, steps, save=False)
+ best_parameters = parameters
+ best_id = id
+
+ log_parameters(id, logger, parameters, delta_parameters, best_err,
+ steps_taken)
+
+ for t in count():
+ if (sum(delta_parameters) < tolerance):
+ break
+ for i in range(len(parameters)):
+ new_id = f'{id}_{t}_{i}_a'
+ world.id = new_id
+ parameters[i] += delta_parameters[i]
+
+ agent = Agent(controller=controller, tau_p=parameters[0],
+ tau_i=parameters[1], tau_d=parameters[2])
+ error, steps_taken = run(new_id, path, world, agent, steps)
+ log_parameters(new_id, logger, parameters, delta_parameters, error,
+ steps_taken)
+
+ if abs(error) < abs(best_err):
+ logger.debug(f'Follow same direction for parameter {i}.')
+ best_err = error
+ best_parameters = parameters
+ best_id = new_id
+ delta_parameters[i] *= 1.1
+ else:
+ logger.debug(f'Trying opposite direction for parameter {i}.')
+ new_id = f'{id}_{t}_{i}_a'
+ world.id = new_id
+ parameters[i] -= 2*delta_parameters[i]
+
+ agent = Agent(controller=controller,
+ tau_p=parameters[0], tau_i=parameters[1],
+ tau_d=parameters[2])
+ error, steps_taken = run(new_id, path, world, agent, steps)
+ log_parameters(new_id, logger, parameters, delta_parameters,
+ error, steps_taken)
+
+ if abs(error) < abs(best_err):
+ logger.debug((f'Follow opposite direction for parameter '
+ f'{i}.'))
+ best_err = error
+ best_parameters = parameters
+ best_id = new_id
+ delta_parameters[i] *= 1.1
+ else:
+ logger.debug((f'Neither direction worked for {i}. '
+ f'Decreasing step size.'))
+ parameters[i] += delta_parameters[i]
+ delta_parameters[i] *= 0.9
+
+ logger.info((f'best run - id: {best_id} | parameters: {best_parameters} | '
+ f'error: {best_err:.5f}'))
+
+def no_adapt(id:str, path:str, steps:int=100, controller:str='simple') -> None:
+ """Run without adaption
+
+ Run the environment without the adaption of the parameters from the
+ controller.
+
+ Args:
+ id (str): Unique identifier for the run.
+ path (str): Path where all files for a run will be stored.
+ steps (int, optional): Number of steps to take within the environment.
+ Defaults to 100.
+ controller (str, optional): Identifies the controller to be used for the
+ run. Defaults to 'simple'.
+ """
+ world = None
+ try:
+ world = World()
+ agent = Agent(controller=controller)
+ run(id=id, path=path, world=world, agent=agent, steps=steps, save=True)
+ finally:
+ if world is not None:
+ world.close()
+
+
+def make_path(folder:str, id:str) -> str:
+ """Create folder
+
+ Checks if a folder for the id already exists and creates the folder if
+ necessary.
+
+ Args:
+ folder (str): Name of the folder where the folder for the run will be
+ located.
+ id (str): Unique identifier for the run. This will be used to identify
+ the specific folder for this run.
+
+ Returns:
+ str: Path including the id of the run.
+ """
+ path = join(os.getcwd(), folder, id)
+
+ if not os.path.exists(path):
+ os.makedirs(path)
+ else:
+ warnings.warn('Path already exists. Files may be overwritten.')
+
+ return path
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(prog='Active Lane Keeping System',
+ description='')
+ parser.add_argument('-id', '--identifier', type=str, help=('Unique '
+ 'identifier used to identify the run.'), dest='id', required=True)
+ parser.add_argument('-c', '--controller', default='pid', type=str,
+ choices=['simple', 'p', 'pd', 'pid'], help=('The method used to '
+ 'control the car.'), dest='controller')
+ parser.add_argument('-s', '--steps', default=100, type=int, help=('The '
+ 'number of steps that the agent controls the car. This does not '
+ 'include the inital setup driving to the fourth lane.'), dest='steps')
+ parser.add_argument('-a', '--adapt', action='store_true',
+ help=('Whether to adapt the parameters. This only takes effect if not '
+ '\'simple\' is selected as controller.'), dest='adapt')
+ parser.add_argument('-t', '--tolerance', default=0.2, type=float,
+ help=('Tolerance range for the adaption of the parameters. This takes '
+ 'effect only if adapt is set to true.'), dest='tolerance')
+ parser.add_argument('-d', '--debug', action='store_true',
+ help=('Whether to use debug mode for logging. This takes effect only '
+ 'if adapt is set to true.'), dest='debug')
+ args = parser.parse_args()
+
+ path = make_path(folder='assets', id=args.id)
+
+ if args.adapt:
+ twiddle(id=args.id, path=path, tolerance=args.tolerance,
+ controller=args.controller, steps=args.steps, debug=args.debug)
+ else:
+ no_adapt(id=args.id, path=path, steps=args.steps,
+ controller=args.controller)
\ No newline at end of file
diff --git a/src/Active-Lane-Keeping-Assistant/src/recorder.py b/src/Active-Lane-Keeping-Assistant/src/recorder.py
new file mode 100644
index 0000000000..043a714df5
--- /dev/null
+++ b/src/Active-Lane-Keeping-Assistant/src/recorder.py
@@ -0,0 +1,63 @@
+import cv2
+import numpy as np
+
+from os.path import join
+
+class Recorder():
+ """The recorder records a run of a simulation and saves it as a video file
+ """
+
+ def __init__(self,
+ fps:int=30,
+ height:int=480,
+ width:int=640,
+ folder:str='videos') -> None:
+ """Constructor for the recorder
+
+ Args:
+ fps (int, optional): Number of fps for video clip. Defaults to 15.
+ height (int, optional): Height of video. Defaults to 480.
+ width (int, optional): Width of video. Defaults to 640.
+ folder (str, optional): Storage location. Defaults to 'videos'.
+ """
+
+ self.cameraCapture = cv2.VideoCapture(0)
+ self.height = height
+ self.width = width
+ self.fps = float(fps)
+ self.folder = folder
+
+ self.fourcc = cv2.VideoWriter_fourcc(*'mp4v')
+ self.video = None
+
+ def init_new_video(self,
+ id:str) -> None:
+ """Triggers the process of recording a new video
+
+ Args:
+ id (str): Id/name of the video file
+ """
+ self.video = cv2.VideoWriter(join(self.folder, f'{id}.mp4'),
+ self.fourcc,
+ self.fps,
+ (self.width, self.height))
+
+ def add_image(self,
+ img:np.ndarray) -> None:
+ """Appends a new image to the video
+
+ Args:
+ img (np.ndarray): Frame to append to the video.
+ """
+ self.video.write(img)
+
+ def close_recording(self) -> None:
+ """Stops the recording
+
+ Raises:
+ Exception:
+ If no recording has been initiated, the video can't be released.
+ """
+ if self.video is None:
+ raise Exception('No video initialized.')
+ self.video.release()
\ No newline at end of file
diff --git a/src/Active-Lane-Keeping-Assistant/src/world.py b/src/Active-Lane-Keeping-Assistant/src/world.py
new file mode 100644
index 0000000000..3e579ea40b
--- /dev/null
+++ b/src/Active-Lane-Keeping-Assistant/src/world.py
@@ -0,0 +1,195 @@
+from __future__ import annotations
+
+import carla
+import cv2
+import numpy as np
+import queue
+
+from lane import Lane
+
+class World():
+ """Environment that wraps around the Carla-API
+ """
+
+ def __init__(self, server_ip:str='127.0.0.1', port:int=2000,
+ timeout:float=5.0, map:str='Town04', image_height:int=480,
+ image_width:int=640, fov:int=110,
+ time_difference:float=0.01) -> None:
+ """Constructor
+
+ Args:
+ server_ip (str, optional): IP address of the server running Carla.
+ Defaults to '127.0.0.1'.
+ port (int, optional): Port used to communicate with the server.
+ Defaults to 2000.
+ timeout (float, optional): Time difference after which the
+ connection attempt with the server is aborted. Note that several
+ attempts may be needed. Defaults to 5.0.
+ map (str, optional): Map to be used. The available options depend on
+ the Carla simulator used. Defaults to 'Town04'.
+ image_height (int, optional): Height of the image to return.
+ Defaults to 480.
+ image_width (int, optional): Width of the image to return. Defaults
+ to 640.
+ fov (int, optional): Field of view of the camera. Defaults to 110.
+ time_difference (float, optional): Difference of time simulated at
+ each step. Defaults to 0.01.
+ """
+
+ self.image_height = image_height
+ self.image_width = image_width
+
+ self.lane = Lane(height=self.image_height, width=self.image_width)
+
+ self.fov = fov
+ self.client = carla.Client(server_ip, port)
+ self.client.set_timeout(timeout)
+ self.world = self.client.load_world(map)
+ settings = self.world.get_settings()
+ settings.fixed_delta_seconds = time_difference
+ # Client and server work synchronously.
+ settings.synchronous_mode = True
+ self.world.apply_settings(settings)
+ self.blueprint_library = self.world.get_blueprint_library()
+ # Use Tesla Model 3 as Car
+ self.bp = self.blueprint_library.filter('model3')[0]
+ # Fixed spawnpoint.
+ self.spawn_point = self.world.get_map().get_spawn_points()[0]
+
+ self.vehicle = None
+ self.sensor = None
+ self.collision_sensor = None
+
+ self.image_queue = queue.Queue()
+ self.collision_detected = False
+ self.initialized = False
+
+ def close(self) -> None:
+ """Destroys all currently used Actors
+ """
+ if self.vehicle is not None:
+ self.vehicle.destroy()
+ if self.sensor is not None:
+ self.sensor.destroy()
+ if self.collision_sensor is not None:
+ self.collision_sensor.destroy()
+
+ def reset(self) -> tuple[float, float, np.ndarray, bool]:
+ """Resets the Actors
+
+ Returns:
+ tuple[float, float, np.ndarray, bool]:
+ [0]: Difference to the center of the detected lane.
+ [1]: Detected surface area.
+ [2]: Image consisting including the detected surface area.
+ [3]: Whether a collision has been detected.
+ """
+ self.image_queue = queue.Queue()
+ self.close()
+ self.vehicle = self.world.spawn_actor(self.bp, self.spawn_point)
+
+ # Spawn camera
+ blueprint = self.blueprint_library.find('sensor.camera.rgb')
+ blueprint.set_attribute('image_size_x', f'{self.image_width}')
+ blueprint.set_attribute('image_size_y', f'{self.image_height}')
+ blueprint.set_attribute('fov', f'{self.fov}')
+
+ # Relative location to car
+ spawn_point = carla.Transform(carla.Location(x=2.5, z=0.7))
+ self.sensor = self.world.spawn_actor(blueprint, spawn_point,
+ attach_to=self.vehicle)
+ self.sensor.listen(self.image_queue.put)
+
+ # Spawn collision sensor
+ blueprint = self.world.get_blueprint_library() \
+ .find('sensor.other.collision')
+ self.collision_sensor = self.world.spawn_actor(blueprint,
+ carla.Transform(), attach_to=self.vehicle)
+ self.collision_detected = False
+ self.collision_sensor.listen(lambda e: {self._set_collision()})
+ self.initialized = True
+
+ return self._change_to_left_lane()
+
+ def _set_collision(self) -> None:
+ """Helper to set collision_detected to True.
+ """
+ self.collision_detected = True
+
+ def _change_to_left_lane(self) -> tuple[float, float, np.ndarray, bool]:
+ """Hard Code to make the Car start in the leftmost Lane
+
+ Returns:
+ tuple[float, float, np.ndarray, bool]:
+ [0]: Difference to the center of the detected lane.
+ [1]: Detected surface area.
+ [2]: Image consisting including the detected surface area.
+ [3]: Whether a collision has been detected.
+ """
+ for throttle, steer, steps in [
+ (0.2, -0.11, 850), (0.2, 0.17, 250), (-0.2, 0.17, 50)]:
+ for _ in range(steps):
+ error, detection_surface_area, transformed_image, _ = self.step(
+ throttle=throttle, steer=steer)
+
+ return error, detection_surface_area, transformed_image, \
+ self.collision_detected
+
+ def get_image(self) -> np.ndarray:
+ """Retrieve the Image in RGB
+
+ Returns:
+ np.ndarray: Image in RGB.
+ """
+ image = self.image_queue.get()
+ image = np.array(image.raw_data)
+ image = image.reshape((self.image_height, self.image_width, 4))
+ image = image[:, :, :3]
+ return image
+
+ @staticmethod
+ def show_image(image:np.ndarray) -> None:
+ """Display the Image
+
+ Args:
+ image (np.ndarray): Image to display.
+ """
+ cv2.imshow("", image)
+ cv2.waitKey(1)
+
+ def step(self, show:bool=True, throttle:float=0,
+ steer:float=0) -> tuple[float, float, np.ndarray, bool]:
+ """Simulate one Step
+
+ Args:
+ show (bool, optional): Whether to show the image of the car driving.
+ Defaults to True.
+ throttle (float, optional): Which throttle to apply. Defaults to 0.
+ steer (float, optional): Which steering to apply. Defaults to 0.
+
+ Raises:
+ Exception: Requires the reset method to be called before step.
+
+ Returns:
+ tuple[float, float, np.ndarray, bool]:
+ [0]: Difference to the center of the detected lane.
+ [1]: Detected surface area.
+ [2]: Image consisting including the detected surface area.
+ [3]: Whether a collision has been detected.
+ """
+
+ if not self.initialized:
+ raise Exception('Reset must be called before step.')
+
+ self.vehicle.apply_control(carla.VehicleControl(throttle=throttle,
+ steer=steer))
+ self.world.tick()
+ image = self.get_image()
+ transformed_image, error, detection_surface_area = self.lane \
+ .pipe(img=image)
+
+ if show:
+ World.show_image(image=transformed_image)
+
+ return error, detection_surface_area, transformed_image, \
+ self.collision_detected
\ No newline at end of file
diff --git a/src/chap02_linear_regression/exercise-linear_regression.py b/src/chap02_linear_regression/exercise-linear_regression.py
index 32b165a534..873c819fef 100644
--- a/src/chap02_linear_regression/exercise-linear_regression.py
+++ b/src/chap02_linear_regression/exercise-linear_regression.py
@@ -47,11 +47,8 @@ def multinomial_basis(x, feature_num=10):
feature_num: 多项式的最高次数
返回 shape (N, feature_num)"""
x = np.expand_dims(x, axis=1) # shape(N, 1)
- # 生成各次幂特征:x^1, x^2, ..., x^feature_num,将其拼接
- ret = [x**i for i in range(1, feature_num + 1)]
- # 将存储不同次幂特征的数组在第二个维度(列方向)上进行拼接
- # 例如,若每个特征数组形状为 (N, 1),拼接后形状变为 (N, feature_num)
- ret = np.concatenate(ret, axis=1)
+ # 优化点:利用NumPy广播直接生成所有次幂特征,替代列表推导+拼接
+ ret = x ** np.arange(1, feature_num + 1)
return ret
@@ -329,4 +326,4 @@ def plot_results(x_train, y_train, x_test, y_test, y_test_pred):
print("预测值与真实值的标准差:{:.1f}".format(std))
# 使用封装的绘图函数
- plot_results(x_train, y_train, x_test, y_test, y_test_pred)
+ plot_results(x_train, y_train, x_test, y_test, y_test_pred)
\ No newline at end of file
diff --git a/src/chap02_linear_regression/linear_regression-tf2.0.py b/src/chap02_linear_regression/linear_regression-tf2.0.py
index 043d7bbd5a..7e8cc969ad 100644
--- a/src/chap02_linear_regression/linear_regression-tf2.0.py
+++ b/src/chap02_linear_regression/linear_regression-tf2.0.py
@@ -6,8 +6,8 @@
import numpy as np
# 导入Matplotlib的pyplot模块 - 用于数据可视化和绘图
import matplotlib.pyplot as plt
-# 导入Matplotlib的pyplot模块 - 用于数据可视化和绘图
-import tensorflow as tf # 导入TensorFlow深度学习框架,并使用别名'tf'简化调用
+# 导入TensorFlow深度学习框架,并使用别名'tf'简化调用
+import tensorflow as tf
# 从Keras导入常用模块
from tensorflow.keras import optimizers, layers, Model
@@ -17,20 +17,17 @@ def identity_basis(x):
返回形状为 (N, 1) 的数组,其中 N 是输入样本数"""
return np.expand_dims(x, axis=1)
- # 生成多项式基函数
+# 生成多项式基函数
def multinomial_basis(x, feature_num=10):
"""多项式基函数:将输入x映射为多项式特征
feature_num: 多项式的最高次数
返回形状为 (N, feature_num) 的数组"""
x = np.expand_dims(x, axis=1) # shape(N, 1)
- # 初始化特征列表
- feat = [x]
- # 生成从 x^2 到 x^feature_num 的多项式特征
- for i in range(2, feature_num + 1):
- feat.append(x**i)
+ # 优化点1:用列表推导式精简多项式特征生成(替代原for循环)
+ feat = [x**i for i in range(1, feature_num + 1)]
# 将所有特征沿着第二维(axis=1)拼接起来
ret = np.concatenate(feat, axis=1)
- return ret # 返回一个二维数组,其中每一行是输入样本的多项式特征向量,列数为 feature_num
+ return ret # 返回一个二维数组,其中每一行是输入样本的多项式特征向量,列数为 feature_num
def gaussian_basis(x, feature_num=10):
@@ -45,10 +42,10 @@ def gaussian_basis(x, feature_num=10):
width = 1.0 * (centers[1] - centers[0])
# 使用np.expand_dims在x的第1维度(axis=1)上增加一个维度以便广播计算
x = np.expand_dims(x, axis=1)
- # 将x沿着第1维度(axis=1)复制feature_num次并连接使其与中心点数量匹配
- x = np.concatenate([x] * feature_num, axis=1) # 将 x 沿着第 1 维度复制 feature_num 次
- out = (x - centers) / width # 计算每个样本点到每个中心点的标准化距离,将输入值减去对应的中心点,然后除以宽度进行标准化
- ret = np.exp(-0.5 * out ** 2) # 对标准化距离应用高斯函数,-0.5 * out**2 对应高斯分布的概率密度函数形式
+ # 可选优化:用np.tile替代concatenate,更高效(保留原逻辑也可)
+ x = np.tile(x, (1, feature_num)) # 等价于原np.concatenate逻辑
+ out = (x - centers) / width # 计算每个样本点到每个中心点的标准化距离
+ ret = np.exp(-0.5 * out ** 2) # 对标准化距离应用高斯函数
return ret
@@ -60,22 +57,21 @@ def load_data(filename, basis_func=gaussian_basis):
with open(filename, "r") as f:
for line in f:
# 读取每一行数据,并将其转换为浮点数列表
- # 改进: 转换为list
- xys.append(list(map(float, line.strip().split()))) # 读取每行数据将数据分离为特征和标签
- xs, ys = zip(*xys) # 解压为特征和标签
- xs, ys = np.asarray(xs), np.asarray(ys) # 转换为numpy数组
- o_x, o_y = xs, ys # 保存原始数据
- phi0 = np.expand_dims(np.ones_like(xs), axis=1) # 添加偏置项(全1列)
- phi1 = basis_func(xs) # 应用基函数变换
- xs = np.concatenate([phi0, phi1], axis=1)
+ xys.append(list(map(float, line.strip().split()))) # 读取每行数据将数据分离为特征和标签
+ xs, ys = zip(*xys) # 解压为特征和标签
+ xs, ys = np.asarray(xs), np.asarray(ys) # 转换为numpy数组
+ o_x, o_y = xs, ys # 保存原始数据
+ phi0 = np.expand_dims(np.ones_like(xs), axis=1) # 添加偏置项(全1列)
+ phi1 = basis_func(xs) # 应用基函数变换
+ xs = np.concatenate([phi0, phi1], axis=1)
# 拼接偏置和变换后的特征
- return (np.float32(xs), np.float32(ys)), (o_x, o_y)# 返回处理好的训练数据和原始数据
+ return (np.float32(xs), np.float32(ys)), (o_x, o_y) # 返回处理好的训练数据和原始数据
-#定义模型
+# 定义模型
class LinearModel(Model):
"""线性回归模型,实现 y = w·x + b"""
-
+
def __init__(self, ndim):
"""
初始化线性模型
@@ -85,32 +81,29 @@ def __init__(self, ndim):
"""
# 调用父类(Model)的构造函数
super(LinearModel, self).__init__()
-
+
# 定义模型参数:权重矩阵 w
# 形状为 [ndim, 1],表示从 ndim 维输入到 1 维输出的线性变换
# 初始值从均匀分布 [-0.1, 0.1) 中随机生成
# trainable=True 表示该变量需要在训练过程中被优化
# 创建一个TensorFlow变量作为模型权重
self.w = tf.Variable(
- shape=[ndim, 1], # 权重矩阵形状:ndim×1
+ shape=[ndim, 1], # 权重矩阵形状:ndim×1
initial_value=tf.random.uniform(
# [ndim, 1] 表示这是一个二维矩阵,有 ndim 行和 1 列
[ndim, 1], minval=-0.1, maxval=0.1, dtype=tf.float32
),
trainable=True,
- name="weight"# 参数名称(用于TensorBoard等可视化工具)
+ name="weight" # 参数名称(用于TensorBoard等可视化工具)
)
-
- # 注意:代码中缺少偏置项 b,完整的线性模型通常需要包含偏置
# 定义偏置参数b,形状为 [1]
-
- self.b = tf.Variable(# 定义偏置参数b,它是一个TensorFlow的变量(Variable)
+ self.b = tf.Variable( # 定义偏置参数b,它是一个TensorFlow的变量(Variable)
initial_value=tf.zeros([1], dtype=tf.float32),
trainable=True,
name="bias"
)
-
+
@tf.function
def call(self, x):
"""模型前向传播
@@ -125,13 +118,12 @@ def call(self, x):
return y
-(xs, ys), (o_x, o_y) = load_data("train.txt") # 加载训练数据,调用load_data函数
+(xs, ys), (o_x, o_y) = load_data("train.txt") # 加载训练数据,调用load_data函数
ndim = xs.shape[1] # 获取特征维度
model = LinearModel(ndim=ndim) # 实例化线性模型
-
-#训练以及评估
+# 训练以及评估
optimizer = optimizers.Adam(0.1)
@@ -139,11 +131,12 @@ def call(self, x):
def train_one_step(model, xs, ys):
# 在梯度带(GradientTape)上下文中记录前向计算过程
with tf.GradientTape() as tape:
- y_preds = model(xs) # 模型前向传播计算预测值
- loss = tf.keras.losses.MSE(ys, y_preds) #计算损失函数
- grads = tape.gradient(loss, model.w) # 计算损失函数对模型参数w的梯度
- optimizer.apply_gradients([(grads, model.w)]) # 更新模型参数
- return loss # 返回模型的预测结果,即模型对输入数据 xs 的输出
+ y_preds = model(xs) # 模型前向传播计算预测值
+ loss = tf.keras.losses.MSE(ys, y_preds) # 计算损失函数
+ # 优化点2:修复梯度更新,同时计算w和b的梯度(原代码仅更新w)
+ grads = tape.gradient(loss, [model.w, model.b]) # 同时计算w和b的梯度
+ optimizer.apply_gradients(zip(grads, [model.w, model.b])) # 同时更新w和b
+ return loss # 返回模型的损失值
# 使用@tf.function装饰器将Python函数转换为TensorFlow图,以提高执行效率
@@ -155,21 +148,21 @@ def predict(model, xs):
def evaluate(ys, ys_pred):
"""评估模型的性能"""
- return np.std(ys - ys_pred) # 计算预测误差的标准差
+ return np.std(ys - ys_pred) # 计算预测误差的标准差
# 评估指标的计算
-for i in range(1000): # 进行1000次训练迭代
- loss = train_one_step(model, xs, ys) # 执行单步训练并获取当前损失值
- if i % 100 == 1: # 每100步打印一次损失值(从第1步开始:1, 101, 201, ...)
+for i in range(1000): # 进行1000次训练迭代
+ loss = train_one_step(model, xs, ys) # 执行单步训练并获取当前损失值
+ if i % 100 == 1: # 每100步打印一次损失值(从第1步开始:1, 101, 201, ...)
print(f"loss is {loss:.4}") # `:.4` 表示保留4位有效数字
# 使用模型对训练集数据进行预测
y_preds = predict(model, xs)
-# 打印测试集预测值与真实值的标准差
+# 计算训练集预测值与真实值的标准差
std = evaluate(ys, y_preds)
# 打印训练集预测值与真实值的标准差
-print("训练集预测值与真实值的标准差:{:.1f}".format(std)) # 格式化输出标准差,保留一位小数
+print("训练集预测值与真实值的标准差:{:.1f}".format(std)) # 格式化输出标准差,保留一位小数
# 加载测试集数据
(xs_test, ys_test), (o_x_test, o_y_test) = load_data("test.txt")
@@ -182,21 +175,15 @@ def evaluate(ys, ys_pred):
print("测试集预测值与真实值的标准差:{:.1f}".format(std))
# 绘制原始数据点:红色圆点标记,大小3
-# o_x: 原始数据X坐标
-# o_y: 原始数据Y坐标
-# "ro": 红色(r)圆形(o)标记
-plt.plot(o_x, o_y, "ro", markersize=3)
+plt.plot(o_x, o_y, "ro", markersize=3)
# 绘制模型预测曲线:黑色实线
-# o_x_test: 测试集X坐标
-# y_test_preds: 模型在测试集上的预测结果
-# "k": 黑色(k)实线(默认线型)
plt.plot(o_x_test, y_test_preds, "k")
# 设置x、y轴标签
plt.xlabel("x")
plt.ylabel("y")
-plt.title("Linear Regression") # 图表标题
+plt.title("Linear Regression") # 图表标题
# 虚线网格,半透明灰色
plt.grid(True, linestyle="--", alpha=0.7, color="gray")
-plt.legend(["train", "test", "pred"]) # 添加图例,元素依次对应
+plt.legend(["train", "pred"]) # 修正图例(原图例多了"test",实际只有train和pred)
plt.tight_layout() # 自动调整布局
-plt.show()# 显示图形
+plt.show() # 显示图形
\ No newline at end of file
diff --git a/src/chap03_SVM/svm.py b/src/chap03_SVM/svm.py
index 4211e1a2a7..7fb66d0e6b 100644
--- a/src/chap03_SVM/svm.py
+++ b/src/chap03_SVM/svm.py
@@ -5,7 +5,8 @@ def load_data(fname):
"""载入数据。"""
# 检查文件是否存在,确保数据加载的可靠性
if not os.path.exists(fname):
- raise FileNotFoundError(f"数据文件未找到: {fname}\n请确认文件路径是否正确,当前工作目录为: {os.getcwd()}") # 如果文件不存在,抛出异常
+ raise FileNotFoundError(f"数据文件未找到: {fname}\
+请确认文件路径是否正确,当前工作目录为: {os.getcwd()}") # 如果文件不存在,抛出异常
with open(fname, 'r') as f: # 打开文件
data = [] # 初始化一个空列表,用于存储数据
line = f.readline() # 跳过表头行
@@ -73,9 +74,9 @@ def train(self, data_train):
# 计算梯度:正则化项梯度 + 误分类样本梯度
# L2正则化:减小权重,防止过拟合
# hinge loss梯度:只对误分类和边界样本计算梯度
- dw = (2 * self.reg_lambda * self.w) - np.sum(y[idx, None] * X[idx], axis=0) / m if len(
- idx) > 0 else 2 * self.reg_lambda * self.w
- db = -np.mean(y[idx]) if len(idx) > 0 else 0
+ # 优化:给 y 增加一个轴以匹配 X 的维度,解决广播错误,确保梯度计算可运行
+ dw = (2 * self.reg_lambda * self.w) - np.mean(y[idx, np.newaxis] * X[idx], axis=0)
+ db = -np.mean(y[idx])
# 梯度下降更新参数
self.w -= self.learning_rate * dw # 权重更新:w = w - η*dw/dw
@@ -97,7 +98,7 @@ def predict(self, x):
3. 距离为负 -> 预测为负类(0)
"""
score = np.dot(x, self.w) + self.b # 计算决策函数值
- return (score >= 0).astype(np.int32) # 更简洁高效的布尔转整数方法
+ return np.where(score >= 0, 1, 0) # 转换回{0, 1}标签格式
if __name__ == '__main__':
# 数据加载部分以及数据路径配置
@@ -129,5 +130,4 @@ def predict(self, x):
acc_test = eval_acc(t_test, t_test_pred) # 测试集准确率
print("train accuracy: {:.1f}%".format(acc_train * 100)) # 输出训练集准确率
- print("test accuracy: {:.1f}%".format(acc_test * 100)) # 输出测试集准确率
-
+ print("test accuracy: {:.1f}%".format(acc_test * 100)) # 输出测试集准确率
\ No newline at end of file
diff --git a/src/chap03_softmax_regression/logistic_regression-exercise.py b/src/chap03_softmax_regression/logistic_regression-exercise.py
index 6749cd2d22..89892eec5e 100644
--- a/src/chap03_softmax_regression/logistic_regression-exercise.py
+++ b/src/chap03_softmax_regression/logistic_regression-exercise.py
@@ -141,10 +141,8 @@ def compute_loss(pred, label):
# 计算所有样本损失的平均值
loss = tf.reduce_mean(losses)
- # 将预测概率大于0.5的设置为1,小于等于0.5的设置为0,得到预测标签
- pred = tf.where(pred > 0.5, tf.ones_like(pred), tf.zeros_like(pred))
- # 计算预测标签与真实标签相等的比例,即准确率
- accuracy = tf.reduce_mean(tf.cast(tf.equal(label, pred), dtype = tf.float32))
+ # 优化:直接使用 tf.round 四舍五入进行二分类判别,替代冗余的 where/ones_like/zeros_like 操作
+ accuracy = tf.reduce_mean(tf.cast(tf.equal(label, tf.round(pred)), dtype=tf.float32))
# 返回计算得到的损失值和准确率
return loss, accuracy
diff --git a/src/chap03_softmax_regression/softmax_regression-exercise.py b/src/chap03_softmax_regression/softmax_regression-exercise.py
index 97ad286f42..62be71cb19 100644
--- a/src/chap03_softmax_regression/softmax_regression-exercise.py
+++ b/src/chap03_softmax_regression/softmax_regression-exercise.py
@@ -167,8 +167,8 @@ def train_one_step(model, optimizer, x_batch, y_batch):
model = SoftmaxRegression()
# 创建一个 SoftmaxRegression 模型实例 model
-opt = tf.keras.optimizers.SGD(learning_rate=0.01)
-# 创建随机梯度下降(SGD)优化器实例 opt,设置学习率为 0.01
+# 将 SGD 更换为 Adam 优化器,收敛速度更快,分类边界更稳定
+opt = tf.keras.optimizers.Adam(learning_rate=0.02)
x1, x2, y = list(zip(*data_set)) #从data_set中提取特征和标签,并将它们分别存储到x1、x2 和 y 中
# 转换为 float32
diff --git a/src/chap04_simple_neural_network/tutorial_minst_fnn-numpy-exercise.py b/src/chap04_simple_neural_network/tutorial_minst_fnn-numpy-exercise.py
index 9c447a3055..72cae41300 100644
--- a/src/chap04_simple_neural_network/tutorial_minst_fnn-numpy-exercise.py
+++ b/src/chap04_simple_neural_network/tutorial_minst_fnn-numpy-exercise.py
@@ -335,8 +335,9 @@ def backward(self, grad_y):
class myModel:
def __init__(self):# 初始化模型参数,使用随机正态分布初始化权重矩阵
# 权重矩阵包含偏置项,通过增加输入特征维度实现
- self.W1 = np.random.normal(size=[28 * 28 + 1, 100]) # 输入层到隐藏层,增加偏置项,W1: 连接输入层(784+1)和隐藏层(100)的权重矩阵
- self.W2 = np.random.normal(size=[100, 10]) # 输入层到隐藏层,增加偏置项,W2: 连接隐藏层(100)和输出层(10)的权重矩阵
+ # 优化:添加 scale=0.1。原代码默认标准差为1.0,会导致初始权重过大,使Softmax饱和、梯度消失,模型难以训练。
+ self.W1 = np.random.normal(scale=0.1, size=[28 * 28 + 1, 100])
+ self.W2 = np.random.normal(scale=0.1, size=[100, 10])
# 初始化各层操作对象
self.mul_h1 = Matmul() # 第一个矩阵乘法层(输入到隐藏层)
self.mul_h2 = Matmul() # 第二个矩阵乘法层(隐藏层到输出层)
diff --git a/src/chap04_simple_neural_network/tutorial_minst_fnn-tf2.0-exercise.py b/src/chap04_simple_neural_network/tutorial_minst_fnn-tf2.0-exercise.py
index c4cda955ba..eb326f56fb 100644
--- a/src/chap04_simple_neural_network/tutorial_minst_fnn-tf2.0-exercise.py
+++ b/src/chap04_simple_neural_network/tutorial_minst_fnn-tf2.0-exercise.py
@@ -71,8 +71,8 @@ def __call__(self, x):
model = MyModel()
-# 使用Adam优化器,用于训练过程中更新模型参数
-optimizer = optimizers.Adam()
+# 优化:针对全量数据训练(Full Batch),默认的0.001学习率太慢,改为0.01能显著加快收敛
+optimizer = optimizers.Adam(learning_rate=0.01)
# ## 计算 loss