Skip to content

Instantly share code, notes, and snippets.

@skeeto
Last active September 23, 2024 02:16
Show Gist options
  • Save skeeto/da7b2ac95730aa767c8faf8ec309815c to your computer and use it in GitHub Desktop.
Save skeeto/da7b2ac95730aa767c8faf8ec309815c to your computer and use it in GitHub Desktop.
AI driving simulation
@skeeto
Copy link
Author

skeeto commented Nov 24, 2020

@giovannigabriella, the header of the C file has example commands. If you have imagemagick or graphicsmagick installed then you can use the images directly as shown below. You'll also need mpv (or vlc).

# Compile the source file
cc -Ofast aidrivers.c -lm

# View the small map
convert map.png ppm:- | ./a.out | mpv --no-correct-pts --fps=60 -

# Record a 1 minute video of the small map
convert map.png ppm:- | ./a.out | x264 --fps=60 --frames 3600 --output map.mp4 /dev/stdin

# View the large map with suggested settings (syntax requires bash)
convert loop.png ppm:- | \
    ./a.out -n512 -a2 -v <(convert loopoverlay.png ppm:-) | \
    mpv --no-correct-pts --fps=60 --fs -

# To view the available options
./a.out -h

If you're on Windows and need a compiler, check out my w64devkit. You'll still need to convert the PNGs in this repository to Netpbm PPM images.

@skeeto
Copy link
Author

skeeto commented Nov 24, 2020

@quantuumsnot, thanks! Unless I'm misunderstanding you, the inputs are not normalized. The drive function calls sense at line 232, which does a raycast along a vector and reports the distance. These raw pixel distances are plugged into the driving equation just below that call. So in that way, any particular pair of driving parameters are tuned to the scale of the map.

@warmwaffles
Copy link

warmwaffles commented Nov 24, 2020

@skeeto this is amazing. Collision avoidance of other cars would be interesting as well.

@skeeto
Copy link
Author

skeeto commented Nov 24, 2020

@warmwaffles, thanks! That would be an interesting next step for this project.

@Nioub
Copy link

Nioub commented Nov 26, 2020

Cheers @skeeto, nice code as usual!

I was able to generate a video via ffmpeg instead of x264:

./a.out <map.ppm | ffmpeg -y -r 60 -i - -r 60 -t 60 -c:v libx264 -pix_fmt yuv420p out.mp4

-y to overwrite the output without warning
-r 60 to set input framerate at 60 frame/second
-i - to read input from stdin
-r 60 to set output framerate at 60 frame/second
-t 60 to stop writing the output after 60 seconds (syntax for duration: https://ffmpeg.org/ffmpeg-utils.html#time-duration-syntax )
-c:v libx264 to encode with x264
-pix_fmt yuv420p to specify the output pixel format

@skeeto
Copy link
Author

skeeto commented Nov 27, 2020

Thanks for the detailed ffmpeg example, @Nioub!

@Kaligule
Copy link

I am currently trying to reimplement this in rust for practice. Are you aware that the loop.png is not fully black and white? It has 2 (neighbouring) pixels in other colors:

Unexpected colored pixel at 157, 257: Rgba([0, 255, 0, 255])
Unexpected colored pixel at 158, 257: Rgba([0, 0, 255, 255])

@skeeto
Copy link
Author

skeeto commented Nov 29, 2020

@Kaligule, yup, that's the starting position (green) and starting direction (blue). See "input image format" in the source file header.

@Kaligule
Copy link

Ahh, I didn't read that part. Thanks.

@hashemi
Copy link

hashemi commented Feb 3, 2021

@skeeto thanks for this great little project! I've been trying to study it by slowly porting it to Swift and I'm stuck with the innermost loop in draw_vehicles:

static void
draw_vehicles(struct ppm *f, struct map *m, struct vehicle *v, int n)
{
    int s = f->w / m->w;
    for (...) {
        for (...) {
            for (int j = -s/2; j < s/2; j++) {
                ...
            }
        }
    }
}

The loop variable j is initialized to -s/2 and the loop condition is j < s/2. But isn't s is going to be 1 in most cases? Since the map is initialized from the ppm file and it's width and height are set to be the same. In that case -s/2 and s/2 are both zero and the loop doesn't do any iterations.

@Kaligule
Copy link

Kaligule commented Feb 4, 2021

@hashemi I am not the author, but I think I know what is going on here.

the dimensions are not the same

First thing to notice is that the loop and loopoverlay are do not have the same dimensions at all:

loopoverlay.png: 1920px × 1080px
loop.png: 480px × 270px

So s (probably short for scale) is actually 4 in this example.

magic numbers

In the two inner loops there are hidden two magic numbers:

        for (int d = -s*2; d < s*2; d++) {
            for (int j = -s/2; j < s/2; j++) {
                ...
            }
        }

is equivalent to :

        int vehicle_length = 4 * s;
        int vehicle_width =   1 * s;
        for (int d = -1/2 * vehicle_length; d < +1/2 * vehicle_length; d++) {
            for (int j = -1/2 * vehicle_width; j < -1/2 * vehicle_width; j++) {
                ...
            }
        }

So actually these numbers in the loop determine how the vehicle is drawn. And as the vehicles in the blogpost are really thin, you are right in that this loop will not be that big.

Does that help you?

@hashemi
Copy link

hashemi commented Feb 5, 2021

Thanks @Kaligule! I'd figured that s likely means scale but didn't realize that the map and final PPM had different dimensions, that clarifies why the code works on the given example.

I guess the code only works correctly if s (scale between map and final ppm) was 2 or more, otherwise vehicles won't get drawn.

Have you considered publishing your rust implementation? I'd love to read it to see how you decided to organize your code. I'm trying to stay as close to C at first but have plans for reorganizing it to be more idiomatic Swift.

@Kaligule
Copy link

Kaligule commented Feb 5, 2021

@hashemi I am a bit hesitant to publish it, because it is a learning project - I am still new to rust and this code is not much prettier then the original (though I gave my best when naming the variables). But I guess there is no shame in learning and not being perfekt. I'd be glad to hear feedback.

Here is the repo.

@Kaligule
Copy link

Kaligule commented Feb 6, 2021

@hashemi I just added the scaling to my implementation (I had just not used the overlay until now, instead I drew onto the collision map). It was interesting and I definitelly had to iterate over it multiple times.

My bigest realization was that it is not enough to just adjust the position of every car pixel. That would spread out the cars pixels over a bigger area, making it less dense. The scale really has to get into the car length and car width before you iteratate over them.

Your problem persists, of course. I decided to make the cars thicker in my simulation, so this is not as big of an issue anymore.

@hashemi
Copy link

hashemi commented Feb 8, 2021

I realized if you don't specify an overlay image file, the code will just create a blank PPM file that is 12 times larger than the map.

@hashemi
Copy link

hashemi commented Feb 9, 2021

In case someone is interested, I just made my Swift port public.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment