├── .gitignore ├── LICENSE ├── README.md ├── adas_cas.py ├── efficientdet-d0-tf ├── efficientdet-d0-tf.bin ├── efficientdet-d0-tf.mapping └── efficientdet-d0-tf.xml ├── images ├── ADAS_Architecture.jpeg ├── CodeCogsEqn.gif ├── Sensor_Fusion_Output.gif ├── adas.gif ├── bom.jpg ├── dummy.jpg ├── equation.jpeg ├── homo.jpeg ├── lidar_cam_geometry.jpeg ├── lidar_cam_sf.jpeg ├── lidar_converstion_formula.jpeg ├── thumbnail_play.png └── trafficSign.gif ├── lidar_getdist.py ├── models.lst ├── pulse_mqtt.py └── ssdlite_mobilenet_v2 ├── ssdlite_mobilenet_v2.bin ├── ssdlite_mobilenet_v2.mapping └── ssdlite_mobilenet_v2.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ADAS: Collision Avoidance System on Indian Roads 2 | _Real-world implementation of ADAS L0 - CAS on Indian Roads - using LIDAR-Camera Low-Level Sensor Fusion_ 3 | 4 | This solution aims **to augment even the least expensive cars in India with an ultra-cheap ADAS Level 0, i.e. collision avoidance and smart surround-view.** Modern cars with a forward-collision warning (FCW) system or autonomous emergency braking (AEB) are expensive, but we can augment such functions on old cars, at a low cost. 5 | 6 | # Project Demo 7 | 8 | **Watch the gadget plying Indian Roads**, giving driver-assist alerts after perceiving the surrounding objects, their depth and direction. 9 | 10 | [![Watch Project Demo](images/thumbnail_play.png)](https://youtu.be/6XW5p3OjkIA) 11 | 12 | 13 | **Project Blog:** 14 | https://towardsdatascience.com/adas-collision-avoidance-system-on-indian-cars-bac64cc8a863 15 | 16 | **Source Code:** 17 | https://github.com/AdroitAnandAI/ADAS-Collision-Avoidance-System-on-Indian-Roads 18 | 19 | **Linkedin:** 20 | http://linkedin.com/in/ananduthaman 21 | 22 | 23 | # Bill of Materials 24 | 25 |

26 | 27 |

28 | 29 | 30 | # How to Use? 31 | 32 | **Please execute the commands in this order**, so that by the time the object detection module runs the LIDAR and Flash modules will be up and running. Object detection module is the publish node while the other two are subscriber nodes. 33 | 34 | To run the LIDAR distance estimation Subscribe module: 35 | ```python 36 | python3 lidar_getdist.py 37 | ``` 38 | 39 | To run the Flash Warning Subscriber Module: 40 | ```python 41 | python3 pulse_mqtt.py 42 | ``` 43 | 44 | To run the main object detection Publish Module: 45 | ```python 46 | python3 adas_cas.py -i 0 -m ssdlite_mobilenet_v2/ssdlite_mobilenet_v2.xml -at ssd --labels ~/open_model_zoo/data/dataset_classes/coco_91cl.txt -d MYRIAD --output_resolution 640x480 47 | ``` 48 | 49 | # Project Description 50 | 51 | The idea is to **use a battery-powered Pi connected with a LIDAR, Pi Cam, LED SHIM, and NCS 2, mounted on the car bonnet to perceive frontal objects with their depth & direction.** This not only enables a forward-collision warning system but also smart driver assistance that gives alerts about traffic signs or pedestrians, walking along the roadside, or crossing the road. 52 | 53 | Cameras generally have higher resolution than LiDAR but cameras have a limited FOV and can't estimate distance. **While rotating LIDAR has a 360° field of view, Pi Cam has only 62x48 degrees Horizontal x Vertical FoV.** As we deal with multiple sensors here, we need to employ **visual fusion techniques** to integrate the sensor output, that is to get the distance and angle of an obstacle in front of the vehicle. Let's first discuss the theoretical foundation of sensor fusion before hands-on implementation. 54 | 55 | ## The Idea of Sensor Fusion 56 | 57 | Each sensor has its own advantages and disadvantages. Take, for instance, RADARs are low in resolution, but are good at measurement without a line of sight. **In an autonomous car, often a combination of LiDARs, RADARs, and Cameras are used to perceive the environment.** This way we can **compensate for the disadvantages, by combining the advantages of all sensors.** 58 | 59 | - **Camera:** excellent to understand a scene or to classify objects 60 | - **LIDAR:** excellent to estimate distances using pulsed laser waves 61 | - **RADAR:** can measure the speed of obstacles using Doppler Effect 62 | 63 | The camera is a 2D Sensor from which features like bounding boxes, traffic lights, lane divisions can be identified. LIDAR is a 3D Sensor that outputs a set of point clouds. **The fusion technique finds a correspondence between points detected by LIDAR and points detected by the camera.** To use LiDARs and Cameras in unison to build ADAS, the 3D sensor output needs to be fused with 2D sensor output by doing the following steps. 64 | 65 | 1. **Project the LiDAR point clouds (3D) onto the 2D image.** 66 | 2. **Do object detection** using an algorithm like YOLOv4. 67 | 3. **Match the ROI** to find the interested LiDAR projected points. 68 | 69 | By doing the 3 steps above, the surrounding objects would be measured and classified using LIDAR-Camera fusion. 70 | 71 | ## LIDAR-Camera Sensor Fusion Considerations 72 | When a raw image from a cam is merged with raw data from RADAR or LIDAR then it's called **Low-Level Fusion or Early Fusion.** In Late Fusion, detection is done before the fusion. Note that there are many challenges to project the 3D LIDAR point cloud on a 2D image. **The relative orientation and translation between the two sensors must be considered in performing fusion.** 73 | 74 | - **Rotation:** The coordinate system of LIDAR and Camera can be different. Distance on the LIDAR may be on the z-axis, while it is x-axis on the camera. **We need to apply rotation on the LIDAR point cloud to make the coordinate system the same**, i.e. multiply each LIDAR point with the Rotation matrix. 75 | - **Translation:** In an autonomous car, the LIDAR can be at the center top and the camera on the sides. The position of LIDAR and camera in each installation can be different. Based on the relative sensor position, **translate the LIDAR Points by multiplying with a Translation matrix. 76 | - **Stereo Rectification:** For stereo camera setup, we need to do Stereo Rectification to make the left and right images co-planar. Thus, we need to multiply with matrix R0 to align everything along the horizontal Epipolar line. 77 | - **Intrinsic calibration:** Calibration is the step where you tell your camera how **to convert a point in the 3D world into a pixel.** To account for this, we need to multiply with an intrinsic calibration matrix containing factory calibrated values. 78 | 79 | To sum it up, we **need to multiply LIDAR points with all the 4 matrices** to project on the camera image. 80 | 81 | **To project a point X in 3D onto a point Y in 2D, 82 | 83 |

84 | 85 |

86 | 87 | - **P = Camera Intrinsic Calibration matrix 88 | - **R0 = Stereo Rectification matrix 89 | - **R|t = Rotation & Translation to go from LIDAR to Camera 90 | - **X = Point in 3D space 91 | - **Y = Point in 2D Image 92 | 93 | Note that we have combined both the rigid body transformations, rotation, and translation, in one matrix, R|t. Putting it together, the 3 matrices, P, R0, and R|t account for extrinsic and intrinsic calibration to project LIDAR points onto the camera image. However, the matrix values highly **depend on our custom sensor installation.** 94 | 95 | ## Real-World Implementation with RPi & RPLIDAR A1 96 | 97 | First, we need to assemble the Pi with RPLIDAR A1, Pi Cam, LED SHIM, and NCS 2. 2D LIDAR is used instead of 3D LIDAR as we aim to make the gadget, cheapest possible. The unit is powered by a 5V 3A 10K mAH battery. For ease of assembly, a LIDAR mount is 3D printed and attached to the RPi. Part of the mount design is taken from the STL files obtained from ![here](https://medium.com/r/?url=https%3A%2F%2Fwww.thingiverse.com%2Fthing%3A3970110) 98 | 99 | Connect RPLIDAR A1 with the USB adapter that is connected to the Pi USB using a micro-USB cable. LIDAR's adapter provides power and converts LIDAR's internal UART serial interface to a USB interface. Use an Aux-to-Aux cable to connect RPi to speakers. Due to physical constraints, an **LED SHIM is used instead of Blinkt** to signal warning messages. While the total cost of the ADAS gadget is just around US$ 150–200, one may have to shell out at least $10–20K more, to get a car model with such advanced features. 100 | 101 | **Let's imagine, a 3D LIDAR is connected** the same way as above. First, **we will try to solve 3D LIDAR-Camera Sensor Fusion** on the above gadget. **Then we will see the variation for 2D LIDAR-Camera Fusion, so as to make it work on RPLIDAR A1.** 102 | 103 |

104 | 105 |

106 | 107 | ## 3D LIDAR-Camera Sensor Fusion 108 | 109 | It is clear from the above discussion that **we need to do rotation, translation, stereo rectification, and intrinsic calibration to project LIDAR points on the image.** We will try to apply the above formula based on the custom gadget that we built. 110 | 111 | From the above image, you can estimate the **Pi Cam is 10 mm below the LIDAR scan plane. i.e. a translation of [0, -10, 0] along the 3D-axis. Consider Velodyne HDL-64E as our 3D LIDAR, which requires 180° rotation to align the coordinate system with Pi Cam. We can compute the R|t matrix now.** 112 | 113 | As we use a monocular camera here, the **stereo rectification matrix will be an identity matrix.** We can make the intrinsic calibration matrix based on the hardware spec of Pi Cam V2. 114 | 115 |

116 | 117 |

118 | 119 | For the **RaspberryPi V2 camera,** 120 | - **Focal Length (FL)** = 3.04 mm 121 | - **FL Pixels** = focal length * sx, where sx = real world to pixels ratio 122 | - **Focal Length * sx** = 3.04mm * (1/ 0.00112 mm per px) = 2714.3 px 123 | 124 | Due to a mismatch in shape, the matrices cannot be multiplied. To make it work, we need to **transition from Euclidean to Homogeneous coordinates by adding 0's and 1's as the last row or column.** After doing the multiplication we need to **convert back to Homogeneous coordinates.** 125 | 126 | 127 |

128 | 129 |

130 | 131 | You can see the **3DLIDAR-CAM sensor fusion projection output after applying the projection formula on the 3D point cloud.** The input sensor data from 360° Velodyne HDL-64E and camera is downloaded [9] and fed in. 132 | 133 |

134 | 135 |

136 | 137 | However, the 3D LiDAR cost is a barrier to build a cheap solution. We can **instead use cheap 2D LiDAR with necessary tweaks**, as it only scans a single horizontal line. 138 | 139 | ## 2D LIDAR-Camera Sensor Fusion 140 | 141 | Our gadget is equipped with 2D RP LIDAR A1 to minimize the cost. **This LIDAR scans the environment in a 2D plane, orthogonal to the camera plane. The rotating scan will estimate the distance to the obstacle, for each angle from 0° to 360°. Due to the placement of LIDAR w.r.t. Pi Cam in the gadget, the camera is at +90° on the LIDAR geometry. However, note that the Field of View of Pi cam V2 is 62°x48° in horizontal x vertical direction respectively.** 142 | 143 | The **integrated front view of the device** is as shown below. 144 | 145 | 146 |

147 | 148 |

149 | 150 | As **both the LIDAR and Camera sensor data is available in the frontal 62° arc, we need to fuse the data.** In the LIDAR scan plane, the camera data starts from +59° to +59° + 62° = 121°. **We can run object detection on the image** to get bounding boxes for the objects of interest. Eg: human, car, bike, traffic light, etc. **Since 2D LIDAR has only width information, consider only the x_min and x_max of each bounding box.** 151 | 152 | We need to compute the LIDAR angle corresponding to an image pixel, in order to estimate the distance to the pixel. **To find the distance to the object inside the bounding box, compute θ_min and θ_max corresponding to x_min & x_max using the below formula**, based on the above diagram, 153 | 154 |

155 | 156 |

157 | 158 | Now you can **find the distance to each angle between θ_min and θ_max based on the latest LIDAR scan data. Then compute the median distance of all LIDAR points that subtends the object bounding box to estimate the object depth.** If the distance is below a threshold, then trigger a warning based on the angle. Repeat warning, only if the box center shift by a significant distance in subsequent frames. 159 | 160 | **To build a non-blocking system flow, a modular architecture is employed wherein, each independent node is dependent on different hardware components.** i.e., the "object detection" node uses Movidius for inference, whereas the "distance estimation" node takes LIDAR data as input, while the "Alert" module signals to Pimoroni Blinkt and the speaker. The modules are communicated via MQTT messages on respective topics. 161 | 162 | 163 | # Architecture Diagram 164 | 165 | 166 |

167 | 168 |

169 | 170 | The **time synchronization module takes care of the "data relevance factor" for Sensor Fusion.** For ADAS, the location of detected objects by 'Node 1' may change, as the objects move. Thus, the distance estimate to the bounding box could go wrong after 2–3 seconds (while the message can remain in the MQTT queue). In order to synchronize, current time = 60*minutes + seconds is appended to the message (to ignore lagged messages). 171 | 172 | The model output is sent from Node 1 to Node 2, where the LiDAR-Cam sensor fusion happens, further pushing a message to Node 3. **For the system to function, the 3 MQTT nodes should work in tandem, orchestrated by MQTT messages, published, and subscribed on respective topics.** 173 | 174 | **This ADAS device can be connected to the CAN bus** to let it accelerate, steer, or apply the brake. RPi doesn't have a built-in CAN Bus, but its GPIO includes SPI Bus, which is supported by a number of CAN controllers like MCP2515. So, autonomous emergency braking (AEB) and collision avoidance system (CAS) can be done by connecting this device to the CAN bus. 175 | 176 | # Tweaking for Indian Conditions 177 | 178 | The Indian traffic conundrum is so unique that it demands custom solutions. To start with, **we need to train object detection models with Indian vehicles such as trucks, tempos, vans, autos, cycle rickshaws, etc.** 179 | 180 | Further, to enhance smart surround view, **we need to train the model with Indian traffic signs and signboards** to give more meaningful driver-assist warnings on Indian roads. It's a common sight in India, the animals like cows, pigs, buffaloes, goats, dogs, etc., cross the roads and highways. Hence, it's beneficial to detect them as well. To find out the ROI from images, we have used SSD MobileNet trained on COCO filtered by potential objects. To detect only people and vehicles, you can use this model also to get better speed and accuracy. 181 | 182 | For the PoC, **see the output of SSD-Mobilenet model trained to classify Indian traffic signs against Indian signboards.** You can further classify the traffic sign to decipher the exact meaning of the sign. 183 | 184 | 185 |

186 | 187 |

188 | 189 | _The annotated Indian Traffic Sign dataset is provided by Datacluster Labs, India. They are yet to finish the annotation of "Indian Vehicles" database. It's just a matter of training time to make this gadget, tailor-made for India._ 190 | 191 | 192 | **Project Blog:** 193 | https://towardsdatascience.com/adas-collision-avoidance-system-on-indian-cars-bac64cc8a863 194 | 195 | **Source Code:** 196 | https://github.com/AdroitAnandAI/ADAS-Collision-Avoidance-System-on-Indian-Roads 197 | 198 | **Linkedin:** 199 | http://linkedin.com/in/ananduthaman 200 | 201 | 202 | # References 203 | 1. **LIDAR-Camera Sensor Fusion High Level:** https://www.thinkautonomous.ai/blog/?p=lidar-and-camera-sensor-fusion-in-self-driving-cars 204 | 2. **LIDAR data scan code stub by Adafruit:** https://learn.adafruit.com/remote-iot-environmental-sensor/code 205 | 3. **Camera Calibration and Intrinsic Matrix Estimation:** https://www.cc.gatech.edu/classes/AY2016/cs4476_fall/results/proj3/html/agartia3/index.html 206 | 4. **Visual Fusion** For Autonomous Cars Course at PyImageSearch University: https://www.pyimagesearch.com/pyimagesearch-university/ 207 | 5. LIDAR Distance Estimation:https://en.wikipedia.org/wiki/Lidar 208 | 6. RPLIDAR A1 M8 Hardware Specification:https://www.generationrobots.com/media/rplidar-a1m8-360-degree-laser-scanner-development-kit-datasheet-1.pdf 209 | 7. Model Training, Data Cleansing & Augmentation:www.roboflow.com 210 | 8. _The Indian traffic sign model has been trained using the Traffic dataset offered by Datacluster Labs, India._ 211 | -------------------------------------------------------------------------------- /adas_cas.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | 4 | The python object detection from OpenVINO demos is taken and extensively 5 | modified to add the ADAS functionality. This module will get the imagea 6 | and identity the bounding boxes. Then it will publish an MQTT message with 7 | the theta min and max, label, time info on "object/getdistance" topic. The 8 | data is decoded by other lidar_getdist.py node to get the distance of the 9 | object and it will send a message to pulse_mqtt node to flash the warning. 10 | 11 | The stub code is taken from the OpenVINO demos and extensively modified to 12 | add the above functions. 13 | 14 | Copyright (C) 2018-2021 Intel Corporation 15 | 16 | Licensed under the Apache License, Version 2.0 (the "License"); 17 | you may not use this file except in compliance with the License. 18 | You may obtain a copy of the License at 19 | 20 | http://www.apache.org/licenses/LICENSE-2.0 21 | 22 | Unless required by applicable law or agreed to in writing, software 23 | distributed under the License is distributed on an "AS IS" BASIS, 24 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 25 | See the License for the specific language governing permissions and 26 | limitations under the License. 27 | """ 28 | 29 | 30 | import colorsys 31 | import logging 32 | import random 33 | import os 34 | import sys 35 | from collections import deque 36 | from argparse import ArgumentParser, SUPPRESS 37 | from pathlib import Path 38 | from time import perf_counter 39 | import time 40 | 41 | import paho.mqtt.client as mqtt 42 | import cv2 43 | import numpy as np 44 | from openvino.inference_engine import IECore 45 | 46 | # sys.path.append(str(Path(__file__).resolve().parents[2] / 'common/python')) 47 | 48 | sys.path.append('/home/pi/open_model_zoo/demos/common/python') 49 | sys.path.append('/opt/intel/openvino_2021/deployment_tools/') 50 | 51 | import models 52 | import monitors 53 | from pipelines import get_user_config, AsyncPipeline 54 | from images_capture import open_images_capture 55 | from performance_metrics import PerformanceMetrics 56 | from helpers import resolution 57 | 58 | logging.basicConfig(format='[ %(levelname)s ] %(message)s', level=logging.INFO, stream=sys.stdout) 59 | log = logging.getLogger() 60 | 61 | objLastFrames = deque(maxlen=30) 62 | objMidsLastFrames = deque(maxlen=30) 63 | noObjFrames = 0 64 | 65 | # This is the Publisher 66 | client = mqtt.Client() 67 | client.connect("localhost", 1883, 600) 68 | 69 | 70 | def build_argparser(): 71 | parser = ArgumentParser(add_help=False) 72 | args = parser.add_argument_group('Options') 73 | args.add_argument('-h', '--help', action='help', default=SUPPRESS, help='Show this help message and exit.') 74 | args.add_argument('-m', '--model', help='Required. Path to an .xml file with a trained model.', 75 | required=True, type=Path) 76 | args.add_argument('-at', '--architecture_type', help='Required. Specify model\' architecture type.', 77 | type=str, required=True, choices=('ssd', 'yolo', 'yolov4', 'faceboxes', 'centernet', 'ctpn', 78 | 'retinaface', 'ultra_lightweight_face_detection', 79 | 'retinaface-pytorch')) 80 | args.add_argument('-i', '--input', required=True, 81 | help='Required. An input to process. The input must be a single image, ' 82 | 'a folder of images, video file or camera id.') 83 | args.add_argument('-d', '--device', default='CPU', type=str, 84 | help='Optional. Specify the target device to infer on; CPU, GPU, HDDL or MYRIAD is ' 85 | 'acceptable. The demo will look for a suitable plugin for device specified. ' 86 | 'Default value is CPU.') 87 | 88 | common_model_args = parser.add_argument_group('Common model options') 89 | common_model_args.add_argument('--labels', help='Optional. Labels mapping file.', default=None, type=str) 90 | common_model_args.add_argument('-t', '--prob_threshold', default=0.6, type=float, 91 | help='Optional. Probability threshold for detections filtering.') 92 | common_model_args.add_argument('--keep_aspect_ratio', action='store_true', default=False, 93 | help='Optional. Keeps aspect ratio on resize.') 94 | common_model_args.add_argument('--input_size', default=(600, 600), type=int, nargs=2, 95 | help='Optional. The first image size used for CTPN model reshaping. ' 96 | 'Default: 600 600. Note that submitted images should have the same resolution, ' 97 | 'otherwise predictions might be incorrect.') 98 | 99 | infer_args = parser.add_argument_group('Inference options') 100 | infer_args.add_argument('-nireq', '--num_infer_requests', help='Optional. Number of infer requests', 101 | default=0, type=int) 102 | infer_args.add_argument('-nstreams', '--num_streams', 103 | help='Optional. Number of streams to use for inference on the CPU or/and GPU in throughput ' 104 | 'mode (for HETERO and MULTI device cases use format ' 105 | ':,: or just ).', 106 | default='', type=str) 107 | infer_args.add_argument('-nthreads', '--num_threads', default=None, type=int, 108 | help='Optional. Number of threads to use for inference on CPU (including HETERO cases).') 109 | 110 | io_args = parser.add_argument_group('Input/output options') 111 | io_args.add_argument('--loop', default=False, action='store_true', 112 | help='Optional. Enable reading the input in a loop.') 113 | io_args.add_argument('-o', '--output', required=False, 114 | help='Optional. Name of the output file(s) to save.') 115 | io_args.add_argument('-limit', '--output_limit', required=False, default=1000, type=int, 116 | help='Optional. Number of frames to store in output. ' 117 | 'If 0 is set, all frames are stored.') 118 | io_args.add_argument('--no_show', help="Optional. Don't show output.", action='store_true') 119 | io_args.add_argument('--output_resolution', default=None, type=resolution, 120 | help='Optional. Specify the maximum output window resolution ' 121 | 'in (width x height) format. Example: 1280x720. ' 122 | 'Input frame size used by default.') 123 | io_args.add_argument('-u', '--utilization_monitors', default='', type=str, 124 | help='Optional. List of monitors to show initially.') 125 | 126 | input_transform_args = parser.add_argument_group('Input transform options') 127 | input_transform_args.add_argument('--reverse_input_channels', default=False, action='store_true', 128 | help='Optional. Switch the input channels order from ' 129 | 'BGR to RGB.') 130 | input_transform_args.add_argument('--mean_values', default=None, type=float, nargs=3, 131 | help='Optional. Normalize input by subtracting the mean ' 132 | 'values per channel. Example: 255 255 255') 133 | input_transform_args.add_argument('--scale_values', default=None, type=float, nargs=3, 134 | help='Optional. Divide input by scale values per channel. ' 135 | 'Division is applied after mean values subtraction. ' 136 | 'Example: 255 255 255') 137 | 138 | debug_args = parser.add_argument_group('Debug options') 139 | debug_args.add_argument('-r', '--raw_output_message', help='Optional. Output inference results raw values showing.', 140 | default=False, action='store_true') 141 | return parser 142 | 143 | 144 | class ColorPalette: 145 | def __init__(self, n, rng=None): 146 | assert n > 0 147 | 148 | if rng is None: 149 | rng = random.Random(0xACE) 150 | 151 | candidates_num = 100 152 | hsv_colors = [(1.0, 1.0, 1.0)] 153 | for _ in range(1, n): 154 | colors_candidates = [(rng.random(), rng.uniform(0.8, 1.0), rng.uniform(0.5, 1.0)) 155 | for _ in range(candidates_num)] 156 | min_distances = [self.min_distance(hsv_colors, c) for c in colors_candidates] 157 | arg_max = np.argmax(min_distances) 158 | hsv_colors.append(colors_candidates[arg_max]) 159 | 160 | self.palette = [self.hsv2rgb(*hsv) for hsv in hsv_colors] 161 | 162 | @staticmethod 163 | def dist(c1, c2): 164 | dh = min(abs(c1[0] - c2[0]), 1 - abs(c1[0] - c2[0])) * 2 165 | ds = abs(c1[1] - c2[1]) 166 | dv = abs(c1[2] - c2[2]) 167 | return dh * dh + ds * ds + dv * dv 168 | 169 | @classmethod 170 | def min_distance(cls, colors_set, color_candidate): 171 | distances = [cls.dist(o, color_candidate) for o in colors_set] 172 | return np.min(distances) 173 | 174 | @staticmethod 175 | def hsv2rgb(h, s, v): 176 | return tuple(round(c * 255) for c in colorsys.hsv_to_rgb(h, s, v)) 177 | 178 | def __getitem__(self, n): 179 | return self.palette[n % len(self.palette)] 180 | 181 | def __len__(self): 182 | return len(self.palette) 183 | 184 | 185 | def get_model(ie, args): 186 | input_transform = models.InputTransform(args.reverse_input_channels, args.mean_values, args.scale_values) 187 | common_args = (ie, args.model, input_transform) 188 | if args.architecture_type in ('ctpn', 'yolo', 'yolov4', 'retinaface', 189 | 'retinaface-pytorch') and not input_transform.is_trivial: 190 | raise ValueError("{} model doesn't support input transforms.".format(args.architecture_type)) 191 | 192 | if args.architecture_type == 'ssd': 193 | return models.SSD(*common_args, labels=args.labels, keep_aspect_ratio_resize=args.keep_aspect_ratio) 194 | elif args.architecture_type == 'ctpn': 195 | return models.CTPN(ie, args.model, input_size=args.input_size, threshold=args.prob_threshold) 196 | elif args.architecture_type == 'yolo': 197 | return models.YOLO(ie, args.model, labels=args.labels, 198 | threshold=args.prob_threshold, keep_aspect_ratio=args.keep_aspect_ratio) 199 | elif args.architecture_type == 'yolov4': 200 | return models.YoloV4(ie, args.model, labels=args.labels, 201 | threshold=args.prob_threshold, keep_aspect_ratio=args.keep_aspect_ratio) 202 | elif args.architecture_type == 'faceboxes': 203 | return models.FaceBoxes(*common_args, threshold=args.prob_threshold) 204 | elif args.architecture_type == 'centernet': 205 | return models.CenterNet(*common_args, labels=args.labels, threshold=args.prob_threshold) 206 | elif args.architecture_type == 'retinaface': 207 | return models.RetinaFace(ie, args.model, threshold=args.prob_threshold) 208 | elif args.architecture_type == 'ultra_lightweight_face_detection': 209 | return models.UltraLightweightFaceDetection(*common_args, threshold=args.prob_threshold) 210 | elif args.architecture_type == 'retinaface-pytorch': 211 | return models.RetinaFacePyTorch(ie, args.model, threshold=args.prob_threshold) 212 | else: 213 | raise RuntimeError('No model type or invalid model type (-at) provided: {}'.format(args.architecture_type)) 214 | 215 | 216 | def isAnnounced(label, x_mid, y_mid): 217 | 218 | for idx, objs in enumerate(objLastFrames): 219 | for idy, lbl in enumerate(objs): 220 | 221 | if lbl == label: 222 | lastMidPt = objMidsLastFrames[idx][idy] 223 | 224 | # Increase this value to reduce repeated announcements and vice versa 225 | if np.abs(lastMidPt[0] - x_mid) + np.abs(lastMidPt[1] - y_mid) < 100: 226 | 227 | return True 228 | 229 | return False 230 | 231 | 232 | 233 | def announceDetection(frame, frameCount, detections, labels, threshold, output_transform): 234 | 235 | global noObjFrames 236 | size = frame.shape[:2] 237 | x_width = 1280 238 | y_height = 720 239 | 240 | objectsInFrame = [] 241 | objectMidPts = [] 242 | 243 | for detection in detections: 244 | 245 | if detection.score > threshold: 246 | 247 | class_id = int(detection.id)-1 248 | 249 | # Potential Objects for self driving car 250 | # person 251 | # bicycle 252 | # car 253 | # motorcycle 254 | # bus 255 | # truck 256 | # traffic light 257 | # street sign 258 | # stop sign 259 | if class_id not in [0, 1, 2, 3, 5, 7, 9, 11, 12]: 260 | continue 261 | 262 | det_label = labels[class_id] if labels and len(labels) >= class_id else '#{}'.format(class_id) 263 | xmin = max(int(detection.xmin), 0) 264 | ymin = max(int(detection.ymin), 0) 265 | xmax = min(int(detection.xmax), x_width) 266 | ymax = min(int(detection.ymax), y_height) 267 | 268 | if xmin > xmax: 269 | swap = xmin 270 | xmin = xmax 271 | xmax = swap 272 | 273 | x_mid = np.mean([xmin, xmax]) 274 | y_mid = np.mean([ymin, ymax]) 275 | 276 | if not isAnnounced(det_label, x_mid, y_mid): 277 | 278 | # theta min and max corresponds to Pi cam FoV angle 279 | # Picam has 62 degrees horizontal FoV. Need to 280 | # convert to LIDAR angles at LIDAR node. 281 | theta_min = xmin / (x_width / 62) 282 | theta_max = xmax / (x_width / 62) 283 | # print('X pixels =' + str(xmin) + ' image width = ' + str(size[1]) + 284 | # ' theta_min = ' + str(theta_min) + ' theta_max = ' + str(theta_max)) 285 | 286 | now = time.localtime() 287 | 288 | client.publish("object/getdistance", str(det_label) + '|' + 289 | str(theta_min) + '|' + str(theta_max) + '|' + str(now.tm_min * 60 + now.tm_sec)) 290 | 291 | objectsInFrame.append(det_label) 292 | objectMidPts.append((x_mid, y_mid)) 293 | 294 | # List of objects and its mid points in last 30 frames will eb stored in dqueue 295 | if len(objectsInFrame) > 0: 296 | objLastFrames.extend([objectsInFrame]) 297 | objMidsLastFrames.extend([objectMidPts]) 298 | noObjFrames = 0 299 | else: 300 | noObjFrames += 1 301 | 302 | if noObjFrames >= 30: 303 | objMidsLastFrames.clear() 304 | objLastFrames.clear() 305 | noObjFrames = 0 306 | 307 | 308 | 309 | def draw_detections(frame, detections, palette, labels, threshold, output_transform): 310 | size = frame.shape[:2] 311 | frame = output_transform.resize(frame) 312 | boxcount = 0 313 | for detection in detections: 314 | if detection.score > threshold: 315 | boxcount = boxcount + 1 316 | class_id = int(detection.id)-1 317 | color = palette[class_id] 318 | det_label = labels[class_id] if labels and len(labels) >= class_id else '#{}'.format(class_id) 319 | xmin = max(int(detection.xmin), 0) 320 | ymin = max(int(detection.ymin), 0) 321 | xmax = min(int(detection.xmax), size[1]) 322 | ymax = min(int(detection.ymax), size[0]) 323 | xmin, ymin, xmax, ymax = output_transform.scale([xmin, ymin, xmax, ymax]) 324 | 325 | cv2.rectangle(frame, (xmin, ymin), (xmax, ymax), color, 2) 326 | cv2.putText(frame, '{} {:.1%} {}'.format(det_label, detection.score, boxcount), 327 | (xmin, ymin - 7), cv2.FONT_HERSHEY_COMPLEX, 0.6, color, 1) 328 | if isinstance(detection, models.DetectionWithLandmarks): 329 | for landmark in detection.landmarks: 330 | landmark = output_transform.scale(landmark) 331 | cv2.circle(frame, (int(landmark[0]), int(landmark[1])), 2, (0, 255, 255), 2) 332 | return frame 333 | 334 | 335 | def print_raw_results(size, detections, labels, threshold): 336 | log.info(' Class ID | Confidence | XMIN | YMIN | XMAX | YMAX ') 337 | for detection in detections: 338 | if detection.score > threshold: 339 | xmin = max(int(detection.xmin), 0) 340 | ymin = max(int(detection.ymin), 0) 341 | xmax = min(int(detection.xmax), size[1]) 342 | ymax = min(int(detection.ymax), size[0]) 343 | class_id = int(detection.id)-1 344 | det_label = labels[class_id] if labels and len(labels) >= class_id else '#{}'.format(class_id) 345 | log.info('{:^9} | {:10f} | {:4} | {:4} | {:4} | {:4} ' 346 | .format(det_label, detection.score, xmin, ymin, xmax, ymax)) 347 | 348 | 349 | def main(): 350 | args = build_argparser().parse_args() 351 | 352 | log.info('Initializing Inference Engine...') 353 | ie = IECore() 354 | 355 | plugin_config = get_user_config(args.device, args.num_streams, args.num_threads) 356 | 357 | log.info('Loading network...') 358 | 359 | model = get_model(ie, args) 360 | 361 | detector_pipeline = AsyncPipeline(ie, model, plugin_config, 362 | device=args.device, max_num_requests=args.num_infer_requests) 363 | 364 | cap = open_images_capture(args.input, args.loop) 365 | # print(cap) 366 | 367 | next_frame_id = 0 368 | next_frame_id_to_show = 0 369 | 370 | log.info('Starting inference...') 371 | print("To close the application, press 'CTRL+C' here or switch to the output window and press ESC key") 372 | 373 | palette = ColorPalette(len(model.labels) if model.labels else 100) 374 | metrics = PerformanceMetrics() 375 | presenter = None 376 | output_transform = None 377 | video_writer = cv2.VideoWriter() 378 | 379 | while True: 380 | if detector_pipeline.callback_exceptions: 381 | raise detector_pipeline.callback_exceptions[0] 382 | # Process all completed requests 383 | results = detector_pipeline.get_result(next_frame_id_to_show) 384 | # print('test') 385 | # print(results) 386 | if results: 387 | objects, frame_meta = results 388 | frame = frame_meta['frame'] 389 | start_time = frame_meta['start_time'] 390 | 391 | if len(objects) and args.raw_output_message: 392 | print_raw_results(frame.shape[:2], objects, model.labels, args.prob_threshold) 393 | 394 | presenter.drawGraphs(frame) 395 | frame = draw_detections(frame, objects, palette, model.labels, args.prob_threshold, output_transform) 396 | announceDetection(frame, next_frame_id, objects, model.labels, args.prob_threshold, output_transform) 397 | 398 | metrics.update(start_time, frame) 399 | 400 | if video_writer.isOpened() and (args.output_limit <= 0 or next_frame_id_to_show <= args.output_limit-1): 401 | video_writer.write(frame) 402 | next_frame_id_to_show += 1 403 | # print(args.no_show) 404 | if not args.no_show: 405 | cv2.imshow('Detection Results', frame) 406 | key = cv2.waitKey(1) 407 | 408 | ESC_KEY = 27 409 | # Quit. 410 | if key in {ord('q'), ord('Q'), ESC_KEY}: 411 | break 412 | presenter.handleKey(key) 413 | continue 414 | 415 | if detector_pipeline.is_ready(): 416 | # Get new image/frame 417 | start_time = perf_counter() 418 | frame = cap.read() 419 | if frame is None: 420 | if next_frame_id == 0: 421 | raise ValueError("Can't read an image from the input") 422 | break 423 | if next_frame_id == 0: 424 | output_transform = models.OutputTransform(frame.shape[:2], args.output_resolution) 425 | if args.output_resolution: 426 | output_resolution = output_transform.new_resolution 427 | else: 428 | output_resolution = (frame.shape[1], frame.shape[0]) 429 | presenter = monitors.Presenter(args.utilization_monitors, 55, 430 | (round(output_resolution[0] / 4), round(output_resolution[1] / 8))) 431 | if args.output and not video_writer.open(args.output, cv2.VideoWriter_fourcc(*'MJPG'), 432 | cap.fps(), output_resolution): 433 | raise RuntimeError("Can't open video writer") 434 | # Submit for inference 435 | detector_pipeline.submit_data(frame, next_frame_id, {'frame': frame, 'start_time': start_time}) 436 | next_frame_id += 1 437 | 438 | else: 439 | # Wait for empty request 440 | detector_pipeline.await_any() 441 | 442 | detector_pipeline.await_all() 443 | # Process completed requests 444 | for next_frame_id_to_show in range(next_frame_id_to_show, next_frame_id): 445 | results = detector_pipeline.get_result(next_frame_id_to_show) 446 | while results is None: 447 | results = detector_pipeline.get_result(next_frame_id_to_show) 448 | objects, frame_meta = results 449 | frame = frame_meta['frame'] 450 | start_time = frame_meta['start_time'] 451 | 452 | if len(objects) and args.raw_output_message: 453 | print_raw_results(frame.shape[:2], objects, model.labels, args.prob_threshold) 454 | 455 | presenter.drawGraphs(frame) 456 | frame = draw_detections(frame, objects, palette, model.labels, args.prob_threshold, output_transform) 457 | metrics.update(start_time, frame) 458 | 459 | if video_writer.isOpened() and (args.output_limit <= 0 or next_frame_id_to_show <= args.output_limit-1): 460 | video_writer.write(frame) 461 | 462 | if not args.no_show: 463 | cv2.imshow('Detection Results', frame) 464 | key = cv2.waitKey(1) 465 | 466 | ESC_KEY = 27 467 | # Quit. 468 | if key in {ord('q'), ord('Q'), ESC_KEY}: 469 | break 470 | presenter.handleKey(key) 471 | 472 | metrics.print_total() 473 | print(presenter.reportMeans()) 474 | 475 | 476 | if __name__ == '__main__': 477 | 478 | sys.exit(main() or 0) 479 | -------------------------------------------------------------------------------- /efficientdet-d0-tf/efficientdet-d0-tf.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdroitAnandAI/ADAS-Car-using-Raspberry-Pi/3775f9bc1b03d564cc6e16f1b159c721d1ecf21b/efficientdet-d0-tf/efficientdet-d0-tf.bin -------------------------------------------------------------------------------- /images/ADAS_Architecture.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdroitAnandAI/ADAS-Car-using-Raspberry-Pi/3775f9bc1b03d564cc6e16f1b159c721d1ecf21b/images/ADAS_Architecture.jpeg -------------------------------------------------------------------------------- /images/CodeCogsEqn.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdroitAnandAI/ADAS-Car-using-Raspberry-Pi/3775f9bc1b03d564cc6e16f1b159c721d1ecf21b/images/CodeCogsEqn.gif -------------------------------------------------------------------------------- /images/Sensor_Fusion_Output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdroitAnandAI/ADAS-Car-using-Raspberry-Pi/3775f9bc1b03d564cc6e16f1b159c721d1ecf21b/images/Sensor_Fusion_Output.gif -------------------------------------------------------------------------------- /images/adas.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdroitAnandAI/ADAS-Car-using-Raspberry-Pi/3775f9bc1b03d564cc6e16f1b159c721d1ecf21b/images/adas.gif -------------------------------------------------------------------------------- /images/bom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdroitAnandAI/ADAS-Car-using-Raspberry-Pi/3775f9bc1b03d564cc6e16f1b159c721d1ecf21b/images/bom.jpg -------------------------------------------------------------------------------- /images/dummy.jpg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /images/equation.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdroitAnandAI/ADAS-Car-using-Raspberry-Pi/3775f9bc1b03d564cc6e16f1b159c721d1ecf21b/images/equation.jpeg -------------------------------------------------------------------------------- /images/homo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdroitAnandAI/ADAS-Car-using-Raspberry-Pi/3775f9bc1b03d564cc6e16f1b159c721d1ecf21b/images/homo.jpeg -------------------------------------------------------------------------------- /images/lidar_cam_geometry.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdroitAnandAI/ADAS-Car-using-Raspberry-Pi/3775f9bc1b03d564cc6e16f1b159c721d1ecf21b/images/lidar_cam_geometry.jpeg -------------------------------------------------------------------------------- /images/lidar_cam_sf.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdroitAnandAI/ADAS-Car-using-Raspberry-Pi/3775f9bc1b03d564cc6e16f1b159c721d1ecf21b/images/lidar_cam_sf.jpeg -------------------------------------------------------------------------------- /images/lidar_converstion_formula.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdroitAnandAI/ADAS-Car-using-Raspberry-Pi/3775f9bc1b03d564cc6e16f1b159c721d1ecf21b/images/lidar_converstion_formula.jpeg -------------------------------------------------------------------------------- /images/thumbnail_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdroitAnandAI/ADAS-Car-using-Raspberry-Pi/3775f9bc1b03d564cc6e16f1b159c721d1ecf21b/images/thumbnail_play.png -------------------------------------------------------------------------------- /images/trafficSign.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdroitAnandAI/ADAS-Car-using-Raspberry-Pi/3775f9bc1b03d564cc6e16f1b159c721d1ecf21b/images/trafficSign.gif -------------------------------------------------------------------------------- /lidar_getdist.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | The getObjectDistance (angle_min, angle_max) function will get the 4 | distance of LIDAR points between angle_min and angle_max. Then it takes 5 | the median of distances and return it. This function is used as a 6 | supporting method for the ADAS system after bounding box detection of 7 | objects. The x_min and x_max of bounding boxes are used to compute the 8 | angle_min and angle_max which is passed in to the getObjectDistance() in 9 | this module to estimate the object distance. 10 | 11 | The code is extensively modified from the base code provided by ADAFruit. 12 | 13 | Adafruit invests time and resources providing this open source code. 14 | Please support Adafruit and open source hardware by purchasing 15 | products from Adafruit! 16 | 17 | Written by Dave Astels for Adafruit Industries 18 | Copyright (c) 2019 Adafruit Industries 19 | Licensed under the MIT license. 20 | 21 | All text above must be included in any redistribution. 22 | """ 23 | 24 | import os 25 | from math import cos, sin, pi, floor 26 | import math 27 | import paho.mqtt.client as mqtt 28 | import pygame 29 | from adafruit_rplidar import RPLidar 30 | import numpy as np 31 | 32 | # Screen width & height 33 | W = 640 34 | H = 480 35 | 36 | SCAN_BYTE = b'\x20' 37 | SCAN_TYPE = 129 38 | 39 | 40 | import ledshim 41 | import colorsys 42 | import time 43 | from sys import exit 44 | 45 | 46 | def make_gaussian(fwhm): 47 | x = np.arange(0, ledshim.NUM_PIXELS, 1, float) 48 | y = x[:, np.newaxis] 49 | x0, y0 = 3.5, (ledshim.NUM_PIXELS / 2) - 0.5 50 | fwhm = fwhm 51 | gauss = np.exp(-4 * np.log(2) * ((x - x0) ** 2 + (y - y0) ** 2) / fwhm ** 2) 52 | return gauss 53 | 54 | 55 | def flashLight(): 56 | print('inside flashlight') 57 | for _ in range(10): 58 | 59 | ledshim.set_clear_on_exit() 60 | 61 | for z in list(range(1, 10)[::-1]) + list(range(1, 10)): 62 | fwhm = 15.0 / z 63 | gauss = make_gaussian(fwhm) 64 | print('after make gaussian') 65 | start = time.time() 66 | y = 4 67 | for x in range(ledshim.NUM_PIXELS): 68 | h = 0.5 69 | s = 1.0 70 | v = gauss[x, y] 71 | rgb = colorsys.hsv_to_rgb(h, s, v) 72 | r, g, b = [int(255.0 * i) for i in rgb] 73 | ledshim.set_pixel(x, r, g, b) 74 | ledshim.show() 75 | end = time.time() 76 | t = end - start 77 | if t < 0.04: 78 | time.sleep(0.04 - t) 79 | 80 | 81 | # Setup the RPLidar 82 | PORT_NAME = '/dev/ttyUSB0' 83 | lidar = RPLidar(None, PORT_NAME) 84 | 85 | # used to scale data to fit on the screen 86 | max_distance = 0 87 | 88 | #pylint: disable=redefined-outer-name,global-statement 89 | def process_data(data): 90 | global max_distance 91 | lcd.fill((0,0,0)) 92 | point = ( int(W / 2) , int(H / 2) ) 93 | 94 | pygame.draw.circle(lcd,pygame.Color(255, 255, 255),point,10 ) 95 | pygame.draw.circle(lcd,pygame.Color(100, 100, 100),point,100 , 1 ) 96 | pygame.draw.line( lcd,pygame.Color(100, 100, 100) , ( 0, int(H/2)),( W , int(H/2) ) ) 97 | pygame.draw.line( lcd,pygame.Color(100, 100, 100) , ( int(W/2),0),( int(W/2) , H ) ) 98 | 99 | for angle in range(360): 100 | distance = data[angle] 101 | if distance > 0: # ignore initially ungathered data points 102 | max_distance = max([min([5000, distance]), max_distance]) 103 | radians = angle * pi / 180.0 104 | x = distance * cos(radians) 105 | y = distance * sin(radians) 106 | point = ( int(W / 2) + int(x / max_distance * (W/2)), int(H/2) + int(y / max_distance * (H/2) )) 107 | pygame.draw.circle(lcd,pygame.Color(255, 0, 0),point,2 ) 108 | pygame.display.update() 109 | 110 | 111 | scan_data = [0]*360 112 | 113 | def _process_scan(raw): 114 | '''Processes input raw data and returns measurment data''' 115 | new_scan = bool(raw[0] & 0b1) 116 | inversed_new_scan = bool((raw[0] >> 1) & 0b1) 117 | quality = raw[0] >> 2 118 | if new_scan == inversed_new_scan: 119 | raise RPLidarException('New scan flags mismatch') 120 | check_bit = raw[1] & 0b1 121 | if check_bit != 1: 122 | raise RPLidarException('Check bit not equal to 1') 123 | angle = ((raw[1] >> 1) + (raw[2] << 7)) / 64. 124 | distance = (raw[3] + (raw[4] << 8)) / 4. 125 | return new_scan, quality, angle, distance 126 | 127 | def lidar_measurments(self, max_buf_meas=500): 128 | 129 | lidar.set_pwm(800) 130 | status, error_code = self.health 131 | 132 | cmd = SCAN_BYTE 133 | self._send_cmd(cmd) 134 | dsize, is_single, dtype = self._read_descriptor() 135 | if dsize != 5: 136 | raise RPLidarException('Wrong info reply length') 137 | if is_single: 138 | raise RPLidarException('Not a multiple response mode') 139 | if dtype != SCAN_TYPE: 140 | raise RPLidarException('Wrong response data type') 141 | while True: 142 | raw = self._read_response(dsize) 143 | self.log_bytes('debug', 'Received scan response: ', raw) 144 | if max_buf_meas: 145 | data_in_buf = self._serial_port.in_waiting 146 | if data_in_buf > max_buf_meas*dsize: 147 | self.log('warning', 148 | 'Too many measurments in the input buffer: %d/%d. ' 149 | 'Clearing buffer...' % 150 | (data_in_buf//dsize, max_buf_meas)) 151 | self._serial_port.read(data_in_buf//dsize*dsize) 152 | yield _process_scan(raw) 153 | 154 | def lidar_scans(self, max_buf_meas=500, min_len=5): 155 | 156 | scan = [] 157 | iterator = lidar_measurments(lidar,max_buf_meas) 158 | for new_scan, quality, angle, distance in iterator: 159 | if new_scan: 160 | if len(scan) > min_len: 161 | yield scan 162 | scan = [] 163 | if quality > 0 and distance > 0: 164 | scan.append((quality, angle, distance)) 165 | 166 | def getObjectDistance (angle_min, angle_max): 167 | 168 | minDist = 0 169 | lidar = RPLidar(None, PORT_NAME) 170 | 171 | try: 172 | for scan in lidar_scans(lidar): 173 | 174 | for (_, angle, distance) in scan: 175 | # print("Angle = " + str(angle) + "distance == " + str(distance)) 176 | scan_data[min([359, floor(angle)])] = distance 177 | 178 | 179 | # fetching all non zero distance values between subtended angles 180 | allDists = [scan_data[i] for i in range(360) 181 | if i >= angle_min and i <= angle_max and scan_data[i] > 0] 182 | 183 | # if half the distance values are filled in then break 184 | if (2 * len(allDists) > angle_max - angle_min): 185 | 186 | minDist = np.median(allDists) 187 | lidar.stop() 188 | lidar.disconnect() 189 | 190 | return minDist 191 | 192 | except KeyboardInterrupt: 193 | print('Stopping LIDAR Scan') 194 | 195 | def roundtoTen(x): 196 | return int(math.ceil(x/10.0)) * 10 197 | 198 | def on_connect(client, userdata, flags, rc): 199 | print("Connected with result code " + str(rc)) 200 | client.subscribe("object/getdistance") 201 | 202 | 203 | def on_message(client, userdata, msg): 204 | 205 | # print(msg.payload.decode()) 206 | 207 | word = msg.payload.decode() 208 | 209 | # objAttributes contains label, 210 | # theta min and max separated by | 211 | objAttributes = word.split('|') 212 | 213 | now = time.localtime() 214 | if (now.tm_min * 60 + now.tm_sec - int(objAttributes[3]) >= 1): 215 | return 216 | 217 | theta1 = float(objAttributes[1]) 218 | theta2 = float(objAttributes[2]) 219 | 220 | dist = getObjectDistance(int(theta1) + 90 + 59, int(theta2) + 90 + 59) 221 | 222 | # convert distance from mm to cms 223 | dist = round(float (dist / 1000), 1) 224 | 225 | # print('The distance to the object = ' + str(dist)) 226 | 227 | theta_mid = int((theta1 + theta2) / 2) 228 | print(' Object is at an angle of ' + str(theta_mid)) 229 | 230 | # if near then announce an alert! 231 | # Passing the hue value on MQTT. 0 = Red. 0.3 = Green 232 | if (dist < 2.0): 233 | # print('setting alert msg') 234 | announceText = "ALERT ALERT " 235 | client.publish("object/flashlight", "0.0") 236 | else: 237 | announceText = "" 238 | client.publish("object/flashlight", "0.3") 239 | 240 | announceText = announceText + str(objAttributes[0]) + ' at ' + str(dist) + ' meters. ' 241 | 242 | # theta_mid can vary from 0 to 62 degrees 243 | if theta_mid > 40: 244 | # print('Right Side') 245 | os.system( 246 | 'espeak \"' + announceText + str (roundtoTen(abs(theta_mid - 31))) + ' degrees right\"') 247 | elif theta_mid < 21: 248 | # print('Left Side') 249 | os.system( 250 | 'espeak \"' + announceText + str (roundtoTen(abs(31 - theta_mid))) + ' degrees left\"') 251 | else: 252 | # theta_mid will be > 20 and < 40, if here 253 | if theta_mid < 30: 254 | direction = ' Right ' 255 | else: 256 | direction = ' Left ' 257 | # Alert to slide to opposite direction at theta + 30 degrees 258 | os.system( 259 | 'espeak \"' + announceText + 'Slide ' + direction + str(abs(roundtoTen(abs(31 - theta_mid)) + 30)) + 'degrees \"') 260 | 261 | 262 | client = mqtt.Client() 263 | client.connect("localhost", 1883, 600) 264 | 265 | client.on_connect = on_connect 266 | client.on_message = on_message 267 | 268 | try: 269 | client.loop_forever() 270 | # To catch SigINT 271 | except KeyboardInterrupt: 272 | client.disconnect() 273 | -------------------------------------------------------------------------------- /models.lst: -------------------------------------------------------------------------------- 1 | # This file can be used with the --list option of the model downloader. 2 | # For --architecture_type=centernet 3 | ctdet_coco_dlav0_384 4 | ctdet_coco_dlav0_512 5 | # For --architecture_type=ctpn 6 | ctpn 7 | # For --architecture_type=faceboxes 8 | faceboxes-pytorch 9 | # For --architecture_type=retinaface-pytorch 10 | retinaface-resnet50-pytorch 11 | # For --architecture_type=ssd 12 | efficientdet-d0-tf 13 | efficientdet-d1-tf 14 | face-detection-???? 15 | face-detection-adas-???? 16 | face-detection-retail-???? 17 | faster-rcnn-resnet101-coco-sparse-60-0001 18 | pedestrian-and-vehicle-detector-adas-???? 19 | pedestrian-detection-adas-???? 20 | pelee-coco 21 | person-detection-???? 22 | person-detection-retail-0013 23 | person-vehicle-bike-detection-???? 24 | product-detection-0001 25 | retinanet-tf 26 | rfcn-resnet101-coco-tf 27 | ssd300 28 | ssd512 29 | ssd_mobilenet_v1_coco 30 | ssd_mobilenet_v1_fpn_coco 31 | ssd_mobilenet_v2_coco 32 | ssd_resnet50_v1_fpn_coco 33 | ssd-resnet34-1200-onnx 34 | ssdlite_mobilenet_v2 35 | vehicle-detection-???? 36 | vehicle-detection-adas-???? 37 | vehicle-license-plate-detection-barrier-???? 38 | # For --architecture_type=ultra_lightweight_face_detection 39 | ultra-lightweight-face-detection-rfb-320 40 | ultra-lightweight-face-detection-slim-320 41 | # For --architecture_type=yolo 42 | mobilefacedet-v1-mxnet 43 | person-vehicle-bike-detection-crossroad-yolov3-1020 44 | yolo-v1-tiny-tf 45 | yolo-v2-ava-0001 46 | yolo-v2-ava-sparse-??-0001 47 | yolo-v2-tf 48 | yolo-v2-tiny-ava-0001 49 | yolo-v2-tiny-ava-sparse-??-0001 50 | yolo-v2-tiny-tf 51 | yolo-v2-tiny-vehicle-detection-0001 52 | yolo-v3-tf 53 | yolo-v3-tiny-tf 54 | # For --architecture_type=yolov4 55 | yolo-v4-tf 56 | yolo-v4-tiny-tf 57 | -------------------------------------------------------------------------------- /pulse_mqtt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from math import cos, sin, pi, floor 5 | import paho.mqtt.client as mqtt 6 | import colorsys 7 | import time 8 | from sys import exit 9 | 10 | try: 11 | import numpy as np 12 | except ImportError: 13 | exit('This script requires the numpy module\nInstall with: sudo pip install numpy') 14 | 15 | import ledshim 16 | 17 | ledshim.set_clear_on_exit() 18 | 19 | 20 | def make_gaussian(fwhm): 21 | x = np.arange(0, ledshim.NUM_PIXELS, 1, float) 22 | y = x[:, np.newaxis] 23 | x0, y0 = 3.5, (ledshim.NUM_PIXELS / 2) - 0.5 24 | fwhm = fwhm 25 | gauss = np.exp(-4 * np.log(2) * ((x - x0) ** 2 + (y - y0) ** 2) / fwhm ** 2) 26 | return gauss 27 | 28 | 29 | def flashLight(colorCode): 30 | # print('inside flashlight') 31 | for _ in range(5): 32 | 33 | ledshim.set_clear_on_exit() 34 | 35 | for z in list(range(1, 10)[::-1]) + list(range(1, 10)): 36 | fwhm = 15.0 / z 37 | gauss = make_gaussian(fwhm) 38 | 39 | start = time.time() 40 | y = 4 41 | for x in range(ledshim.NUM_PIXELS): 42 | h = colorCode 43 | s = 1.0 44 | v = gauss[x, y] 45 | rgb = colorsys.hsv_to_rgb(h, s, v) 46 | r, g, b = [int(255.0 * i) for i in rgb] 47 | ledshim.set_pixel(x, r, g, b) 48 | ledshim.show() 49 | end = time.time() 50 | t = end - start 51 | if t < 0.04: 52 | time.sleep(0.04 - t) 53 | 54 | 55 | 56 | def on_connect(client, userdata, flags, rc): 57 | print("Connected with result code " + str(rc)) 58 | client.subscribe("object/flashlight") 59 | 60 | 61 | def on_message(client, userdata, msg): 62 | 63 | colorCode = msg.payload.decode() 64 | flashLight(float(colorCode)) 65 | 66 | 67 | client = mqtt.Client() 68 | client.connect("localhost", 1883, 600) 69 | 70 | client.on_connect = on_connect 71 | client.on_message = on_message 72 | 73 | try: 74 | client.loop_forever() 75 | # To catch SigINT 76 | except KeyboardInterrupt: 77 | client.disconnect() -------------------------------------------------------------------------------- /ssdlite_mobilenet_v2/ssdlite_mobilenet_v2.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdroitAnandAI/ADAS-Car-using-Raspberry-Pi/3775f9bc1b03d564cc6e16f1b159c721d1ecf21b/ssdlite_mobilenet_v2/ssdlite_mobilenet_v2.bin --------------------------------------------------------------------------------