Team Two/Final
From Maslab 2007
Final Paper
Contents |
Basic Strategy
Overview
We use a greedy strategy, essentially approaching and picking up the nearest ball we see, driving forward whene unobstructed, avoiding walls by turning when we are too close to one, and, if we are holding any balls, going towards a goal as soon as it appears. More specifically, we decide what to do by keeping track of three state variables, the closest ball that is in our latest image, the closest goal that is in our latest image, and whether we have a ball or not. (The last is set to true every time we try to eat a ball and set to false every time we try to push a ball into a goal.)
If the closest ball is null, and the closest goal is null or we do not have a ball, then we wander. If the closest ball is not null, then we try to eat it. If the closest ball is null, we have a ball, and the closest goal is not null, then we try to go to the goal and push the ball in.
Wandering
To wander, we back up a small amount, then move forward until our front IR sensor tells us that we are near a wall; at that point, we randomly turn 90 degrees to the left or right, and start moving forward again.
Ball-eating
To go to a ball, we first center the ball by calculating the approximate angle the ball is from our robot. We did this by calculating the angle the ball made with the bottom center of our image, and then multiplied this angle by 0.2, where 0.2 was determined by trial and error. This angle guess worked decently for balls that were relatively far away, but did not work well at all for balls that were very close.
After calculating the angle, we would try to rotate to that angle, and move forward until the ball moved outside a center window of our image, at which point we would try to center the ball again. This did not work well, because we tried to use a PID controller to rotate to the desired angle and we did not have accurate rotation, so we would end up oscillating for quite a while while trying to rotate to the correct angle.
When we moved close enough to the ball (determined when the ball was below a center height in the image), we would simply open our gate, move forward, and then close the gate again.
Goal docking
To go to a goal, we used the same approach to move close. Then, we would open our gate, use our plunger to push balls out of the robot, back up, close the gate, move forward to ram the balls, and then open the gate again to flick the balls.
Retrospective view
In hindsight, we probably should not have tried to use our gyro to center balls and goals, but instead should have simply rotated until the desired object was in a good center window.
Mechanical Design
The design of our robot centered around finding the simplest mechanical implementation possible, as well as minimizing the overall size of the robot. In addition to satisfying the official size restriction of a 14 X 14 inch footprint, we felt that the smaller our robot was, the less likely it was to get caught on corners. Hence, we felt that a small rectangular design would be better than a very large circular design. Additionally, a smaller robot would be lighter, and thus able to move faster and more easily.
Ball Acquisition
The immediate consensus was that the easiest way to pick up balls would be to drive over them and store them underneath the robot. Thus, our robot is a bottomless box with an opening in front. To keep the balls from rolling out, we installed a front gate that can be lifted and lowered by a servo. To pick up balls, the robot lifts the gate, drives over the ball, and then lowers the gate.
Scoring Goals
We considered several different methods for scoring a goal:
1) Lifting the gate and discharging the balls using an internal plunger;
2) Lifting the gate, backing up, lowering the gate and ramming into the balls;
3) Lifting the gate, backing up, and flicking the balls with the gate.
The primary idea was to use the plunger. However, because it is the most mechanically complicated, we planned to use the other options as a backup software implementation. In the end, we used all three of these ideas. Our final scoring routine involved lifting the gate, pushing the balls out with the plunger, backing up, then ramming into the balls and flicking them with the gate.
Frame Construction
The main component of the frame is a single pieces of 1/8 inch 50/51 aluminum bent into a square U-shape. Before bending, we had two slots milled into the metal to make tracks for the plunger to slide in. We also drilled all the holes necessary for the motor brackets, brackets for our front casters, and corner braces used to hold the top of the box to the frame.
We considered wood, aluminum, and polycarbonate as materials for our frame. Wood we discarded because it would require that the U-shape be made from at least three pieces of wood joined together. We felt that this would be less strong than something made from a single piece. Additionally, in order for the frame to be rigid enough to not bend or buckle under the weight of our battery, computer, and additional hardware, the wood would have to be fairly thick and thus rather heavy. Polycarbonate could be bent into a U, but this is only possible for thin pieces. While this means our frame would be very light, the plastic is also not rigid enough to hold the U-shape solidly under pressure. We decided to use aluminum because it is rigid enough at bendable thicknesses. Additionally, because thin aluminum was adequately rigid, the frame would not be nearly has heavy as it would be were wood to be used.
The top of the box on which we mounted all of our hardware was made out of 1/8 inch plywood. It bowed slightly in the middle, but we felt this was acceptable for the purpose it was serving.
Our front gate was bent out of two wire hanger s held in rigid positions by threading their ends in and out of holes on the servo horns. One horn was actually mounted to the servo that drove the gate, while the other was held up by a bent screw going through the center hole, which allowed it to rotate freely.
Plunger Construction
The main feature of the plunger was a servo-driven wheel that drove its mounting forwards and backwards on the bottom of our hardware platform, thus providing the propulsion for the plunger. The body of the plunger consisted of two parallel wooden dowels. It turned out that Singer style 66 sewing maching bobbins are the perfect size for sliding along 1/8 inch aluminim. Hence, the ends of the dowels are glued into the center hole of these bobbins. A platform for the servo was made from a small piece of plywood glued between the dowels. This platform was placed at a height so the wheel would have contact with the bottom of our hardware platform.
We added a thin piece of polycarbonate to the hardware platform to improve the contact between it and the wheel, and sandpaper was glued to both the platform along the wheel's path as well as the wheel to increase the friction.
Additionally, the slots milled into the aluminum were lined with plastic cable ties to make a smoother track for the bobbins. The bobbins themselves had a small amount of thread wound onto them to create a better gliding surface. They simply sat in the tracks and glided, they were not intended to roll or turn while the plunger was moving.
Image
Overview
Our team ended up processing images at the rate of around 60-80 milliseconds per frame. To achieve this speed, we first subsampled images, examining only every six pixels, starting at the bottom left of the image and moving to the right and then up.
Ball- and goal-finding
Whenever we encountered a red or yellow pixel, we extended lines to try to find a bounding box for what was likely a ball or goal, ending the line as soon as 5 non-red or non-yellow pixels were encountered. For balls, lines were extended down and then to the left or right, to try to find the bottom center of the ball; then extended up to find the top center of the ball; using the bottom and top centers, we guessed the exact center of the ball; and from this exact center, we finally extended left and right to fully find a bounding box for the ball. A similar process was used to find bounding boxes for goals.
Notice that we only classified red and yellow pixels as "interesting". This is because we ignored barcodes, and we put the camera at the blue line's height and tilted it in such a way that nothing above the blue line was ever seen.
RGB space
To classify a pixel's color, we did not convert our images to HSV space, as was mentioned in the image tutorial, since the conversion took too much time. Instead, we used a k-nearest neighbor algorithm on around 150 images that we collected and annotated, in order to split pixels into red, yellow, or neither.
In more detail, we took 150 pictures from the first mock contest, and cut out all the balls and goals using Photoshop. For each RGB value that appeared in a ball or goal, we added all RGB values within 20 of its red, green, or blue values to our data, and counted the number of times an RGB value appeared as a ball or goal. Then to classify an RGB value as red or yellow or neither, if the maximum of its red and yellow counts was larger than 50, we called it red or yellow depending on which color gave the maximum, and otherwise, if the maximum was lower than 50, it was classified as neither.
Motion
The motion of the robot was based on two basic functions : driving straight and rotating, both of which make heavy use of the gyroscope.
For extremely short distances (driving for time periods of about 500 milliseconds), we found that simply setting both motors to drive forwards or backwards gave good results. For longer distances, we employed feedback using readings from the gyro. To keep the difference between the original gyro reading and the current gyro reading within an acceptable range, we made slight decreases in the individual motor speeds as needed.
We planned to rotate using PID feedback from the gyro. However, after experimentation, we found that nonzero constants for I and D actually caused our performance to deteriorate, so we ended up only using proportional feedback.
Multithreading
When we first started coding, we had everything running in just one thread. This meant that all our actions, such as image analyzing, rotating with feedback, and the overall control logic happened serially. It was impossible for us to get feedback while driving straight or rotating while simultaneously getting intelligence from the camera. We had to move a little. Stop. Look forward and analyze what we saw. Stop. Move a bit, and so on.
In the revised code architecture, we had four threads running:
- ShararaTimer: This thread simply kept time and ensured the robot's code terminated in 4:53, a little early just in case.
- MotionThreader: Here ran all the drive code. We had methods to drive and rotate the robot using feedback that didn't return until they finished managing the robot or timed out.
- ImageThreader: Manager of picture taking and image analyzing. It constantly took pictures and found exciting features in them as fast as possible. At any time, the control thread could query this thread for most recent image analysis results available.
- Checkpoint3: The control thread and home of the main. It made decisions based on the results it obtained from ImageThreader and ordered MotionThreader around accordingly. For example, Checkpoint3 had to piece together the basic actions such as "drive straight" or "rotate" in MotionThreader to make the robot hone in on a ball found by ImageThreader.
After multithreading our application, we found that the robot ran much smoother. It really helped that we could gather camera intelligence while driving with feedback and make decisions as we went without having to stop and think. However, since we started with a single-threaded application, we had a little trouble in the conversion. As a result, we sometimes had difficulty managing all the threads and ensuring that they stopped their actions properly when told or timed out as desired. In the future, it would very advantageous to design the code for a multithreaded application from the start to avoid synchronization and consistency issues. We fixed our issues with a series of patches, which was pretty sloppy and dangerous from a coding perspective.
Commentary
Should've
We ended up not using a quad-phase encoder or doing any sort of odometry. On reflection, it would've helped us maintain a notion of how far we'd travelled and use that to determine roughly where we'd been. With one, we might've been able to go around an obstacle, losing sight of the ball and then being able to relocate it again.
Could've
We devised a clean and well-structured FSM code but we gave up using it in the end. There were various reasons but the prominent one was over-abstraction, making adding new features too elaborate. We could have revised our initial design to come up with a more practical model and change our final code to adhere to the model.
Our goal docking code could also be improved. The lack of distance measurement (i.e. encoder) made it hard for us to improve it.
We wanted to use 5 IRs for better obstacle avoidance but ultimately, we failed. Our initial plan only included 3 IRs but it proved to be inadequate for wall avoidance, taking into account that we didn't use bump sensors. However, the plan with 5 IRs was devised too late for us to finish the code so we ventured to change the direction of the IRs and during contest day, it didn't work so well as we had hoped.
Mapping was also part of our initial plan. We has gone as far as working out the math of Kalman filter and writing code for going in graph. However, at last, the delay and later giving up of quad-phase encoder forced us to abandon this part completely.
