|
| 1 | +原文:[Building a (semi) Autonomous Drone with Python](http://blog.yhat.com/posts/autonomous-droning-with-python.html) |
| 2 | + |
| 3 | +--- |
| 4 | + |
| 5 | +They might not be delivering our mail ([or our burritos](http://tacocopter.com/)) yet, but drones |
| 6 | +are now simple, small, and affordable enough that they can be considered a toy. You can |
| 7 | +even customize and program some of them via handy dandy Application Programming Interfaces ([APIs](https://www.quora.com/What-is-an-API-4))! |
| 8 | +The Parrot AR Drone has an API that lets you control not only the drone's movement but also stream video and images from its camera. |
| 9 | +In this post, I'll show you how you can use Python and node.js to build a drone that |
| 10 | +moves all by itself. |
| 11 | + |
| 12 | + |
| 13 | + |
| 14 | +Hold onto your butts. |
| 15 | + |
| 16 | +### The Project |
| 17 | + |
| 18 | +So given that I'm not a drone, or a machine vision professional, I'm going to have |
| 19 | +to keep things simple. For this project, I'm going to **teach my drone how to follow |
| 20 | +a red object**. |
| 21 | + |
| 22 | +I know, I know, it's a far cry from a T-800 Model 101 (or [even something like this](https://www.youtube.com/watch?v=YQIMGV5vtd4)), but |
| 23 | +given my time and budget constraints it's a good place to start! In the meantime, feel free to send your best autonomous terminators or drone swarms my way. |
| 24 | + |
| 25 | + |
| 26 | + |
| 27 | +No neural net processors here, just node.js and Python. |
| 28 | + |
| 29 | +### The Drone |
| 30 | + |
| 31 | +When I opened my drone on Christmas morning I wasn't entirely sure what I was going |
| 32 | +to do with it, but one thing was for certain: This thing was cool. The [AR Drone 2.0](http://ardrone2.parrot.com/) (I know |
| 33 | +super lame name) is a quadcopter. If you're imagining those fit in the palm of your hand, single-rotor, |
| 34 | +RC gizmos, you're in the wrong ballpark. The first thing I noticed (and was most surprised by) |
| 35 | +was how big the AR Drone is. With its "indoor shell" on, it's about 2 feet wide, 2 feet long, |
| 36 | +and 6 inches high. It's also kind of loud--in a good way (like a terrify your dog kind of way, unlike [this down to drone pup](https://vimeo.com/95078536)). |
| 37 | +Combine that with 2 cameras--one front and one bottom, and you've got yourself the ultimate grown up geek toy. |
| 38 | + |
| 39 | + |
| 40 | + |
| 41 | +### Programming your drone |
| 42 | + |
| 43 | +What sets the AR Drone apart is that it's old (in drone years)--it was first released in 2012. This |
| 44 | +might seem like a bad thing BUT since we're trying to program this gizmo, it's actually |
| 45 | +a good thing. |
| 46 | + |
| 47 | +Given that it's had 4 years to "mature", there are some really great APIs, helper |
| 48 | +libraries, and project/code samples for controlling/programming the drone (see list of resources below). So in essence, someone |
| 49 | +else has already done the hard part of figuring out how to communicate with the drone in bytecode, so all I have |
| 50 | +to do is import the _node_module_ and I'm off to the figurative [drone races](http://thedroneracingleague.com/). |
| 51 | + |
| 52 | +Programming the drone is actually quite easy. I'm using the [`ar-drone`](https://github.com/felixge/node-ar-drone) node.js module. I've |
| 53 | +found that it works really well despite not being under _super_ active development. To start, let me |
| 54 | +show you how to do a pre-programmed flightplan. The following program is going to: |
| 55 | + |
| 56 | +* connect to the drone over wifi |
| 57 | +* tell the drone to takeoff |
| 58 | +* after 1 second, spin clockwise at full throttle |
| 59 | +* 1 second after that, stop and then move forwards at 50% thrust |
| 60 | +* another 1 second after that, stop and land |
| 61 | + |
| 62 | +Pretty simple little program. Now even though it's pretty straightforward, I will |
| 63 | +still **highly recommend having an _[emergency landing](https://gist.github.com/glamp/0b6f0ef87525e3cefcfb4f5bd146712c)_ script readily available**. Cause |
| 64 | +you never know you need one till you really need one ;) |
| 65 | +```python |
| 66 | +var arDrone = require('ar-drone'); |
| 67 | +var drone = arDrone.createClient(); |
| 68 | +drone.takeoff(); |
| 69 | + |
| 70 | +drone |
| 71 | + .after(1000, function() { |
| 72 | + drone.clockwise(1.0); |
| 73 | + }) |
| 74 | + .after(1000, function() { |
| 75 | + drone.stop(); |
| 76 | + drone.front(0.5); |
| 77 | + }) |
| 78 | + .after(1000, function() { |
| 79 | + drone.stop(); |
| 80 | + drone.land(); |
| 81 | + }) |
| 82 | +``` |
| 83 | + <iframe width="420" height="315" src="https://www.youtube.com/embed/Edh98lNtFfo" frameborder="0" allowfullscreen=""></iframe> |
| 84 | + |
| 85 | + |
| 86 | +You can also pull off some fancier moves--you know, to impress your friends. My personal |
| 87 | +favorite is a backflip. |
| 88 | + |
| 89 | + |
| 90 | +<iframe width="420" height="315" src="https://www.youtube.com/embed/aF8V8p1n3P0" frameborder="0" allowfullscreen=""></iframe> |
| 91 | + |
| 92 | + |
| 93 | +### Le Machine Vision |
| 94 | + |
| 95 | +Ok now for the second piece of the puzzle: teaching our drone how to see. To do this, we're going |
| 96 | +to be using [OpenCV](http://opencv.org/) and the Python module [cv2](http://opencv-python-tutroals.readthedocs.org/). OpenCV can |
| 97 | +be a little prickly to work with, but it can do some really impressive stuff and even has |
| 98 | +some machine learning libraries baked right into it. |
| 99 | + |
| 100 | +We're going to be using OpenCV to do some basic object tracking. We're going to have the |
| 101 | +**camera track anything red** that appears |
| 102 | +in its field of vision. Sort of like a bull at a bullfight. |
| 103 | + |
| 104 | + |
| 105 | + |
| 106 | +Just like this, except substitute the bull for a drone, and the red cape (_muleta_) for a Greg ☹! Also, my pants aren't quite that tight. |
| 107 | + |
| 108 | +Good news for us is that `cv2` makes this really easy to do. |
| 109 | +```python |
| 110 | +import numpy as np |
| 111 | +import cv2 |
| 112 | +from skimage.color import rgb2gray |
| 113 | +from PIL import Image |
| 114 | +from StringIO import StringIO |
| 115 | +from scipy import ndimage |
| 116 | +import base64 |
| 117 | +import time |
| 118 | + |
| 119 | +def get_coords(img64): |
| 120 | + "Reads in a base64 encoded image, filters for red, and then calculates the center of the red" |
| 121 | + # convert the base64 encoded image a numpy array |
| 122 | + binaryimg = base64.decodestring(img64) |
| 123 | + pilImage = Image.open(StringIO(binaryimg)) |
| 124 | + image = np.array(pilImage) |
| 125 | + |
| 126 | + # create lower and upper bounds for red |
| 127 | + red_lower = np.array([17, 15, 100], dtype="uint8") |
| 128 | + red_upper = np.array([50, 56, 200], dtype="uint8") |
| 129 | + |
| 130 | + # perform the filtering. mask is another word for filter |
| 131 | + mask = cv2.inRange(image, red_lower, red_upper) |
| 132 | + output = cv2.bitwise_and(image, image, mask=mask) |
| 133 | + # convert the image to grayscale, then calculate the center of the red (only remaining color) |
| 134 | + output_gray = rgb2gray(output) |
| 135 | + y, x = ndimage.center_of_mass(output_gray) |
| 136 | + |
| 137 | + data = { |
| 138 | + "x": x, |
| 139 | + "y": y, |
| 140 | + "xmax": output_gray.shape[1], |
| 141 | + "ymax": output_gray.shape[0], |
| 142 | + "time": time.time() |
| 143 | + } |
| 144 | + return data |
| 145 | +``` |
| 146 | + |
| 147 | +As you can see above, I'm using a color mask to filter the pixels in an image. It's |
| 148 | +a simple but intuitive approach. And more importantly **it works**. Take a look: |
| 149 | + |
| 150 | +<div class="row"> |
| 151 | + <div class="col-sm-6"> |
| 152 | +  |
| 153 | + Raw camera feed |
| 154 | + </div> |
| 155 | + <div class="col-sm-6"> |
| 156 | +  |
| 157 | + Processed with red filter |
| 158 | + </div> |
| 159 | +</div> |
| 160 | + |
| 161 | +It's learning! Ok well maybe not quite like a T-800 Model 101, but it's at least a start. |
| 162 | + |
| 163 | + |
| 164 | + |
| 165 | +Is the red dot a coincidence? Think again... |
| 166 | +<div class="row"> |
| 167 | + <div class="col-sm-6"> |
| 168 | +  |
| 169 | + Raw camera feed |
| 170 | + </div> |
| 171 | + <div class="col-sm-6"> |
| 172 | +  |
| 173 | + Processed with red filter |
| 174 | + </div> |
| 175 | +</div> |
| 176 | + |
| 177 | +### Stitching things together |
| 178 | + |
| 179 | +Ok here comes the tricky part. We've got our little node.js script that can control |
| 180 | +the drone's navigation, and we've got the python bit that can detect where red things |
| 181 | +are in an image, but the question looms: **How do we glue them together?** |
| 182 | + |
| 183 | + |
| 184 | + |
| 185 | +Well my friends, to do this I'm going to use Yhat's own model deployment software, [ScienceOps](https://www.yhat.com/products/scienceops). |
| 186 | +I'm going to deploy my Python code onto ScienceOps, where it'll be accessible via an API, and then from node.js |
| 187 | +I can call my model on ScienceOps. What this means is that I've boiled my OpenCV red-filtering |
| 188 | +model into a really simple HTTP endpoint. I'm using ScienceOps to make my childhood drone bull fighting dreams come true, but |
| 189 | +you could use it to embed any R or Python model into any application capable of making API requests, be it drone or otherwise. |
| 190 | + |
| 191 | + |
| 192 | + |
| 193 | +No more recoding to get models into production. FREEDOM! |
| 194 | + |
| 195 | +I don't need to mess around with any cross-platform |
| 196 | +baloney, and if I need to up the horsepower of my model (say for instance if I'm controlling more than |
| 197 | +one drone), I can let ScienceOps scale out my model automatically. If you want more info about |
| 198 | +deploying models (or drones) into production using ScienceOps, head over to our [site](www.yhat.com) or |
| 199 | +schedule a demo to see it live. |
| 200 | +```python |
| 201 | +from yhat import Yhat, YhatModel |
| 202 | + |
| 203 | +class DroneModel(YhatModel): |
| 204 | + REQUIREMENTS = [ |
| 205 | + "opencv" |
| 206 | + ] |
| 207 | + def execute(self, data): |
| 208 | + return get_coords(data['image64']) |
| 209 | + |
| 210 | +yh = Yhat(USERNAME, APIKEY, "https://sandbox.yhathq.com/") |
| 211 | +yh.deploy("DroneModel", DroneModel, globals(), True) |
| 212 | +``` |
| 213 | + |
| 214 | +What does all this mean? Well for one, it means my node.js code just got a lot simpler. I can even |
| 215 | +use the Yhat node.js library to execute my model: |
| 216 | +```python |
| 217 | +var fs = require('fs'); |
| 218 | +var img = fs.readFileSync('./example-image.png').toString('base64'); |
| 219 | + |
| 220 | +var yhat = require('yhat'); |
| 221 | +var cli = yhat.init('greg', 'my-apikey-goes-here', 'https://sandbox.yhathq.com/'); |
| 222 | + |
| 223 | +cli.predict('DroneModel', base64edImage, function(err, data) { |
| 224 | + console.log(JSON.stringify(data, null, 2)); |
| 225 | + // { |
| 226 | + // "result": { |
| 227 | + // "time": 1460067540.30213, |
| 228 | + // "total_red": 5.810973333333334, |
| 229 | + // "x": 425.0256166460453, |
| 230 | + // "xmax": 640, |
| 231 | + // "y": 220.03434178824077, |
| 232 | + // "ymax": 360 |
| 233 | + // }, |
| 234 | + // "version": 1, |
| 235 | + // "yhat_id": "529b84c9c4957008446a56faadc152a6", |
| 236 | + // "yhat_model": "DroneModel" |
| 237 | + // } |
| 238 | + var x = data.x / data.xmax - 0.5 |
| 239 | + , y = data.y / data.ymax - 0.5; |
| 240 | + |
| 241 | + if (x > 0) { |
| 242 | + drone.right(Math.abs(x)); |
| 243 | + } else { |
| 244 | + drone.left(Math.abs(x)); |
| 245 | + } |
| 246 | + if (y > 0) { |
| 247 | + drone.up(Math.abs(y)); |
| 248 | + } else { |
| 249 | + drone.down(Math.abs(y)); |
| 250 | + } |
| 251 | +}); |
| 252 | +``` |
| 253 | + |
| 254 | +Sweet! Now I can pretty much just drop this into my navigation script. All I need to do is tell |
| 255 | +my script how I want to react to the response. In this case it's going to be a couple steps: |
| 256 | + |
| 257 | +* Call the `DroneModel` model hosted on ScienceOps |
| 258 | +* If there weren't any errors, look at the result. The result will give me the `x` and `y` coordinates |
| 259 | +of the red in the image. |
| 260 | +* Make course adjustments to the drone that attempt to move the red to the center of the drone's |
| 261 | +field of vision. |
| 262 | + |
| 263 | +So simple! What could possibly go wrong? |
| 264 | + |
| 265 | + |
| 266 | + <iframe width="560" height="315" src="https://www.youtube.com/embed/ZVDfMPHqHKc?t=7" frameborder="0" allowfullscreen=""></iframe> |
| 267 | + |
| 268 | + |
| 269 | +### Mending my metaphorical stitching |
| 270 | + |
| 271 | +As the adage goes, If at first you don't succeed try, try again. It took me a few |
| 272 | +iterations to get the autonomous piece to actually work. Turns out, combining individual |
| 273 | +components has the propensity to compound your error! |
| 274 | + |
| 275 | +But not to worry! My drone took its fair share of bumps and bruises but it's a tough |
| 276 | +little guy--**Pro Tip: **You can patch up your drone with duct tape. Just be sure to apply |
| 277 | +equal amounts to each side of the drone so it's balanced! |
| 278 | + |
| 279 | + |
| 280 | + |
| 281 | +Duct Tape: More than a metaphor. |
| 282 | + |
| 283 | +A couple of things I learned the hard way: |
| 284 | + |
| 285 | +* **_Build a helper app: _** After a few trial runs I built a helper app (see video below) to |
| 286 | +determine what/why things were happening. Let me tell you, **this should've been step #1**. It |
| 287 | +was invaluable being able to see what code my program was executing and what the processed |
| 288 | +images looked like. |
| 289 | +* **_Don't over-correct: _** For simple things like this, if you tell the drone to do |
| 290 | +too much at the same time, it freaks out and either (a) just sits there or (b) starts |
| 291 | +errantly flying all over the room (see video above). |
| 292 | +* **_Always have an emergency landing script handy: _** I mentioned this earlier but it can't |
| 293 | +be overemphasized. The reason is that if your program crashes and you haven't landed your |
| 294 | +drone, you're in deep ... trouble. Your drone is going to keep flying (possibly errantly) until |
| 295 | +you tell it to land. Having [`emergency-landing.js`](https://gist.github.com/glamp/0b6f0ef87525e3cefcfb4f5bd146712c) handy |
| 296 | +will save you some maintenance (and possibly from a lawsuit). |
| 297 | +* **_If it's windy, don't fly your drone: _** Learned this one the _really_ hard way... |
| 298 | + |
| 299 | +In the end with some persistence and a little luck, I was able to get a couple of |
| 300 | +good autonomous runs in! |
| 301 | + |
| 302 | + |
| 303 | + <iframe width="560" height="315" src="https://www.youtube.com/embed/VdgQajDuA5E" frameborder="0" allowfullscreen=""></iframe> |
| 304 | + |
| 305 | + |
| 306 | +### In the wild |
| 307 | + |
| 308 | +I wound up presenting this at [PAPIs Valencia](http://www.papis.io/connect-valencia-2016/program) which |
| 309 | +was a lot of fun (BTW PAPIs is awesome! I highly recommend it for anyone interested |
| 310 | +in predictive analytics). Unfortunately [my PAPIs demo didn't go quite as smoothly](http://www.papis.io/connect-valencia-2016/talks/2016/3/using-ml-to-build-an-autonomous-drone-greg-lamp). The lighting |
| 311 | +in the lecture hall was different than in our office and as a result, the red didn't |
| 312 | +quite get filtered the same way. Despite the less than stellar performance, it was |
| 313 | +still a lot of fun! |
| 314 | + |
| 315 | +### Resources |
| 316 | + |
| 317 | +Want to learn more about programming your own drone? Here are some great resources |
| 318 | +for getting started: |
| 319 | + |
| 320 | +* [NodeCopter](http://www.nodecopter.com/) - JS community for drones. Not really active anymore, but has some great getting started guides. |
| 321 | +* [node-ar-drone](https://github.com/felixge/node-ar-drone) - Node library for programming your drone. |
| 322 | +* [Parrot AR Drone 2.0](http://www.amazon.com/Parrot-Drone-Quadricopter-Edition-Orange/dp/B007HZLLOK) - Buy it here. |
| 323 | +* [OpenCV](http://opencv.org/) - Info for using OpenCV. |
| 324 | +* [scikit-image](http://scikit-image.org/) - Higher level, more ML focused computer vision library. |
| 325 | +* [dronestream](https://github.com/bkw/node-dronestream) - Realtime video feed for Parrot AR 2.0. |
| 326 | + |
| 327 | +Also, here's a [link](https://github.com/yhat/semi-autonomous-drone) to the repo if you want my code. |
| 328 | + |
| 329 | + |
| 330 | + <iframe width="420" height="315" src="https://www.youtube.com/embed/2fWr6CBARMw" frameborder="0" allowfullscreen=""></iframe> |
| 331 | + |
| 332 | +One for the road. |
0 commit comments