{"id":20346659,"url":"https://github.com/arthurfdlr/race-track-generator","last_synced_at":"2025-09-23T10:31:29.238Z","repository":{"id":112737016,"uuid":"364454624","full_name":"ArthurFDLR/race-track-generator","owner":"ArthurFDLR","description":"🏁 How to generate race tracks efficiently using Fourier descriptors and GANs.","archived":false,"fork":false,"pushed_at":"2021-05-29T04:20:52.000Z","size":51668,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-11-14T22:14:31.032Z","etag":null,"topics":["fourier-transform","gan","generative-adversarial-network","race-track"],"latest_commit_sha":null,"homepage":"","language":"Jupyter Notebook","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ArthurFDLR.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-05-05T03:50:02.000Z","updated_at":"2023-11-30T18:53:49.000Z","dependencies_parsed_at":null,"dependency_job_id":"c8c9512a-ee3f-48c4-8161-a3100b05fbcf","html_url":"https://github.com/ArthurFDLR/race-track-generator","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArthurFDLR%2Frace-track-generator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArthurFDLR%2Frace-track-generator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArthurFDLR%2Frace-track-generator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArthurFDLR%2Frace-track-generator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ArthurFDLR","download_url":"https://codeload.github.com/ArthurFDLR/race-track-generator/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":233967970,"owners_count":18758725,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["fourier-transform","gan","generative-adversarial-network","race-track"],"created_at":"2024-11-14T22:13:45.988Z","updated_at":"2025-09-23T10:31:21.924Z","avatar_url":"https://github.com/ArthurFDLR.png","language":"Jupyter Notebook","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align = \"center\"\u003e Race Track GAN \u003c/h1\u003e\n\n[![MIT License][license-shield]][license-url]\n[![LinkedIn][linkedin-shield]][linkedin-url]\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"./.github/banner.png\" alt=\"Generated track samples\" width=\"80%\"\u003e\n\u003c/p\u003e\n\nThis project aims at creating the most efficient race track generation model. To do so, we will leverage the frequency decomposition of race track curves to simplify their representations. However, available race track datasets are extremely limited. Only a few race tracks maps are available. Given the large number of samples needed to train a GAN, we will create our dataset. We will extract tracks from [Matt Dunlop's poster](https://www.redbubble.com/i/poster/Race-Tracks-to-Scale-Plain-Layouts-by-SirDunny/11752820.LVTDI) which contains 95 of the most iconic race tracks on to one image. Note that the generated dataset does not aim at being remarkably accurate; the rough shape of the tracks is enough to train the dataset, especially that they will be smoothed out through a low-pass filter.\n\n- [Dataset creation](#dataset-creation)\n  - [Description of the boundary following algorithm](#description-of-the-boundary-following-algorithm)\n  - [Fourier Descriptors](#fourier-descriptors)\n- [Custom GAN](#custom-gan)\n  - [Architecture](#architecture)\n  - [Training](#training)\n\n## Dataset creation\n\nThere are three main steps involved in the creation of the dataset from the source image.\n - Find the location of each track in the image.\n - Extract the shape of each track using a boundary following algorithm.\n - Compute the Fourier descriptors of each track.\n\nIt can be reproduced using [this Python app](./src/get-tracks.py). The [final dataset](./data/tracks_fourier.json) contains arrays of coordinates and the associated 256 first Fourier descriptors of 77 tracks as a JSON file.\n\n### Description of the boundary following algorithm\n\nThe shape must present clean and sharp edges. We can remove the noises and sharpen the image using a combination of a $9 \\times 9$ averaging filter and Otsu's algorithm. These steps can be skipped in our case since the image is already well suited for the boundary following algorithm:\n\n- Let the starting point, $b_0$, be the uppermost-leftmost point in the image. Store its coordinates. Denote by $c_0$ the west neighbor of $b_0$. Examine the 8-neighbors of $b_0$ starting at $c_0$ and proceeding in a clockwise direction. Let $b_1$ denote the first neighbor encountered whose value is 1, and let $c_1$ be the (background) point immediately preceding $b_1$ in the sequence.\n- Initiate a couple of variables, $b=b_1$ and $c=c_1$.\n- Let the N-neighbors (N being 4 or 8) of $b$, starting at $c$ and proceeding in a clockwise direction, be denoted by $\\{n_i, \\forall i \\in [\\![0, N-1]\\!]\\}$. Find the first $n_k$ labeled 1.\n- Let $b=n_k$ and $c=n_{k-1}$.\n- Repeat Steps 3 and 4 until $b=b_0$ and the next boundary point found is The sequence of points found when the algorithm stops constitutes the set of ordered boundary points\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"./.github/bound_follow.png\" alt=\"llustration of the first few steps in the boundary-following algorithm\" width=\"90%\"\u003e\n\u003c/p\u003e\n\n\u003cdetails\u003e\n    \u003csummary\u003eSee Python implementation\u003c/summary\u003e\n\n```python\ndef boundary_following(border, stay_out=True, allow_diag=True):\n    \n    x_n = [0, -1, -1, -1,  0,  1, 1, 1] if allow_diag else [0, -1,  0, 1]\n    y_n = [1,  1,  0, -1, -1, -1, 0, 1] if allow_diag else [1,  0, -1, 0]\n    nbr_neighbore = len(x_n)\n    angle = nbr_neighbore//2\n    \n    def get_neighbor(p,a):\n        x, y = p[0] + x_n[a], p[1] + y_n[a]\n        if (0 \u003c= x \u003c border.shape[0]) and (0 \u003c= y \u003c border.shape[1]):\n            return border[x, y]\n        else: return None\n\n    b = find_upper_left(border)\n    b_init = False\n    coord_border = [b]\n    chain_code = []\n\n    while True:\n        # Revolve around b until hit border\n        while not get_neighbor(b, angle):\n            angle = (angle - 1) if angle else (nbr_neighbore - 1)\n        # Prefer direct neighbore\n        if (not stay_out) and allow_diag and (angle%2 == 1) \\\n            and get_neighbor(b, (angle - 1) if angle else 7):\n            angle = (angle - 1) if angle else (nbr_neighbore - 1)\n        # Update b \u003c- n(k)\n        b = (b[0] + x_n[angle], b[1] + y_n[angle])\n        # End condition: two successive boundary pixels already visited\n        if b_init:\n            if b == coord_border[1]: break\n            else: b_init = False\n        if b == coord_border[0]: b_init = True\n        # Store new border pixel\n        chain_code.append(angle)\n        coord_border.append(b)\n        # Reset angle, c \u003c- n(k−1)\n        angle = (angle+angle%2+2)%8 if allow_diag else (angle+1)%4\n    return np.array(coord_border), chain_code\n```\n\u003c/details\u003e\n\n### Fourier Descriptors\n\nThe Fourier Transform (FT) has proven to be extremely useful in computer vision and signal processing in general. Once again, we can take advantage of the frequency representation of the shape's boundary series. The frequency representation of the tracks allow to reduce the quantity of data used to describe a boundary with virtually no resolution loss. In addition, such representation ensure the each generated track form a closing loop.\n\nAt first sight, one can think about using a 2D FT to generate the frequency description of a boundary due to its visual nature. However, tracks really are one-dimensional. Each coordinate pair can be treated as a complex number. Consider a boundary $\\left\\{\\left(x_i, y_i\\right), \\forall i \\in [\\![0, K-1]\\!] \\right\\}$,\n\n$$ s(k) = y(k) + j \\cdot x(k) $$\n\nThe classical one-dimensional Fourier Transform can be applied to this complex representation to obtain the Fourier descriptors $a(u)$ of the boundary. $\\forall u \\in [\\![0, K-1]\\!]$\n\n$$ a(u) = \\sum_{k=0}^{K-1} s(k) \\cdot e^{-j 2 \\pi \\frac{uk}{K}} $$\n\nThen, we can reconstruct the border using only a fraction of the available Fourier descriptors. Consider $P \u003c K$ descriptors, $\\forall p \\in [\\![0, P-1]\\!]$\n\n$$ \\widehat{s}(p) = \\frac{1}{K} \\sum_{u=0}^{P-1} a(u) \\cdot e^{j 2 \\pi \\frac{up}{K}} $$\n\nHere is some examples of race tracks reconstructed using only 64 Fourier descriptors:\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"./.github/fourier_descp_samples.png\" alt=\"Animated training history\" width=\"100%\"\u003e\n\u003c/p\u003e\n\n## Custom GAN\n\n### Architecture\n\nGAN architecture generally implies convolutional layers. However, here we are not dealing with images but an array of complex numbers, our Fourier descriptors, which can then be used to draw a race track. Thus, the discriminator and the generator of our GAN model will be simple, fully connected networks. In addition, for the sack of simplicity and efficiency, we can set the order of the Fourier representation fairly low; let's say 10. Note that we have to flatten the complex representation so that the actual array length is 20. Such a low number ensures that we have reasonably good results even with our minimal dataset of 77 samples. We can use elementary models:\n\n\n\u003ctable\u003e\n\u003ctr\u003e\u003cth\u003e Generator (6 272 parameters) \u003c/th\u003e\u003cth\u003e Discriminator (5 409 parameters) \u003c/th\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd style=\"min-width: 450px; vertical-align:top\"\u003e\n\n| Layer      | Output length | Number of Parameters |\n| ---------- | :-----------: | :------------------: |\n| Input      | 8             | 0                    |\n| Dense (Leaky ReLu)       | 16            | 128                  |\n| Batch Normalization      | 16            | 64                  |\n| Dense (Leaky ReLu)       | 16            | 256                  |\n| Batch Normalization      | 16            | 64                  |\n| Dense (Leaky ReLu)       | 32            | 512                  |\n| Batch Normalization      | 32            | 128                  |\n| Dense (Leaky ReLu)       | 64            | 2048                  |\n| Batch Normalization      | 64            | 256                  |\n| Dense (Leaky ReLu)       | 32            | 2048                  |\n| Batch Normalization      | 32            | 128                  |\n| Dense (Leaky ReLu)       | 20            | 640                  |\n\n\u003c/td\u003e\u003ctd style=\"vertical-align:top\"\u003e\n\n| Layer      | Output length | Number of Parameters |\n| ---------- | :-----------: | :------------------: |\n| Input      | 20            | 0                    |\n| Dense      | 32            | 672                  |\n| Dense      | 64            | 2112                 |\n| Dense      | 32            | 2080                 |\n| Dense      | 16            | 528                  |\n| Dense      | 1             | 17                   |\n\n\u003c/td\u003e\u003c/tr\u003e \u003c/table\u003e\n\nOnce trained, the generator can be easily embedded in a web application using TensorFlow.js given its low complexity. You can download a trained generator model [here](./track_generator.h5). This model has generated the illustration samples at the top of this page.\n\n### Training\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"./.github/training_history_loss.png\" alt=\"Training history loss\" width=\"80%\"\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"./.github/training_history.gif\" alt=\"Animated training history\" width=\"80%\"\u003e\n\u003c/p\u003e\n\n\n\n\u003c!-- MARKDOWN LINKS \u0026 IMAGES --\u003e\n[license-shield]: https://img.shields.io/github/license/ArthurFDLR/race-track-generator?style=for-the-badge\n[license-url]: https://github.com/ArthurFDLR/race-track-generator/blob/master/LICENSE\n[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge\u0026logo=linkedin\u0026colorB=555\n[linkedin-url]: https://linkedin.com/in/arthurfdlr/","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farthurfdlr%2Frace-track-generator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farthurfdlr%2Frace-track-generator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farthurfdlr%2Frace-track-generator/lists"}