From 3d091ff4c74a5e27dfdf741b701771ae20f51b2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Vanden=20Bemden?= Date: Mon, 27 Jan 2020 15:18:27 +0100 Subject: [PATCH] Release v2.2 pushed --- HOWTO.md | 378 +++++++++++++++++++++++++++ README.md | 33 +++ TOGOFURTHER.md | 73 ++++++ compare.py | 10 +- detect.py | 8 +- detection/detection.py | 112 +++++--- detection/detection_setup.py | 34 ++- detection/food_colors.py | 19 +- detection/food_limits.py | 5 +- detection/limits_maker.py | 11 + detection/refine.py | 48 +++- detection/utils.py | 38 ++- play.py | 6 +- requirements.txt | 5 + setup.py | 4 +- simulation/board.py | 94 ++++++- simulation/interface.py | 29 +- simulation/logic/advanced_scouter.py | 42 ++- simulation/logic/blob_manager.py | 60 ++++- simulation/logic/dumb_scouter.py | 16 +- simulation/logic/fsm_ant.py | 44 +++- simulation/logic/gatherer.py | 64 ++++- simulation/logic/sensing_scouter.py | 56 +++- simulation/player.py | 41 ++- 24 files changed, 1086 insertions(+), 144 deletions(-) create mode 100644 HOWTO.md create mode 100644 TOGOFURTHER.md create mode 100644 requirements.txt diff --git a/HOWTO.md b/HOWTO.md new file mode 100644 index 0000000..9a7f7fd --- /dev/null +++ b/HOWTO.md @@ -0,0 +1,378 @@ +# How to use +Le dossier "blob-simulation" contient quatre scripts Python qui sont autant de points d'entrées à différents moments du processus. + ++ `setup.py` permet de configurer le setup entre la reconnaissance dans l'image et certains paramètres de la simulation. Il n'est à utiliser qu'une fois au moment de la configuration. ++ `detect.py` réalise la reconnaissance d'images, calcule la taille du blob et virtualise celui-ci. Il est possible de raffiner cette détection avec des données json supplémentaires. ++ `play.py` permet de lancer une simulation (automatique ou manuelle) du blob virtualisé ++ `compare.py` utilise deux sauvegardes de simulation pour générer une image représentant les différences entre les deux simulations. + +Dans le dossier "data", un fichier "example.jpg" permet d'effectuer un test allant du premier au dernier script. + +## Setup (setup.py) + +**Commande rapide** : `python setup.py data/example.jpg` + + > python setup.py -h + usage: Setup the config file for blob detection and simulation + [-h] [-s SCALE] [-c CONFIG] INPUT + + positional arguments: + INPUT Uses this input as image for setup + + optional arguments: + -h, --help show this help message and exit + -s SCALE, --scale SCALE + Scales image by this factor (default: x0.25) + -c CONFIG, --config CONFIG + Saves config under this filename (default: + detection/config.json) + +Une fois lancé, le programme affiche un menu avec différentes options. Celles-ci sont à entrer dans la console : + ++ **1** : permet de cliquer sur l’image pour définir les 4 coins du plateau (de préférence en commençant par celui en haut à gauche et en tournant dans le sens horloger). Lorsque les 4 coins ont été définis, appuyez sur ENTER pour valider ou sur un autre bouton pour recommencer. ++ **2** : permet de cliquer sur des échantillons de pixels représentant la couleur de la nourriture. A chaque click, les pixels désormais pris en compte s’affichent en rouge. A éviter : ne pas cliquer sur de la nourriture recouverte par du blob, ce serait alors ce dernier qui serait repéré dans la suite… Dans le cas d’une erreur, il est possible de retirer le dernier pixel ajouté en appuyant sur BACKSPACE. Lorsque suffisamment d’échantillons ont été sélectionnés, appuyez sur ENTER. ++ **3** : dans la console, vous pouvez entrer le ratio d’image à utiliser pour la détection et l’affichage. C’est un rapport « longueur / hauteur ». Il peut être différent de la résolution utilisée pour la virtualisation. ++ **4** : dans la console, vous pouvez entrer la résolution discrète de la hauteur et de la largeur du blob virtuel. ++ **5** : permet de cliquer sur les 4 coins d’un comprimé de nourriture afin d’en définir la plus petite taille possible. Il est donc plus intéressant de prendre un des plus petits comprimés et de tracer un carré plutôt inscrit à l’intérieur du comprimé. ++ **S** : pour sauver et quitter ++ **Q** : pour quitter sans sauver + +*N.B. Lors de l'enregistrement, s'il existait un précédent fichier, celui-ci est archivé dans le dossier "bkp"* + +## Détection (detect.py) + +**Commande rapide** : python detect.py data/example.jpg + + > python detect.py -h + usage: Detect a blob and foods in an image. + [-h] [-s SCALE] [-c CONFIG] + [--save SAVE] [--hide] + [--refine REFINE] + INPUT + + positional arguments: + INPUT Uses this input as image for detection + + optional arguments: + -h, --help show this help message and exit + -s SCALE, --scale SCALE + Scales images by this factor (default: x0.1) + -c CONFIG, --config CONFIG + Loads config from this file (default: detection/config.json) + --save SAVE Pass the directory where saves are stored. (default: save/) + --hide Hide images if parameter is set + --refine REFINE Pass a json file to refine model + +Une fois lancé, le programme réalise tout en une seule fois. Lorsque l’image s’affiche, appuyer sur n’importe quelle touche pour fermer le programme. +L’image affiche : + ++ **En haut à gauche** : l’image d’origine ainsi qu’un trait correspondant au milieu de l’image (utilisé comme démarcation entre le plateau du haut et le plateau du bas) ++ **En haut à droite** : l’image virtualisée du blob (telle qu’elle sera utilisée dans la simulation) ++ **Au milieu à gauche** : les pixels détectés appartenant au blob ++ **Au milieu à droite** : ces mêmes pixels avec les couleurs d’origine ++ **En bas à gauche** : les pixels détectés appartenant à de la nourriture ++ **En bas à droite** : les agrégats de pixels, correspondant chacun à une nourriture. + +Dans l’image virtualisée, les carrés verts correspondent à de la nourriture. Une croix les recouvre lorsque le blob les a découvertes. Les nuances de gris correspondent à la présence de blob (de noir à blanc selon la quantité de blob supposée). + +Dans la console, la quantité de blob sur la totalité, la moitié supérieure et la moitié inférieure du plateau est affichée. Le nombre de nourritures trouvées est également donné. + +## Simulation (play.py) +**Commande rapide** : python play.py save/example-detect.board + + > python play.py -h + usage: play.py [-h] [--height HEIGHT] [--width WIDTH] [-s SCALE] [--save SAVE] + [--computing_ratio COMPUTING_RATIO] [--auto_loops AUTO_LOOPS] + [--display DISPLAY] [--init_foods INIT_FOODS] + [INPUT] + + positional arguments: + INPUT Initialize game from a save. Overwrite height and + width parameters. Pass the board filename as input (.board extension) + + optional arguments: + -h, --help show this help message and exit + --height HEIGHT New game board height resolution if no input is + given(default: 40) + --width WIDTH New game board width resolution if no input is + given(default: 100) + -s SCALE, --scale SCALE + Scales board resolution by this factor (default: x10) + --save SAVE Pass the directory where saves are stored. (default: save/) + --computing_ratio COMPUTING_RATIO + How many times computing loop is done before drawing GUI + --auto_loops AUTO_LOOPS + Set number of loops needed before saving and closing automatically + --display DISPLAY Set to '1' to display as centered window, + '2' to display as centered window with no border, + '3' to display as fullscreen, + '0' to hide display (only if auto_loops is set correctly) + --init_foods INIT_FOODS + Starts the game by initializing a certain quantity of + foods in one of the half-board + +Les couleurs dépendent du fichier "default/interface.json" mais il existe différents types de cases identifiables : + ++ Les cases de nourriture non-découvertes par le blob sont de la couleur "FOOD_COLOR". ++ Les cases explorées par le blob sont soit de la couleur "TOUCHED_COLOR" si le blob s'en est retiré, soit entre "BLOB_LOWER_COLOR" et "BLOB_HIGHER_COLOR" selon la quantité de blob présente. ++ Les cases barrées par une croix sont des cases où de la nourriture est présente et connue du blob. ++ Les cases blanches correspondent aux actuateurs (ou "fourmis") du blob. Ce sont elles qui déposent une nouvelle quantité de blob sur une case. + +En mode automatique, le programme démarre la simulation et se termine à l'issue du nombre de boucles renseignées en sauvegardant l'état du jeu ainsi qu'un fichier de résultats. Si l'utilisateur effectue une interaction avec le dispositif, le mode automatique s'interrompt pour repasser en mode manuel. + +En mode manuel, différentes commandes sont disponibles. + +### Commandes de la simulation + +#### Commandes d'administration + ++ **D** : Passage du mode normal au mode debug et inversement ++ **Flèche du haut / du bas** : Augmente / Diminue l'évaporation du blob sur toutes les cases ++ **ESPACE** : Montre / Cache les actuateurs (ou fourmis) ++ **S** : Sauvegarde l'état actuel du jeu + +#### Commandes modifiées en mode debug + ++ **Click droit** : Ajoute une petite quantité de blob sur cette case + +#### Commandes du joueur + ++ **Click droit** : Ajoute de la nourriture sur cette case (et les adjacentes selon la taille de nourriture enregistrée) ++ **C** : Nettoie le plus vieux des deux demi-plateaux ++ **R** : Dépose aléatoirement de la nourriture sur le demi-plateau le plus récent. ++ **H** : Affiche l'information sur la taille du blob ++ **ESCAPE** : Quitte le jeu, sans sauvegarder + +#### Commandes de contrôle du blob + ++ **P** : Démarre / Arrête la progression du blob ++ **RETOUR** : Effectue un seul pas de progression pour le blob ++ **K** : Diminue le nombre minimum d'actuateurs (fourmis) que doit avoir le blob ++ **A** : Augmente le nombre minimum d'actuateurs (fourmis) que doit avoir le blob + +### Logique du blob (dossier simulation/logic) +Le comportement du blob est configurable de différentes manières grâce au fichier "default/blob.json". + +La classe *BlobManager* prend en charge la gestion complète du blob. Celui-ci se comporte comme une colonie de fourmis, avec un nombre maximum de fourmis à disposition, chacune d'entre elles se déplaçant sur le plateau selon une certaine logique et déposant une certaine quantité de "blob" sur les cases où elles se trouvent. Elles disposent toutes d'un savoir partagé, contenu dans la variable "knowledge". Cette variable contient, entre autres, la logique à adopter et tous les emplacements de nourriture connus. + +Pour calculer la taille maximale de la colonie, différentes propriétés du blob sont utilisées avec certains facteurs d'ajustement : + ++ "Blob Size Factor" ajuste la taille de la colonie selon la quantité de blob présent sur le plateau. ++ "Covering Factor" ajuste la taille de la colonie selon la portion de plateau couverte par le blob (indépendamment de la quantité de blob sur chaque case). ++ "Known Foods Factor" modifie la taille de la colonie selon la quantité d'emplacements de nourriture connus. ++ "Global Factor" multiplie chaque facteur afin de notamment s'adapter à la taille du plateau. + +Quelques autres variables sont encore utilisées dans cette classe : + ++ "Global Decrease" représente la quantité de blob retirée sur chaque case après chaque tour, un tour étant équivalent à un déplacement pour chaque fourmi de la colonie. ++ "Remaining Blob on Food" contrecarre cette décroissance en imposant une limite minimum de blob restant lorsqu'il est sur une case de nourriture ++ "Scouters"->"Min" indique, quant à lui, la taille minimale de la colonie à respecter. + +Chaque fourmi utilise la classe *FSMAnt*, une classe utilisant une machine FSM permettant de faire passer la fourmi d'une logique d'exploration (*Scouting*) à une logique de récolte (*Gathering* ou *Harvesting*). Chaque fourmi dispose d'une réserve de nourriture, qu'elle utilise au fur et à mesure de ses déplacements, proportionnellement à la quantité de blob déposée. Les variables suivantes sont utilisées : + ++ "Harvesting"->"Eat" indique la quantité de nourriture (maximum) que la fourmi mange pour effectuer un déplacement. Elle en consomme moins si elle se déplace sur une case déjà occupée par du blob, proportionnellement à la quantité de blob se trouvant sur la case. ++ "Scouters"->"Drop by eat" indique la quantité de blob déposée par rapport à la quantité de nourriture utilisée. ++ "Harvesting"->"Collect" correspond à la valeur maximale emmagasinée par la fourmi lorsque celle-ci arrive sur une case de nourriture (peu importe la logique dans laquelle elle se trouve) ++ "Harvesting"->"Min" est la valeur minimale à emmagasiner avant de pouvoir sortir d'une logique de récolte vers une logique d'exploration ++ "Harvesting"->"Max" est la valeur maximale qu'une fourmi peut emmagasiner. + +Au départ, une fourmi commence avec un stock minimum de nourriture et se trouve dans une logique d'exploration. Lorsqu'elle se retrouve sans réserve, la fourmi devient affamée et passe dans une logique de récolte jusqu'à avoir recouvré la valeur minimale à emmagasinner. Elle repasse alors dans une logique d'exploration. + +Chaque logique est représentée par une classe : *Gatherer* et *AdvancedScouter*. Elles utilisent trois même types de variables, configurables indépendamment pour chacune des deux logiques : + ++ "Diagonal Moves" autorise ou non des déplacements en diagonal sur le plateau ++ Lorsque "Light Compute" est actif, chaque fourmi ne calcule qu'une fois son trajet jusqu'à son objectif (d'exploration ou d'emplacement de nourriture). Le chemin trouvé est donc utilisé peu importe l'évolution du plateau (notamment d'éventuelles décroissances de blob). Cela permet cependant de diminuer la quantité de calculs effectués à chaque itération. ++ "Sightline" représente l'horizon vu par la fourmi en nombre de cases. La valeur -1 signifie que la fourmi a une vue sur l'ensemble du plateau. Attention cependant, cela ne signifie pas qu'elle connait pour autant l'emplacement de la nourriture en dehors du blob. Seul le fait que la case est inexplorée est utilisé, peu importe que celle-ci contienne de la nourriture ou pas. + +Enfin, deux variables spéciales sont utilisées pour l'exploration : + ++ Par défaut, une fourmi en exploration cherche à aller sur une des cases dans son horizon contenant le moins de blob. Cependant, à chaque déplacement, il existe une probabilité "Global Explore Probability" (entre 0 et 1 donc) de passer vers une recherche où la fourmi se déplace vers la case ayant le moins de blob dans son horizon. Ce n'est donc pas la quantité sur la case qui est minimisée mais bien la quantité vue sur l'entiereté de l'horizon. Pour repasser dans le premier type d'exploration, une probabilité de valeur 1-"Global Explore Probability" est utilisée. ++ Lorsque "Search Locally on Food" est vraie, lorsqu'une exploratrice trouve de la nourriture, elle repasse et reste automatiquement dans le premier type d'exploration. + +Il existe encore deux autres types de logique implémentées mais non-utilisées : *SensingScouter* correspond à une exploratrice avec "Global Explore Probability" valant 0. *DumbScouter* est l'implétation minimale d'une exploratrice, sans connaissance ou logique de récolte. + + +## Comparaison (compare.py) + +**Commande rapide** : python compare.py save/example-detect.board save/example-10_loops.board + + > python compare.py -h + usage: compare.py [-h] [-s SCALE] [-o OUTPUT] FIRST_INPUT SECOND_INPUT + + Compare two board files and express it as an image. + + positional arguments: + FIRST_INPUT first board file + SECOND_INPUT second board file to compare with + + optional arguments: + -h, --help Show this help message and exit + -s SCALE, --scale SCALE + Scales board resolution by this factor (default: x10) + -o OUTPUT, --output OUTPUT + Give a name to save the jpeg file + +Une fois lancé, le programme s'exécute en une fois. Si le paramètre "output" a été fourni, le résultat est sauvegardé sans être affiché. Autrement, une fenêtre pygame est ouverte montrant le résultat de la comparaison. + +**Légende :** + ++ Les nuances de bleu indiquent la présence plus importante de blob dans le premier mais pas le second fichier ++ Les nuances de rouge indiquent, à l'inverse, une présence plus importante de blob dans le second fichier plutôt que dans le premier ++ S'il existe des différences de position dans les nourritures, celles-ci sont affichées en nuances de vert + +## Format des fichiers de configuration +### config.json (NON-modifiable) + + { + "Aspect Ratio": 0.4, + "Discrete Height": 160, + "Discrete Width": 400, + + "Limits": [ + [136, 268], + [5484, 208], + [5452, 3296], + [136, 3308] + ], + "Low Food Color": [150, 185, 198], + "High Food Color": [214, 230, 237], + "Min Food Size": 60 + } + +Fichier généré par le script setup.py. A ne pas modifier à la main, le script est prévu pour encoder les différentes valeurs. + +### "refine.json" (A créer si besoin) + + { + "Width": 100, + "Height": 40, + "Foods": [ + [10, 10], + [18,28], + [23, 8], + [44,23], + [96,22] + ], + "Clean Top": false + } + +Fichier pouvant servir d'argument optionnel au script detect.py. Il doit contenir les quatre labels mentionnés ci-dessus. "Width" et "Height" correspondent à la résolution utilisée pour renseigner la position des "Foods". "Clean Top" est un booléen indiquant si le prochain demi-plateau à nettoyer est celui du haut (True) ou celui du bas (False). + +### default/blob.json (Modifiable) + + { + "Computing": { + "Blob Size Factor": 0.25, + "Covering Factor": 0.75, + "Global Factor": 2.78, + "Known Foods Factor": 0.05 + }, + "Gathering": { + "Diagonal Moves": true, + "Light Compute": true, + "Sightline": -1 + }, + "Global Decrease": 0.1, + "Harvesting": { + "Collect": 10, + "Eat": 5, + "Max": 30, + "Min": 30 + }, + "Remaining Blob on Food": 50, + "Scouters": { + "Drop by eat": 25, + "Min": 2 + }, + "Scouting": { + "Diagonal Moves": true, + "Global Explore Probability": 0.02, + "Light Compute": true, + "Search Locally on Food": true, + "Sightline": 3 + } + } + +Fichier permettant d'initialiser les différentes variables du comportement du blob. A modifier à la main en fonction du comportement souhaité. Il est également enregistré lorsqu'une sauvegarde de la simulation est effectuée. Pour utiliser la logique la plus avancée du blob, tous les labels ci-dessus doivent être présents. + +### default/interface.json (Modifiable) + + { + "FOOD_COLOR": [244, 210, 128], + "TOUCHED_COLOR": [218, 196, 136], + "BLOB_LOWER_COLOR": [206, 182, 86], + "BLOB_HIGHER_COLOR": [162, 106, 59], + "BACKGROUND": [120, 120, 120], + "BOARD_SEPARATOR": [0, 0, 0] + } + +Fichier contenant les différentes couleurs utilisées pour l'affichage de la simulation. Modifiable à la main. + +### default/player.json (Modifiable) + + { + "clean_top": true, + "food_size": 5, + "use_food_circle": true + } + +Fichier enregistrant les variables pour le "joueur". Modifiable à la main. "clean_top" à la même fonction que décrite plus haut. "food_size" correspond à la taille de la nourriture ajoutée par le joueur lorsqu'il clique dans l'interface. "use_food_circle" doit être à vrai pour poser des nourritures circulaires, sinon celles-ci seront en forme de carré. + +### .board file (NON-Modifiable) + + 300 120 + 0,0.0,0.0 0,0.0,0.0 0,100.0,0.0 1,0.0,25.0 ... + ... + 0,0.0,0.0 0,0.0,0.0 0,0.0,0.0 0,0.0,0.0 ... + +Fichier enregistré lorsqu'une sauvegarde de simulation est réalisée. A ne pas modifier à la main. Il est accompagné d'un fichier jpeg, d'un fichier player.json et d'un fichier blob.json. Il peut être accompagné d'un fichier results.json si le mode automatique était activé. + +La première ligne indique la résolution (largeur hauteur) utilisée par la simulation. Le reste du fichier est utilisé pour représenté l'état du plateau au moment de la sauvegarde. Chaque groupe de trois valeurs est séparé des autres par un espace et les valeurs en elles-mêmes sont séparées par des virgules. + +La première valeur indique si le blob a déjà exploré cette case ou non. La seconde valeur indique la quantité de nourriture présente sur cette case (valeur maximale hardcodée à 100) La troisième valeur indique la quantité de blob présent sur cette case (valeur hardcodée entre 0 et 255) + +### .results.json + + { + "Covering": { + "Bottom": 34.56, + "Top": 10.09, + "Total": 22.33 + }, + "From": "save/example-detect", + "Init_foods": [ + [183, 56], + ], + "Loops": 10, + "To": "save/output" + } + +Fichier généré après une simulation en mode automatique. + ++ "Covering" donne les pourcentages sur les deux demi-plateaux et le plateau complet que le blob a couvert (ou exploré). ++ "From" indique le fichier avec lequel la simulation a été générée. ++ "Init_foods" est présent s'il y a eu un dépôt aléatoire de nourriture au début de la simulation. Il contient alors les positions de celles-ci, dans la résolution de la simulation (autrement dit, la première ligne du fichier board). La taille utilisée est celle enregistrée dans le fichier player.json. ++ "Loops" indique le nombre de boucles effectuées. ++ "To" mentionne le nom des fichiers de sortie. + +## License +Copyright (C) 2019 - UMons + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +## Mentions légales +Cette publication a été réalisée dans le cadre du projet Interreg de coopération transfrontalière C2L3PLAY, cofinancé par L’Union Européenne. Avec le soutien du Fonds européen de développement régional / Met steun van het Europees Fonds voor Regionale Ontwikkeling + + \ No newline at end of file diff --git a/README.md b/README.md index 247cdca..6c5669e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,39 @@ # Blob Simulation & Detection Détection d'un blob dans une image ainsi que détection des comprimés de nourriture. Simulation d'un blob dans PyGame à travers un système multi-agents à connaissance partagée. +1. [How to use](HOWTO.md) +2. [Requirements](#requirements) +3. [Pour aller plus loin](TOGOFURTHER.md) +4. [Releases](#releases) +5. [License](#license) +6. [Mentions légales](#mentions-legales) + +## How to use +Vous trouverez la description détaille des différents scripts et des fichiers de configuration [ici](HOWTO.md). + +## Requirements ++ [Python 3.6.8](https://www.python.org/downloads/release/python-368/) - [Documentation](https://docs.python.org/3.6/) ++ [Numpy 1.16.4](https://pypi.org/project/numpy/1.16.4/) - BSD License ++ [Pathfinding 0.0.4](https://pypi.org/project/pathfinding/0.0.4/) - MIT License ++ [PyGame 1.9.6](https://pypi.org/project/pygame/1.9.6/) - LGPL License - [Documentation](https://www.pygame.org/docs/) ++ [Imutils 0.5.2](https://pypi.org/project/imutils/0.5.2/) - MIT License ++ [OpenCV Python 4.1.0.25](https://pypi.org/project/opencv-python/4.1.0.25/) - MIT License - [Documentation](https://docs.opencv.org/4.1.0/) + +## Pour aller plus loin +Vous trouverez quelques articles scientifiques et conseils de lecture en lien avec le développement de ce projet [ici](TOGOFURTHER.md). + +## Releases +## Release 2.2 - *24/06/2019* +### Modifications : ++ Ajout d'un fichier requirements.txt avec les versions des modules utilisées ++ Transformation de l'argument "input" en argument positionnel + +### Scripts : ++ Setup : `python setup.py data/example.jpg` ++ Detection : `python detect.py data/example.jpg` ++ Simulation : `python play.py data/output-examples/example-detect.board -s 3` ++ Compare : `python compare.py data/output-examples/simulation/10_loops/10_loops.board data/output-examples/simulation/100_loops/100_loops.board -s 3` + ## Release 2.1 - *7/06/2019* ### Modifications : diff --git a/TOGOFURTHER.md b/TOGOFURTHER.md new file mode 100644 index 0000000..d79c14e --- /dev/null +++ b/TOGOFURTHER.md @@ -0,0 +1,73 @@ +# Pour aller plus loin +## Physarum polycephalum ou Blob +Beaucoup d'articles scientifiques (et des livres de vulgarisation) existent sur le blob mais ceux-ci se concentrent sur l'analyse de son comportement et sur sa faculté à résoudre certains problèmes. La plupart de ses problèmes sont liés à des représentations sous forme de graphe (TSP Problem, Dijkstra, Voronoï diagram, Delaunay triangluation, ...) Il n'y a donc pas d'articles cherchant à modéliser son comportement. Au niveau détection, il existe le code MatLab fourni par le CNRS. + +**Dussutour**, Audrey (2017).*Tout ce que vous avez toujours voulu savoir sur le blob sans avoir jamais oser le demander.* Des Équateurs, Hors collection, 179 pages. + +**Tsuda**, Soichiro, **Zauner**, Klaus-Peter and **Gunji**, Yukio-Pegio (2006) *Robot Control: From Silicon Circuitry to Cells.* Ijspeert, Auke Jan, Masuzawa, Toshimitsu and Kusumoto, Shinji (eds.) In Biologically Inspired Approaches to Advanced Information Technology, Second International Workshop, BioADIT 2006, Osaka, Japan, January 26-27, 2006, Proceedings. Springer. pp. 20-32. + +**Whiting** JG, **Jones** J, **Bull** L, **Levin** M, **Adamatzky** A.* Towards a Physarum learning chip*. *Sci Rep*. 2016;6:19948. Published 2016 Feb 3. + +**Jones, Jeff, & Andrew Adamatzky.** [Computation of the travelling salesman problem by a shrinking blob](http://www.phychip.eu/wp-content/uploads/2013/03/Computation-of-the-travelling-salesman-problem-by-a-shrinking-blob.pdf) *Natural Computing,* (13), 1, p. 1-16, (2014).

+**Shirakawa**, Tomohiro & **Adamatzky**, Andrew & **Gunji**, Yukio-Pegio & **Miyake**, Yoshihiro. (2009). On Simultaneous Construction of Voronoi Diagram and Delaunay Triangulation by. I. J. Bifurcation and Chaos. 19. 3109-3117. 10.1142/S0218127409024682. + +## Système multi-agents + +En soi, le blob n'est pas un système multi-agents puisqu'il n'est constitué que d'une seule cellule. Néanmoins, la plupart des problèmes qu'il arrive à résoudre sont aujourd'hui plus facilement résolu par des systèmes multi-agents. + +Un type connu de système multi-agents est le comportement d'une colonie de fourmis. On retrouve donc le lien entre blob et fourmis à travers ses différents problèmes. Audrey Dussutour, avant de se concentrer sur le blob, se spécifiait dans le comportement des fourmis également. + +NetLogo est un langage de programmation pour système multi-agents, basé sur du Logo (lui-même basé sur du LISP). L'environnement n'est pas ultra-modulable mais il fournit une série de modèles implémentés et prêts à l'emploi. Certains de ceux-ci (listés ci-dessous) correspondent aux problèmes résolus par le blob ou par des algorithmes ACO (Ants Colony Optimization). + +Il existe également un pseudo-code permettant de simuler le comportement d'un blob à travers une approche multi-agents. + +Wilensky, U. (1999). **NetLogo**. [http://ccl.northwestern.edu/netlogo/](http://ccl.northwestern.edu/netlogo/) Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL.

+ +### Modèles ++ Stonedahl, F. and Wilensky, U. (2008). [**NetLogo Virus on a Network model**](http://ccl.northwestern.edu/netlogo/models/VirusonaNetwork). Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. + ++ Wilensky, U. (2005). [**NetLogo Preferential Attachment model**](http://ccl.northwestern.edu/netlogo/models/PreferentialAttachment). Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. + ++ Stonedahl, F. and Wilensky, U. (2008). [**NetLogo Diffusion on a Directed Network model**](http://ccl.northwestern.edu/netlogo/models/DiffusiononaDirectedNetwork). Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. + ++ Grider, R. and Wilensky, U. (2015). [**NetLogo Paths model**](http://ccl.northwestern.edu/netlogo/models/Paths). Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL ++ Wilensky, U. (1997). [**NetLogo Ant Lines model**](http://ccl.northwestern.edu/netlogo/models/AntLines). Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. + +[Pseudo-code multi-agents pour simuler un blob](http://www.simulace.info/index.php/Multi-agent_systems) dans NetLogo. + +## IA & Optimization Algorithm Problems +Aucune base de données n'étant disponible pour simuler le blob dans le cas précis du projet, il est nécessaire d'en revenir à des comportements prédits de manière algorithmiques. + +Les techniques classiques d'IA utilisées pour résoudre des problèmes par exploration d'arbres de solution sont donc à privilégier. Le blob peut être envisagé comme un joueur devant découvrir au plus vite des ressources dans un environnement inconnu et à optimiser les connexions entre ces ressources. Sur ce second point, il travaille donc comme un algorithme ACO. Pour la partie découverte, il existe également déjà des simulateurs de parcours de fourmis mais ceux trouvés sont difficilement modifiables. + +**Russel**, Stuart Jonathan, **Norvig**, Peter (2009). *Artificial Intelligence : A modern approach*. Pearson, 3rd Edition,  1152 pages. + +**geoyar**(2013). [*Applying Ant Colony Optimization Algorithms to Solve the Traveling Salesman Problem*](https://www.codeproject.com/articles/644067/applying-ant-colony-optimization-algorithms-to-sol). Code Project. + +**Kohout** Peter (2006). [*Genetic and Ant Colony Optimization Algorithms*](https://www.codeproject.com/Articles/5436/Genetic-and-Ant-Colony-Optimization-Algorithms). Code Project. + +**Lichtenberg** Malte, **Tittmann** Lucas (2012). [Programmation Project - Ant Simulation](https://github.com/Andarin/Ant-Colony-Simulation-Python). Andarin, Github Code (Python 2, Cython). + +**Akavall** (2019). [Ant Colony Optimization Algorithm using Python](https://github.com/Akavall/AntColonyOptimization). Akavall, Github Code (Python 3). + +## License +Copyright (C) 2019 - UMons + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +## Mentions légales +Cette publication a été réalisée dans le cadre du projet Interreg de coopération transfrontalière C2L3PLAY, cofinancé par L’Union Européenne. Avec le soutien du Fonds européen de développement régional / Met steun van het Europees Fonds voor Regionale Ontwikkeling + + \ No newline at end of file diff --git a/compare.py b/compare.py index 677a9fa..11f0a61 100644 --- a/compare.py +++ b/compare.py @@ -21,19 +21,19 @@ def main(): - ap = argparse.ArgumentParser() - ap.add_argument("--first", required=True, help="first board file") - ap.add_argument("--second", required=True, help="second board file") + ap = argparse.ArgumentParser(description="Compare two board files and express it as an image.") + ap.add_argument("input_1", metavar= "FIRST_INPUT", help="first board file") + ap.add_argument("input_2", metavar= "SECOND_INPUT", help="second board file to compare with") ap.add_argument("-s", "--scale", type=float, default=10, help="Scales board resolution by this factor (default: x10)") ap.add_argument("-o", "--output", type=str, help="Give a name to save the jpeg file") args = ap.parse_args() board_1 = board.Board(0, 0) - board_1.load(args.first) + board_1.load(args.input_1) board_2 = board.Board(0, 0) - board_2.load(args.second) + board_2.load(args.input_2) board_comp = board_1.compare(board_2) diff --git a/detect.py b/detect.py index 75b5eb0..1804c9f 100644 --- a/detect.py +++ b/detect.py @@ -22,8 +22,8 @@ def main(): - ap = argparse.ArgumentParser() - ap.add_argument("-i", "--input", required=True, help="Uses this input as image for detection") + ap = argparse.ArgumentParser("Detect a blob and foods in an image.") + ap.add_argument("input", metavar="INPUT", help="Uses this input as image for detection") ap.add_argument("-s", "--scale", type=float, default=0.10, help="Scales images by this factor (default: x0.1)") ap.add_argument("-c", "--config", type=str, default="detection/config.json", help="Loads config from this file (default: detection/config.json)") @@ -57,7 +57,9 @@ def main(): else: file_path = None - print_results(orig, blob_mask, blob, food_mask, food_img, dsc_img, args.scale, file_path, args.hide) + labels = ["Original", "Discrete", "Blob Mask", "Blob", "Food Mask", "Food Regions"] + images = [orig, img, blob_mask, blob, food_mask, food_img] + print_results(labels, images, args.scale, file_path, args.hide, nbr_width=2) if __name__ == "__main__": diff --git a/detection/detection.py b/detection/detection.py index bd7ff14..ebe523e 100644 --- a/detection/detection.py +++ b/detection/detection.py @@ -15,10 +15,21 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from detection.utils import * +from math import ceil def detect(input_file, config): - + """ + Starts blob detection in the image and returns + - the part of the image used (resized and warp perpective) + - blob mask detected + - blob segmented image + - food mask detected + - the image with food regions + + :param input_file: string filename for jpeg image detection + :param config: dict with config used for detection + """ img = cv2.imread(input_file) height, width, _ = img.shape @@ -64,56 +75,62 @@ def detect(input_file, config): return img, blob_mask, blob, food_mask, food_img -def print_results(orig, blob_mask, blob, food_mask, food, discrete, scale=1.0, filename=None, hide=False): +def print_results(labels, images, scale=1.0, filename=None, hide=False, nbr_width=2): + """ + Aggregate and show images in one window with each label displayed at the top of them. + It can as well save the aggregated image. + Each line contains 'nbr_width' images. + If the number of images doesn't fit the last line, it's padded with a zero image. + + :param labels: each label to use for each image in images. assert len(labels) == len(images) + :param images: each image to print in results window + :param scale: the scale to use for results window with respect to image size + :param filename: if provide, save results window under 'filename'.jpg name + :param hide: if true, the results window isn't shown + :param nbr_width: number of images to fit in line + """ padding = 35 - nbr_width = 2 - nbr_height = 3 font = cv2.FONT_HERSHEY_SIMPLEX fontsize = 0.45 thickness = 1 - scaled_height = int(orig.shape[0]*scale) - scaled_width = int(orig.shape[1]*scale) + scaled_height = int(images[0].shape[0]*scale) + scaled_width = int(images[0].shape[1]*scale) + pad = np.zeros((scaled_height, padding, 3), dtype=np.uint8) + line_pad = np.zeros((padding, (scaled_width + padding) * nbr_width + padding, 3), dtype=np.uint8) + img_pad = np.zeros((scaled_height, scaled_width, 3), dtype=np.uint8) + + print_images = [] + for image in images: + full_color = image if len(image.shape) == 3 else cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) + print_image = cv2.resize(full_color, (scaled_width, scaled_height)) + print_images.append(print_image) + + middle = ((0, int(scaled_height / 2)), (scaled_width, int(scaled_height / 2))) + cv2.line(print_images[0], middle[0], middle[1], (0, 255, 0), thickness=1) + cv2.putText(print_images[0], 'Mid Line', (middle[0][0] + 5, middle[0][1] - 5), + font, fontsize, (0, 255, 0), thickness, cv2.LINE_AA) + + lines = [line_pad] + for j in range(ceil(len(images)/nbr_width)): + concat_line = [pad] + for i in range(nbr_width): + if i + j * nbr_width < len(images): + concat_line.append(print_images[i + j * nbr_width]) + else: + concat_line.append(img_pad) + concat_line.append(pad) - pad = np.zeros((scaled_height, padding, orig.shape[2]), dtype=np.uint8) - line_pad = np.zeros((padding, (scaled_width + padding) * nbr_width + padding, orig.shape[2]), dtype=np.uint8) - print_img = cv2.resize(orig, (scaled_width, scaled_height)) + lines.append(np.concatenate(tuple(concat_line), axis=1)) + lines.append(line_pad) - middle = ((0, int(scaled_height/2)), (scaled_width, int(scaled_height/2))) - cv2.line(print_img, middle[0], middle[1], (0, 255, 0), thickness=1) - cv2.putText(print_img, 'Mid Line', (middle[0][0] + 5, middle[0][1] - 5), - font, fontsize, (0, 255, 0), thickness, cv2.LINE_AA) + aggregate = np.concatenate(tuple(lines)) - print_blob_mask = cv2.resize(cv2.cvtColor(blob_mask, cv2.COLOR_GRAY2BGR), (scaled_width, scaled_height)) - print_blob = cv2.resize(blob, (scaled_width, scaled_height)) - print_food_mask = cv2.resize(cv2.cvtColor(food_mask, cv2.COLOR_GRAY2BGR), (scaled_width, scaled_height)) - print_food = cv2.resize(food, (scaled_width, scaled_height)) - print_discrete = cv2.resize(discrete, (scaled_width, scaled_height)) - - concat_line1 = np.concatenate((pad, print_img, pad, print_discrete, pad), axis=1) - concat_line2 = np.concatenate((pad, print_blob_mask, pad, print_blob, pad), axis=1) - concat_line3 = np.concatenate((pad, print_food_mask, pad, print_food, pad), axis=1) - - aggregate = np.concatenate((line_pad, concat_line1, line_pad, concat_line2, line_pad, concat_line3, line_pad)) - - cv2.putText(aggregate, 'Original:', - (0 * (scaled_width + padding) + padding + 5, 0 * (scaled_height + padding) + padding - 5), - font, fontsize, (255, 255, 255), thickness, cv2.LINE_AA) - cv2.putText(aggregate, 'Discrete:', - (1 * (scaled_width + padding) + padding + 5, 0 * (scaled_height + padding) + padding - 5), - font, fontsize, (255, 255, 255), thickness, cv2.LINE_AA) - cv2.putText(aggregate, 'Blob Mask:', - (0 * (scaled_width + padding) + padding + 5, 1 * (scaled_height + padding) + padding - 5), - font, fontsize, (255, 255, 255), thickness, cv2.LINE_AA) - cv2.putText(aggregate, 'Blob:', - (1 * (scaled_width + padding) + padding + 5, 1 * (scaled_height + padding) + padding - 5), - font, fontsize, (255, 255, 255), thickness, cv2.LINE_AA) - cv2.putText(aggregate, 'Food Mask:', - (0 * (scaled_width + padding) + padding + 5, 2 * (scaled_height + padding) + padding - 5), - font, fontsize, (255, 255, 255), thickness, cv2.LINE_AA) - cv2.putText(aggregate, 'Food Regions:', - (1 * (scaled_width + padding) + padding + 5, 2 * (scaled_height + padding) + padding - 5), - font, fontsize, (255, 255, 255), thickness, cv2.LINE_AA) + for i in range(len(print_images)): + cv2.putText(aggregate, labels[i], + ((i % nbr_width) * (scaled_width + padding) + padding + 5, + int(i/nbr_width) * (scaled_height + padding) + padding - 5), + font, fontsize, (255, 255, 255), thickness, cv2.LINE_AA) if filename is not None: cv2.imwrite(filename + ".jpg", aggregate) @@ -125,6 +142,15 @@ def print_results(orig, blob_mask, blob, food_mask, food, discrete, scale=1.0, f def discretize(blob_img, food_mask, width, height): + """ + Transform blob and foods into a numeric blob and foods with discrete 'width' and 'height' resolution + + :param blob_img: a blob segmented numpy image + :param food_mask: a food mask numpy image + :param width: (int) the discrete width resolution + :param height: (int) the discrete height resolution + :return: the new image (as a saved simulated-like), the blob values in the image and the complete food position list + """ img_height, img_width, _ = blob_img.shape discrete_blob = cv2.resize(blob_img, (width, height), interpolation=cv2.INTER_NEAREST) diff --git a/detection/detection_setup.py b/detection/detection_setup.py index b33fa47..1340bbd 100644 --- a/detection/detection_setup.py +++ b/detection/detection_setup.py @@ -27,6 +27,14 @@ def setup(setup_img, config_filename, scale=1.0, bkp_path="detection/bkp/"): + """ + Display an image and show menu to setup every parameter in config.json file + + :param setup_img: a image filename to use to setup "config.json" file + :param config_filename: the "config.json" file to load from and adapt + :param scale: the scale to use for window with respect to image size + :param bkp_path: path where old 'config_filename' will be saved for back up + """ img = cv2.imread(setup_img) height, width, _ = img.shape @@ -40,6 +48,7 @@ def setup(setup_img, config_filename, scale=1.0, bkp_path="detection/bkp/"): food_limits = FoodLimits(img, scale, window_name) setup_vars = {'Aspect Ratio': 1.0, 'Discrete Height': 100, 'Discrete Width': 100} + # Load previous configurations if path.exists(config_filename): with open(config_filename, "r") as file: config = json.load(file) @@ -70,28 +79,26 @@ def show_menu(): while not done: cv2.waitKey(10) - if state == 0: + if state == 0: # Waiting commands cv2.imshow(window_name, cv2.resize(img, (0, 0), fx=scale, fy=scale)) cv2.setMouseCallback(window_name, null_callback) key = input("Enter command: ") - if key == "q": + if key == "q": # Quit without saving done = True - elif key == "1": + elif key == "1": # Setup board limits state = 1 - # board_setup.clear() board_setup.help() cv2.setMouseCallback(window_name, board_setup.on_mouse) - elif key == "2": + elif key == "2": # Setup food color range state = 2 - # food_color.clear() food_color.help() cv2.setMouseCallback(window_name, food_color.on_mouse) - elif key == "3": + elif key == "3": # Insert aspect ratio value setup_vars['Aspect Ratio'] = -1 while setup_vars['Aspect Ratio'] <= 0: try: @@ -104,7 +111,7 @@ def show_menu(): show_menu() - elif key == "4": + elif key == "4": # Insert discrete width and height values setup_vars['Discrete Width'] = -1 while setup_vars['Discrete Width'] <= 0: try: @@ -125,16 +132,15 @@ def show_menu(): if setup_vars['Discrete Height'] <= 0: print("Insert only round numbers.") - show_menu() - elif key == "5": + elif key == "5": # Setup food size limits state = 3 food_limits.clear() food_limits.help() cv2.setMouseCallback(window_name, food_limits.on_mouse) - elif key == "s": + elif key == "s": # Save configuration ts = datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d_%H.%M.%S-') if path.exists(config_filename): if not path.exists(bkp_path): @@ -151,19 +157,19 @@ def show_menu(): print("Error: Unrecognised Command.") show_menu() - elif state == 1: + elif state == 1: # Adapt image to board limits setup board_setup.draw() if board_setup.done: state = 0 show_menu() - elif state == 2: + elif state == 2: # Adapt image to food color setup food_color.draw() if food_color.done: state = 0 show_menu() - elif state == 3: + elif state == 3: # Adapt image to food limits setup food_limits.draw() if food_limits.done: state = 0 diff --git a/detection/food_colors.py b/detection/food_colors.py index 16e563a..78c851d 100644 --- a/detection/food_colors.py +++ b/detection/food_colors.py @@ -19,7 +19,9 @@ class FoodColors: - + """ + Setup color range for foods by getting min and max values for r,g,b channels + """ def __init__(self, img, scale, window_name): self.colors = [] self.orig = img @@ -29,12 +31,20 @@ def __init__(self, img, scale, window_name): self.done = False def add(self, x, y): + """ + Add the color pixel at the given position in the food range + :param x: x value in the img + :param y: y value in the img + """ x_img = int(x / self.scale) y_img = int(y / self.scale) self.colors.append(self.orig[y_img, x_img]) self.show_selected() def show_selected(self): + """ + Adapt img pixels concerned by the actual range to red + """ if len(self.colors) >= 2: low, high = self.compute() mask = cv2.inRange(self.img, np.array(low, dtype=np.uint8), np.array(high, dtype=np.uint8)) @@ -50,6 +60,10 @@ def draw(self): self.confirm() def compute(self): + """ + Compute color range + :return: first tuple is the lowest values found in colors for rgb channels, second tuple is for highest values + """ low_color = [255, 255, 255] high_color = [0, 0, 0] @@ -85,6 +99,9 @@ def on_mouse(self, event, x, y, flags, param): self.add(x, y) def confirm(self): + """ + Wait input key for clearing last color or ending food color setup + """ key = cv2.waitKey(10) & 0xFF if key == 13: # Enter print("--- Color Setup: " + str(self.compute())) diff --git a/detection/food_limits.py b/detection/food_limits.py index 92045cf..8de2b51 100644 --- a/detection/food_limits.py +++ b/detection/food_limits.py @@ -14,12 +14,15 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import cv2 from detection.limits_maker import LimitsMaker from math import sqrt class FoodLimits(LimitsMaker): + """ + Based on LimitsMaker, modify displayed names and record config as a minimal distance + (and not 4 points as in LimitsMaker) + """ def __init__(self, img, scale, window_name): LimitsMaker.__init__(self, img, scale, window_name, "Food Setup") diff --git a/detection/limits_maker.py b/detection/limits_maker.py index d827c98..2b5ed27 100644 --- a/detection/limits_maker.py +++ b/detection/limits_maker.py @@ -18,6 +18,9 @@ class LimitsMaker: + """ + Setup 4 points to define limits and returns them + """ def __init__(self, img, scale, window_name, name): self.limits = [] @@ -31,6 +34,11 @@ def __init__(self, img, scale, window_name, name): self.name = name def add_limit(self, x, y): + """ + Add the pixel at the given position in the limits + :param x: x value in the img + :param y: y value in the img + """ x_img = int(x / self.scale) y_img = int(y / self.scale) self.limits.append((x_img, y_img)) @@ -72,6 +80,9 @@ def clear(self): self.done = False def confirm(self): + """ + Wait input key for clearing last color or ending food color setup + """ print("--- " + self.name + ": Press enter if you're ok with data or any other key if you want to restart " "setup...") key = cv2.waitKey(0) & 0xFF diff --git a/detection/refine.py b/detection/refine.py index 9661182..0656b8b 100644 --- a/detection/refine.py +++ b/detection/refine.py @@ -20,7 +20,16 @@ def simulate(discrete_img, discrete_blob, discrete_food_list, config, refine=None): - + """ + Adapt discrete variables to simulation files. + + :param discrete_img: + :param discrete_blob: a numpy image with blob quantity for each pixel + :param discrete_food_list: the list of all foods position + :param config: a dict config variables + :param refine: a json file to refine model + :return: board, player instances for simulation and an associated jpeg file + """ height, width = discrete_blob.shape board = Board(width, height) @@ -28,6 +37,7 @@ def simulate(discrete_img, discrete_blob, discrete_food_list, config, refine=Non player.food_size = compute_discrete_food_size(config, player.use_circle) player.clean_top = refine['Clean Top'] if refine is not None else True + # Add food to board for (x, y) in discrete_food_list: if board.is_touched(x, y): # TODO Set up value @@ -35,26 +45,38 @@ def simulate(discrete_img, discrete_blob, discrete_food_list, config, refine=Non else: board.set_food(x, y) + # Add blob to board for x in range(width): for y in range(height): if discrete_blob[y, x] != 0: board.update_blob(x, y, discrete_blob[y, x]) + # Add missing information if refine is not None: - adapt_food(board, player, config, refine) + # TODO Adapt discrete_img to refine information + adapt_food(board, player, refine) return board, player, discrete_img -def adapt_food(board, player, config, refine): - square_size = round(1 / refine["Width"] * config["Discrete Width"]) +def adapt_food(board, player, refine): + """ + Refine food based on position given in refine file + + :param board: the board instance to adapt + :param player: the player variables used to put food + :param refine: a json refine file with Width, Height and Foods position + """ + + square_size = round(1 / refine["Width"] * board.width) adding_food = 0 for food_origin in refine["Foods"]: - discrete_food = (int(food_origin[0] / refine["Width"] * config["Discrete Width"]), - int(food_origin[1] / refine["Height"] * config["Discrete Height"])) + discrete_food = (int(food_origin[0] / refine["Width"] * board.width), + int(food_origin[1] / refine["Height"] * board.height)) + # Search for food in the pixels corresponding to a real given coordinate food_found = False for i in range(square_size): for j in range(square_size): @@ -71,6 +93,13 @@ def adapt_food(board, player, config, refine): def save(filename, board, player, img): + """ + Save a simulation as 'filename' with given instances + :param filename: the filename to use + :param board: a board instance + :param player: a player instance + :param img: a numpy image + """ with open(filename + ".board", 'w') as file: file.write(board.save()) @@ -81,7 +110,12 @@ def save(filename, board, player, img): def compute_discrete_food_size(config, use_circle=False): - + """ + Based on config variables, find the size food should have in simulation + :param config: a dict with config variables + :param use_circle: a boolean set to true if food should be circle rather than square + :return: the food size to use to fit with real size + """ food_size = config["Min Food Size"] limits = config["Limits"] x_min = limits[0][0] diff --git a/detection/utils.py b/detection/utils.py index bdcbb41..524d903 100644 --- a/detection/utils.py +++ b/detection/utils.py @@ -20,11 +20,20 @@ def saturation(img): + """ + :param img: a BGR numpy image + :return: the saturation of the image + """ hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) return hsv[:, :, 1] def mean_image(images): + """ + + :param images: a list of one-channel images + :return: the pixel-by-pixel mean image + """ mean = np.zeros(images[0].shape) for image in images: @@ -35,10 +44,27 @@ def mean_image(images): # If percentage should be linked to a smaller region than whole image, fill img_ratio with factor value def mean_percent_value(img, img_ratio=1.0): + """ + Calculate the mean percent value of an image. + :param img: the image to use + :param img_ratio: a ratio to adapt by if a blank image shouldn't be equal to 100% + :return: the mean value + """ return np.sum(img, dtype=np.int64) / img_ratio / img.size / 255 * 100 def find_food(img, min_food_size, lower_color_boundary, upper_color_boundary, kernel=None): + """ + Detect food in the image based on color range. + + :param img: the image to analyze + :param min_food_size: a minimal restriction to food size region + :param lower_color_boundary: the lower color range value + :param upper_color_boundary: the upper color range value + :param kernel: a structuring element to remove noise, by default an ellipsis of half minimal food size is used + :return: a list of detected foods regions (position and size), the corresponding mask + and an image with corresponding drawn regions + """ img = img.copy() lower = np.array(lower_color_boundary, dtype="uint8") upper = np.array(upper_color_boundary, dtype="uint8") @@ -69,9 +95,17 @@ def find_food(img, min_food_size, lower_color_boundary, upper_color_boundary, ke return foods, mask, img -# Set area_ratio to 0 if you want to have the maximum possible blobs. -# Otherwise adding of blobs is break when the next blob to add is smaller than the first blob times the area_ratio def find_blob(sat_img, max_blob=1, area_ratio=0.8, kernel=None): + """ + Detect blob in the saturation image based on OTSU Thresholding (background-foreground separation) + + :param sat_img: the image to analyze + :param max_blob: the maximum number of blob regions that has to be detected + :param area_ratio: a size ratio condition to add a new blob to the detected ones. + Detection is stopped if the new blob is smaller than the first one with respect to this ratio. + :param kernel: a structuring element used to remove noise. By default a 5-by-5 cross structure is used. + :return: a bitmask image of the pixels kept as being blob pixels + """ blur = cv2.GaussianBlur(sat_img, (5, 5), 0) thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] diff --git a/play.py b/play.py index 47c699d..64e98ea 100644 --- a/play.py +++ b/play.py @@ -39,10 +39,10 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument('--height', type=int, default=40, - help='New game board height resolution (default: 40)') + help='New game board height resolution if no input is given(default: 40)') parser.add_argument('--width', type=int, default=100, - help='New game board width resolution (default: 100)') - parser.add_argument('-i', '--input', type=str, + help='New game board width resolution if no input is given(default: 100)') + parser.add_argument('input', metavar="INPUT", type=str, nargs='?', default=None, help='Initialize game from a save. Overwrite height and width parameters. ' 'Pass the board filename as input (.board extension)') parser.add_argument('-s', '--scale', type=int, default=10, diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c18566b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +pygame==1.9.6 +imutils==0.5.2 +numpy==1.16.4 +pathfinding==0.0.4 +opencv_python==4.1.0.25 diff --git a/setup.py b/setup.py index a431232..baad117 100644 --- a/setup.py +++ b/setup.py @@ -20,8 +20,8 @@ def main(): - ap = argparse.ArgumentParser() - ap.add_argument("-i", "--input", required=True, help="Uses this input as image for setup") + ap = argparse.ArgumentParser("Setup the config file for blob detection and simulation") + ap.add_argument("input", metavar="INPUT", help="Uses this input as image for setup") ap.add_argument("-s", "--scale", type=float, default=0.25, help="Scales image by this factor (default: x0.25)") ap.add_argument("-c", "--config", type=str, default="detection/config.json", help="Saves config under this filename (default: detection/config.json)") diff --git a/simulation/board.py b/simulation/board.py index 1c9848b..215cf90 100644 --- a/simulation/board.py +++ b/simulation/board.py @@ -18,12 +18,17 @@ class Board: + """ Board keeps tracks of food or blob quantities on each square """ - MAX_BLOB = 255.0 - MIN_BLOB = 0.0 - INIT_FOOD = 100 + MAX_BLOB = 255.0 # Blob highest possible value + MIN_BLOB = 0.0 # Blob lowest possible value + INIT_FOOD = 100 # Highest and initial food value def __init__(self, width, height): + """ + :param width: number of squares for board width + :param height: number of squares for board height + """ self.width = width self.height = height @@ -32,6 +37,9 @@ def __init__(self, width, height): self.touched = np.zeros(shape=(width, height), dtype=bool) def save(self): + """ + :return: a string value containing all information to save board current state + """ stream = str(self.width) + ' ' + str(self.height) + '\n' for y in range(self.height): for x in range(self.width): @@ -43,6 +51,10 @@ def save(self): return stream.rstrip('\n') def load(self, filename): + """ + Restore from file the board state + :param filename: the name of a file which contains the board saved data + """ with open(filename, 'r') as file: dim = file.readline() dims = dim.split(' ') @@ -70,25 +82,52 @@ def load(self, filename): y += 1 def has_food(self, x, y): + """ + :param x: horizontal square position + :param y: vertical square position + :return: True if square (x,y) exists and has food + """ return self.inside(x, y) and self.foods[x, y] > 0 def set_food(self, x, y, value=INIT_FOOD): + """ + Set food on (x, y) square with 'value' as quantity of food + :param x: horizontal square position + :param y: vertical square position + :param value: the quantity of food to set + """ if not self.foods[x, y] > 0: self.foods[x, y] = value def remove_food(self, x, y): + """ + Set food on (x, y) square to zero + :param x: horizontal square position + :param y: vertical square position + """ if self.foods[x, y] > 0: self.foods[x, y] = 0 def update_blob(self, x, y, change_value): + """ + Modify the quantity of blob on (x, y) square and marks the square as being touched by the blob + :param x: horizontal square position + :param y: vertical square position + :param change_value: the blob value to add on this square + """ if self.inside(x, y): self.touched[x, y] = True self.dropped_blob[x, y] = max(Board.MIN_BLOB, min(self.dropped_blob[x, y] + change_value, Board.MAX_BLOB)) - return True - else: - return False def eat_food(self, x, y, change_value): + """ + Remove 'change_value' quantity from food quantity on (x,y) square + :param x: horizontal square position + :param y: vertical square position + :param change_value: the food value to remove from this square + :return: a tuple with actual change_value (if food was lower than given change_value) + and a boolean if food has been finished + """ if self.foods[x, y] > 0: if self.foods[x, y] - change_value >= 0: self.foods[x, y] -= change_value @@ -101,21 +140,42 @@ def eat_food(self, x, y, change_value): return change_value, self.foods[x, y] <= 0 def get_blob(self, x, y): + """ + :param x: horizontal square position + :param y: vertical square position + :return: the blob quantity on (x,y) square or None if square doesn't exist + """ if self.inside(x, y): return self.dropped_blob[x, y] else: return None def inside(self, x, y): + """ + :param x: horizontal square position + :param y: vertical square position + :return: True if square exists + """ return 0 <= x < self.width and 0 <= y < self.height def is_touched(self, x, y): + """ + :param x: horizontal square position + :param y: vertical square position + :return: True if square exists and has been touched by blob + """ if self.inside(x, y): return self.touched[x, y] else: return False def get_cover(self, half_board=0): + """ + :param half_board: 1 to return cover from up half-board, + 2 to return cover from bottom half-board + or 0 to return cover from all the board + :return: the covering percentage (number of touched square) on the complete board or given half-board + """ if half_board == 1: val = np.sum(self.touched[:, 0:int(self.height/2)]) * 2 elif half_board == 2: @@ -126,6 +186,9 @@ def get_cover(self, half_board=0): return val / self.height / self.width * 100 def get_blob_total(self): + """ + :return: the total quantity of blob on the board + """ total = 0 for x in range(self.width): for y in range(self.height): @@ -134,6 +197,12 @@ def get_blob_total(self): return total / self.height / self.width / self.MAX_BLOB * 100 def manage_blob(self, value, min_food_value=MIN_BLOB): + """ + On all squares decrease blob by 'value' + except if it's a food square then minimal value is set to 'min_food_value' + :param value: use to decrease all blob squares + :param min_food_value: minimal remaining blob value when it's a food square as well + """ for x in range(self.width): for y in range(self.height): if self.touched[x, y]: @@ -141,12 +210,25 @@ def manage_blob(self, value, min_food_value=MIN_BLOB): self.update_blob(x, y, -value) def reset(self, x, y): + """ + Reset square, meaning non-touched, no blob and no food + :param x: horizontal square position + :param y: vertical square position + """ if self.inside(x, y): self.touched[x, y] = False self.dropped_blob[x, y] = 0 self.foods[x, y] = 0 def compare(self, board): + """ + :param board: another board instance to compare to + :return: A new board instance set with comparison on each square: + food is set with this food square minus the board parameter food square + touched is true if both squares are touched or untouched + dropped_blob is set with this blob square minus the board parameter blob square + Return None if sizes don't match + """ if board.height != self.height and board.width != self.width: print("Size don't match !") return None diff --git a/simulation/interface.py b/simulation/interface.py index c6d47c1..feba9ed 100644 --- a/simulation/interface.py +++ b/simulation/interface.py @@ -25,6 +25,7 @@ class Interface: + """ Interface to show blob process and let's user interact with it """ FOOD_COLOR = (0, 150, 0) TOUCHED_COLOR = (50, 50, 0) @@ -35,16 +36,19 @@ class Interface: def __init__(self, board, player, blob, scale, save_dir, mode, hidden=False, colors_file=None): """ - :type board: Board - :type player: Player - :type blob: Blob_Manager - :type scale: float - :type save_dir: str + :param board: A board instance + :param player: A player instance + :param blob: A blob manager instance + :param scale: the scale to apply from board resolution to window resolution + :param save_dir: the save directory to use to save games + :param mode: a pygame mode flags + :param hidden: set to True if you want to keep interface hidden from user + :param colors_file: a color config file to modify interface colors """ - pygame.init() # pygame.key.set_repeat(5, 50) + # Reload colors if there is a file for if colors_file is not None: with open(colors_file, 'r') as file: colors = json.load(file) @@ -86,7 +90,9 @@ def __init__(self, board, player, blob, scale, save_dir, mode, hidden=False, col self.discovered_food = pygame.transform.scale(discovered_food, (scale, scale)) def draw(self): - + """ + Update interface and draw it again on the window + """ width = self.board.width * self.scale height = self.board.height * self.scale @@ -133,6 +139,10 @@ def draw(self): pygame.display.flip() def save(self, name=None): + """ + Store save files of the current state in given save directory + :param name: name used to save file, if none, use timestamp + """ if name is None: name = datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d_%H.%M.%S') @@ -156,6 +166,11 @@ def save(self, name=None): return name def event_listener(self, event): + """ + Check the type of event or key used and apply correct interactions + :param event: the event recevied from user + :return: True if event has been used + """ # ADMIN ACTIONS if event.type == KEYDOWN and event.key == 100: # D Letter diff --git a/simulation/logic/advanced_scouter.py b/simulation/logic/advanced_scouter.py index 94215d6..7b01f4f 100644 --- a/simulation/logic/advanced_scouter.py +++ b/simulation/logic/advanced_scouter.py @@ -22,17 +22,32 @@ class AdvancedScouter(SensingScouter): """ - Knowledge used: - - ["Scouting"]["Global Explore Probability"] : (float, between 0 and 1) Set the ratio between exploring - globally and exploring locally - - ["Scouting"]["Search Locally on Food"] : when stepping on food, automatically search locally + Improve scouting with different goals : searching after local-square minima and searching after sightline-square minima + Knowledge used: + - ["Scouting"]["Global Explore Probability"] : (float, between 0 and 1) Set the ratio between exploring + globally and exploring locally + - ["Scouting"]["Search Locally on Food"] : when stepping on food, automatically search locally + - See "SensingScouter" for remaining knowledge used """ def __init__(self, board, knowledge, x, y, use_diagonal=False, sightline=3, light_compute=True): + """ + :param board: A board class instance + :param knowledge: a dict containing all blob knowledge and set up + :param x: current horizontal position of the ant + :param y: current vertical position of the ant + :param use_diagonal: boolean set to true if diagonal moves are available + :param sightline: size of the ant sightline, used to compute goal decision + :param light_compute: boolean set to true if path is computing only once and then memorized until reaching goal + """ SensingScouter.__init__(self, board, knowledge, x, y, use_diagonal, sightline, light_compute) self.state = 0 def choose_goal(self): + """ + Based on self.state return goal for global or local search + Modify self.state for next time + """ if self.state == 0: if not (self.board.has_food(self.x, self.y) and self.knowledge["Scouting"]["Search Locally on Food"]) \ and self.knowledge["Scouting"]["Global Explore Probability"] < random.random(): @@ -44,17 +59,24 @@ def choose_goal(self): return self.choose_global_goal() def choose_local_goal(self): + """ + Return "classical" SensingScouter goal + """ return SensingScouter.choose_goal(self) def choose_global_goal(self): - x0, y0 = max(0, self.x - self.sight_see), max(0, self.y - self.sight_see) - x1, y1 = min(self.board.width, self.x + self.sight_see + 1), min(self.board.height, self.y + self.sight_see + 1) + """ + Special goal based on minimizing the average blob seen by the ant (with respect to sight see) + :return: + """ + x0, y0 = max(0, self.x - self.sightline), max(0, self.y - self.sightline) + x1, y1 = min(self.board.width, self.x + self.sightline + 1), min(self.board.height, self.y + self.sightline + 1) scores = np.zeros((x1 - x0, y1 - y0), dtype=float) for x in range(x1 - x0): for y in range(y1 - y0): - local_x0, local_y0 = max(x0, x0 + x - self.sight_see), max(y0, y0 + y - self.sight_see) - local_x1, local_y1 = min(x1, x0 + x + self.sight_see + 1), min(y1, y0 + y + self.sight_see + 1) + local_x0, local_y0 = max(x0, x0 + x - self.sightline), max(y0, y0 + y - self.sightline) + local_x1, local_y1 = min(x1, x0 + x + self.sightline + 1), min(y1, y0 + y + self.sightline + 1) scores[x, y] = np.sum(self.board.dropped_blob[local_x0:local_x1, local_y0:local_y1]) total_area = (y1-y0) * (x1-x0) @@ -69,6 +91,10 @@ def choose_global_goal(self): return min_indices[0][i] + x0, min_indices[1][i] + y0 def move(self): + """ + "classical" SensingScouter move with special state branching if Search Locally on Food is true + :return: + """ if self.board.has_food(self.x, self.y) and self.knowledge["Scouting"]["Search Locally on Food"] \ and self.state == 1: self.goal = None diff --git a/simulation/logic/blob_manager.py b/simulation/logic/blob_manager.py index 41d266e..0db38a6 100644 --- a/simulation/logic/blob_manager.py +++ b/simulation/logic/blob_manager.py @@ -17,23 +17,34 @@ import random import json -# from ant import Ant -# from gatherer import Gatherer from simulation.logic.fsm_ant import FSMAnt from simulation.board import Board class BlobManager: - - def __init__(self, board, default_knowledge): + """ + Top-level class to manage blob and therefore ants colony numbering and moving, manage also foods known + Knowledge used: + - ["max_scouters"] to keep in memory maximum number of scouters + - ["food"] for known food positions + - ["Global Decrease"] to globally decrease blob on every board square + - ["Remaining Blob on Food"] to set a minimum blob value to keep on food sqaure + - ["Computing"] values : ["Blob Size Factor"]["Covering Factor"]["Known Foods Factor"]["Global Factor"] + as different factors to compute maximum scouters number + - ["Scouters"]["Min"] to ensure blob keeps a minimal number of scouters + - See "FSMAnt" class for remaining knowledge used + """ + + def __init__(self, board, knowledge): """ - :type board: Board + :param board: A board class instance + :param knowledge: A json file with all knowledge used for blob managing """ self.board = board self.knowledge = dict() self.scouters = [] - with open(default_knowledge, 'r') as file: + with open(knowledge, 'r') as file: self.knowledge.update(json.load(file)) self.knowledge['food'] = [] @@ -42,6 +53,7 @@ def __init__(self, board, default_knowledge): if self.board.has_food(x, y) and self.board.is_touched(x, y): self.knowledge['food'].append((x, y)) + # TODO Refactor ['max_scouters'] as ['Scouters']['Max'] for consistency with minimum scouters self.knowledge['max_scouters'] = self.compute_max_scouters() while len(self.scouters) < self.knowledge['max_scouters']: self.add_scouter() @@ -49,12 +61,19 @@ def __init__(self, board, default_knowledge): print("Scouters: " + str(len(self.scouters))) def save(self): + """ + Return a json structure to save all knowledge (except food and max_scouters which are computed on the fly) + """ d = self.knowledge.copy() del d["food"] del d["max_scouters"] return json.dumps(d, indent=4, sort_keys=True) def move(self): + """ + Update all ants position, remove possible trapped ants and remove or add ants based on max_scouters capability + Finally decrease blob all over the board + """ deads = [] for scouter in self.scouters: old = (scouter.x, scouter.y) @@ -90,6 +109,9 @@ def move(self): self.board.manage_blob(self.knowledge["Global Decrease"], self.knowledge["Remaining Blob on Food"]) def add_scouter(self): + """ + Add a new scouter inside blob squares except if max has already been reached + """ if len(self.scouters) < self.knowledge['max_scouters']: if len(self.knowledge['food']) != 0: index = random.randrange(len(self.knowledge['food'])) @@ -102,10 +124,16 @@ def add_scouter(self): print("Max scouters already reached !") def remove_scouter(self): + """ + Remove randomly a scouter + """ nbr = random.randrange(len(self.scouters)) del self.scouters[nbr] def compute_max_scouters(self): + """ + Based on given factor compute the maximum number of scouters that can be used + """ total_scouters = self.knowledge["Computing"]["Blob Size Factor"] * self.board.get_blob_total() \ + self.knowledge["Computing"]["Covering Factor"] * self.board.get_cover() \ + self.knowledge["Computing"]["Known Foods Factor"] * len(self.knowledge['food']) @@ -115,6 +143,10 @@ def compute_max_scouters(self): return max(self.knowledge["Scouters"]["Min"], int(total_scouters)) def find_blob_square(self): + """ + Return a random position where there is blob. + Random selection is weighted with blob quantity on each square + """ availables = [] total_blob = 0 for x in range(self.board.width): @@ -137,6 +169,11 @@ def find_blob_square(self): return square def reset(self, x, y): + """ + Reset all information and potential ants on the given position + :param x: current horizontal position to reset + :param y: current vertical position to reset + """ for scouter in self.scouters.copy(): if scouter.x == x and scouter.y == y: self.scouters.remove(scouter) @@ -147,6 +184,12 @@ def reset(self, x, y): self.knowledge['max_scouters'] -= 1 def food_discovered(self, x, y): + """ + Add this food to the blob knowledge + :param x: current horizontal position where food has been discovered + :param y: current vertical position where food has been discovered + """ + self.knowledge['food'].append((x, y)) # self.knowledge['max_scouters'] += 1 @@ -156,4 +199,9 @@ def food_discovered(self, x, y): # print("Food discovered in (" + str(x) + ", " + str(y) + ")") def food_destroyed(self, x, y): + """ + Remove this food from the blob knowledge + :param x: current horizontal position where food has been discovered + :param y: current vertical position where food has been discovered + """ self.knowledge['food'].remove((x, y)) diff --git a/simulation/logic/dumb_scouter.py b/simulation/logic/dumb_scouter.py index b50a1ee..0bb6af7 100644 --- a/simulation/logic/dumb_scouter.py +++ b/simulation/logic/dumb_scouter.py @@ -20,14 +20,14 @@ class DumbScouter: - """ Dumb scouter searching food randomly and without any knowledge """ + """ Dumb scouter searching food randomly and without any knowledge except a drop ratio""" def __init__(self, board, knowledge, x, y): """ - :type board: Board - :type knowledge: dict - :type x: int - :type y: int + :param board: A board class instance + :param knowledge: a dict containing all blob knowledge and set up + :param x: current horizontal position of the ant + :param y: current vertical position of the ant """ self.board = board self.knowledge = knowledge @@ -36,6 +36,9 @@ def __init__(self, board, knowledge, x, y): self.drop = self.knowledge["Scouters"]["Drop by eat"] def move(self): + """ + Move the scouter by one square + """ x = self.x + random.randint(-1, 1) y = self.y + random.randint(-1, 1) if self.board.inside(x, y): @@ -43,4 +46,7 @@ def move(self): self.y = y def update(self): + """ + Update board with drop ratio on the current position + """ self.board.update_blob(self.x, self.y, self.drop) \ No newline at end of file diff --git a/simulation/logic/fsm_ant.py b/simulation/logic/fsm_ant.py index 25bf727..e8645d0 100644 --- a/simulation/logic/fsm_ant.py +++ b/simulation/logic/fsm_ant.py @@ -17,30 +17,30 @@ from simulation.board import Board from simulation.logic.dumb_scouter import DumbScouter from simulation.logic.gatherer import Gatherer -from simulation.logic.sensing_scouter import SensingScouter from simulation.logic.advanced_scouter import AdvancedScouter class FSMAnt(DumbScouter): """ - Knowledge used: - - ["Harvesting"]["Min"]: (float) Min value an ant has to store to stop being starved - - ["Harvesting"]["Max"]: (float) Max value an ant can carry - - ["Harvesting"]["Eat"]: (float) Value an ant eat to do a step - - ["Harvesting"]["Collect"]: (float) Value an ant collect by stepping on food - - ["Gathering"]/["Scouting"]["Diagonal Moves"]: (bool) Allow ants to use diagonals to travel + Adapting ant with several states : + - scouting state where she search after new food (see AdvancedScouter) + - gathering state where she collect food to stop starving (see Gatherer) + + Knowledge used: + - ["Harvesting"]["Min"]: (float) Min value an ant has to store to stop being starved + - ["Harvesting"]["Max"]: (float) Max value an ant can carry + - ["Harvesting"]["Eat"]: (float) Value an ant eat to do a step + - ["Harvesting"]["Collect"]: (float) Value an ant collect by stepping on food + - ["Gathering"]/["Scouting"]["Diagonal Moves"]: (bool) Allow ants to use diagonals to travel """ - # Multiplication factor for drop value (see blob variable) when an ant is starving - RATIO_DROP_STARVE = 2 - def __init__(self, board, knowledge, x, y): """ - :type board: Board - :type knowledge: dict - :type x: int - :type y: int + :param board: A board class instance + :param knowledge: a dict containing all blob knowledge and set up + :param x: current horizontal position of the ant + :param y: current vertical position of the ant """ DumbScouter.__init__(self, board, knowledge, x, y) self.gatherer_logic = Gatherer(board, knowledge, x, y, self.knowledge["Gathering"]["Diagonal Moves"], @@ -54,6 +54,9 @@ def __init__(self, board, knowledge, x, y): self.starving = False def move(self): + """ + Move the ant based on the state she is using + """ if self.starving: self.gatherer_logic.move() self.x = self.gatherer_logic.x @@ -64,16 +67,27 @@ def move(self): self.y = self.scouting_logic.y def init_gathering(self): + """ + Reset gathering logic + """ self.gatherer_logic.reset() self.gatherer_logic.x = self.x self.gatherer_logic.y = self.y def init_scouting(self): + """ + Reset scouting logic + """ self.scouting_logic.reset() self.scouting_logic.x = self.x self.scouting_logic.y = self.y def update(self): + """ + Update square where the ant is, check state based on remaining stored food and collect food if square has any + """ + + # Update square and eat used value eat_ratio = self.knowledge["Harvesting"]["Eat"] * (Board.MAX_BLOB - self.board.get_blob(self.x, self.y)) \ / Board.MAX_BLOB self.drop = self.knowledge["Scouters"]["Drop by eat"] * eat_ratio @@ -83,6 +97,7 @@ def update(self): self.stored -= eat_ratio self.stored = max(0, self.stored) + # Collect food if square has any if self.board.has_food(self.x, self.y): if len(self.knowledge['food']) == 1: wanted = min(self.knowledge["Harvesting"]["Min"], self.knowledge["Harvesting"]["Max"] - self.stored) @@ -95,6 +110,7 @@ def update(self): if finished: self.knowledge['food'].remove((self.x, self.y)) + # Update FSM State if self.stored == 0 and not self.starving: self.starving = True self.init_gathering() diff --git a/simulation/logic/gatherer.py b/simulation/logic/gatherer.py index a49f5c3..dc1c814 100644 --- a/simulation/logic/gatherer.py +++ b/simulation/logic/gatherer.py @@ -26,17 +26,40 @@ class Gatherer(DumbScouter): + """ An ant with the only goal is to move to different food squares""" + + # TODO Gatherer could have a more intuitive logic without knowing the exact location of foods outside sightline + # and by following only maximum blob quantity to hope find food. + # But this will likely lead to disconnection of blob from food to food... + def __init__(self, board, knowledge, x, y, use_diagonal=True, sightline=-1, light_compute=True): + """ + :param board: A board class instance + :param knowledge: a dict containing all blob knowledge and set up + :param x: current horizontal position of the ant + :param y: current vertical position of the ant + :param use_diagonal: boolean set to true if diagonal moves are available + :param sightline: size of the ant sightline, used to compute goal decision + :param light_compute: boolean set to true if path is computing only once and then memorized until reaching goal + """ DumbScouter.__init__(self, board, knowledge, x, y) self.use_diagonal = use_diagonal self.light_compute = light_compute - self.sight_see = sightline if sightline > 0 else max(self.board.width, self.board.height) + self.sightline = sightline if sightline > 0 else max(self.board.width, self.board.height) self.goal = None self.path = [] def get_matrix(self, x0, y0, x1, y1): + """ + Compute and return a pathfinding matrix filled with board squares values + in the rectangle given by (x0,y0) and (x1,y1) + :param x0: the x coordinate of the up left corner of the rectangle + :param y0: the y coordinate of the up left corner of the rectangle + :param x1: the x coordinate of the bottom right corner of the rectangle + :param y1: the x coordinate of the bottom right corner of the rectangle + """ width = x1 - x0 height = y1 - y0 matrix = np.zeros((width, height)) @@ -52,10 +75,19 @@ def get_matrix(self, x0, y0, x1, y1): return np.transpose(matrix) def compute_sight_see_goal(self, x0, y0, x1, y1): + """ + Compute and return a local goal in (x0,y0) (x1,y1) area to reach global food goal + :param x0: the x coordinate of the up left corner of the rectangle + :param y0: the y coordinate of the up left corner of the rectangle + :param x1: the x coordinate of the bottom right corner of the rectangle + :param y1: the x coordinate of the bottom right corner of the rectangle + """ + + # Check if goal is not in sightline rectangle if x0 <= self.goal[0] < x1 and y0 <= self.goal[1] < y1: - # Goal in sight_see return self.goal[0] - x0, self.goal[1] - y0 + # Compute linear projection from the goal inside rectangle and find minimal t parameter of this projection delta_x = self.x - self.goal[0] delta_y = self.y - self.goal[1] @@ -76,8 +108,10 @@ def compute_sight_see_goal(self, x0, y0, x1, y1): else: t = min(t_x, t_y) + # First hit square by projection symb_goal = (int(self.goal[0] + t * delta_x), int(self.goal[1] + t * delta_y)) + # Iterate over t parameter until a touched square is found found = self.board.is_touched(symb_goal[0], symb_goal[1]) while not found and t <= 1: inc = 1 / (self.board.width + self.board.height) @@ -88,8 +122,11 @@ def compute_sight_see_goal(self, x0, y0, x1, y1): return symb_goal[0] - x0, symb_goal[1] - y0 def best_way_to(self): - x0, y0 = max(0, self.x - self.sight_see), max(0, self.y - self.sight_see) - x1, y1 = min(self.board.width, self.x + self.sight_see + 1), min(self.board.height, self.y + self.sight_see + 1) + """ + Inside sightline, compute pathfinding matrix, set local goal, compute and store path + """ + x0, y0 = max(0, self.x - self.sightline), max(0, self.y - self.sightline) + x1, y1 = min(self.board.width, self.x + self.sightline + 1), min(self.board.height, self.y + self.sightline + 1) grid = Grid(matrix=self.get_matrix(x0, y0, x1, y1)) @@ -109,9 +146,16 @@ def best_way_to(self): self.path[i] = (step[0] + x0, step[1] + y0) def reached(self, goal): + """ + :param goal: a x,y tuple coordinate of the goal + :return: True if goal exists and has been reached + """ return goal is not None and self.x == goal[0] and self.y == goal[1] def choose_goal(self): + """ + :return: a new goal for ant, based on unreached known food + """ if len(self.knowledge['food']) == 0: return None elif len(self.knowledge['food']) == 1: @@ -126,22 +170,31 @@ def choose_goal(self): return self.knowledge['food'][i] def reset(self): + """ + Reset the ant including goal, path and position + """ self.goal = None self.path = [] self.x = 0 self.y = 0 def move(self): + """ + Move ant towards set goal or compute a new goal if needed + """ + # Scouter has no more goal if self.goal is None or self.goal not in self.knowledge['food']: self.goal = self.choose_goal() self.path = [] - # No goal + # No new goal found if self.goal is None: return # Scouter has no more path to goal + # TODO Light compute could be integrated by only checking if next move is an autorized move + # and otherwise recalculate if len(self.path) == 0 or not self.light_compute: self.best_way_to() @@ -150,6 +203,7 @@ def move(self): self.goal = None return + # Move the ant by one square new_pos = self.path[0] self.path = self.path[1:] diff --git a/simulation/logic/sensing_scouter.py b/simulation/logic/sensing_scouter.py index 7d73a47..812ee8d 100644 --- a/simulation/logic/sensing_scouter.py +++ b/simulation/logic/sensing_scouter.py @@ -25,20 +25,39 @@ class SensingScouter(DumbScouter): + """ An ant with goal is to explore unknown (or non-touched) square """ def __init__(self, board, knowledge, x, y, use_diagonal=False, sightline=-1, light_compute=True): + """ + :param board: A board class instance + :param knowledge: a dict containing all blob knowledge and set up + :param x: current horizontal position of the ant + :param y: current vertical position of the ant + :param use_diagonal: boolean set to true if diagonal moves are available + :param sightline: size of the ant sightline, used to compute goal decision + :param light_compute: boolean set to true if path is computing only once and then memorized until reaching goal + """ DumbScouter.__init__(self, board, knowledge, x, y) self.use_diagonal = use_diagonal - self.sight_see = sightline if sightline > 0 else 1 + self.sightline = sightline if sightline > 0 else 1 self.light_compute = light_compute self.goal = None self.path = [] def get_matrix(self, x0, y0, x1, y1): + """ + Compute and return a pathfinding matrix filled with board squares values + in the rectangle given by (x0,y0) and (x1,y1) + :param x0: the x coordinate of the up left corner of the rectangle + :param y0: the y coordinate of the up left corner of the rectangle + :param x1: the x coordinate of the bottom right corner of the rectangle + :param y1: the x coordinate of the bottom right corner of the rectangle + """ width = x1 - x0 height = y1 - y0 matrix = np.zeros((width, height)) + for y in range(height): for x in range(width): if self.board.get_blob(x0 + x, y0 + y) > 0: @@ -47,11 +66,15 @@ def get_matrix(self, x0, y0, x1, y1): matrix[x, y] = Board.MAX_BLOB * 2 else: matrix[x, y] = 1 + return np.transpose(matrix) def choose_goal(self): - x0, y0 = max(0, self.x - self.sight_see), max(0, self.y - self.sight_see) - x1, y1 = min(self.board.width, self.x + self.sight_see + 1), min(self.board.height, self.y + self.sight_see + 1) + """ + :return: a new goal for ant, based on unreached known food + """ + x0, y0 = max(0, self.x - self.sightline), max(0, self.y - self.sightline) + x1, y1 = min(self.board.width, self.x + self.sightline + 1), min(self.board.height, self.y + self.sightline + 1) mask = np.zeros((x1 - x0, y1 - y0), dtype=bool) mask[self.x - x0, self.y - y0] = True @@ -65,10 +88,13 @@ def choose_goal(self): return min_indices[0][i] + x0, min_indices[1][i] + y0 def best_way_to(self): - if self.sight_see > 0: - x0, y0 = max(0, self.x - self.sight_see), max(0, self.y - self.sight_see) - x1, y1 = min(self.board.width, self.x + self.sight_see + 1), min(self.board.height, - self.y + self.sight_see + 1) + """ + Inside sightline, compute pathfinding matrix, set local goal, compute and store path + """ + if self.sightline > 0: + x0, y0 = max(0, self.x - self.sightline), max(0, self.y - self.sightline) + x1, y1 = min(self.board.width, self.x + self.sightline + 1), min(self.board.height, + self.y + self.sightline + 1) else: x0, y0 = 0, 0 x1, y1 = self.board.width, self.board.height @@ -89,9 +115,17 @@ def best_way_to(self): self.path[i] = (step[0] + x0, step[1] + y0) def reached(self, goal): + """ + :param goal: a x,y tuple coordinate of the goal + :return: True if goal exists and has been reached + """ return goal is not None and self.x == goal[0] and self.y == goal[1] def move(self): + """ + Move ant towards set goal or compute a new goal if needed + """ + # Scouter has no more goal if self.goal is None: # or self.board.get_blob(self.goal[0], self.goal[1]) != 0: self.goal = self.choose_goal() @@ -99,11 +133,13 @@ def move(self): print("Shouldn't happen") self.path = [] - # No goal + # No new goal found if self.goal is None: return # Scouter has no more path to goal + # TODO Light compute could be integrated by only checking if next move is an autorized move + # and otherwise recalculate if len(self.path) == 0 or not self.light_compute: self.best_way_to() @@ -112,6 +148,7 @@ def move(self): self.goal = None return + # Move the ant by one square new_pos = self.path[0] self.path = self.path[1:] @@ -124,6 +161,9 @@ def move(self): self.path = [] def reset(self): + """ + Reset the ant including goal, path and position + """ self.goal = None self.path = [] self.x = 0 diff --git a/simulation/player.py b/simulation/player.py index 0ce3673..891659c 100644 --- a/simulation/player.py +++ b/simulation/player.py @@ -22,16 +22,18 @@ class Player: + """ A player class giving methods to help user interacts with Board """ - def __init__(self, board, blob, default_config): + def __init__(self, board, blob, config): """ - :type blob: Blob_Manager - :type board: Board + :param board: a Board instance + :param blob: a BlobManager instance + :param config: a config file to set up player variables """ self.board = board self.blob = blob - with open(default_config, 'r') as file: + with open(config, 'r') as file: d = json.load(file) self.clean_top = d['clean_top'] @@ -39,6 +41,9 @@ def __init__(self, board, blob, default_config): self.use_circle = d['use_food_circle'] def save(self): + """ + :return: a json string with player variables to save + """ d = dict() d['clean_top'] = self.clean_top d['food_size'] = self.food_size @@ -46,6 +51,13 @@ def save(self): return json.dumps(d, indent=4, sort_keys=True) def set_random_food(self, qt, random_top=None): + """ + Put a certain quantity of foods on the board with size self.food_size + :param qt: number of foods to put + :param random_top: set to True if you want to put only on top board + or false if you want to put them only on bottom board + :return: the list of random food positions used + """ if random_top is None: # Randomize over all the board y_offset = 0 y_range = self.board.height @@ -69,6 +81,12 @@ def set_random_food(self, qt, random_top=None): return foods_list def remove_food(self, x, y): + """ + Remove all non-touched food in the area self.food_size of (x,y) square + :param x: horizontal square position + :param y: vertical square position + :return: True if any food has been removed, False otherwise + """ food_remove = False x0, y0 = int(x - self.food_size / 2), int(y - self.food_size / 2) for x_size in range(self.food_size): @@ -87,6 +105,14 @@ def remove_food(self, x, y): return True def set_food(self, x, y, force=False, value=Board.INIT_FOOD): + """ + Put a food of size self.food_size with center (x,y) square + :param x: horizontal square position + :param y: vertical square position + :param force: if true, put food even if blob already touched square + :param value: the initial value of food set + :return: True if any food has been put, False otherwise + """ food_put = False x0, y0 = int(x - self.food_size / 2), int(y - self.food_size / 2) for x_size in range(self.food_size): @@ -105,9 +131,16 @@ def set_food(self, x, y, force=False, value=Board.INIT_FOOD): return True def check_blob_cover(self): + """ + :return: a tuple with blob half-board (top, bottom) covering + """ return self.board.get_cover(1), self.board.get_cover(2) def clean_board(self): + """ + Reset one of the half-board, depending of self.clean_top variable + (if true, reset top board, otherwise bottom board) + """ y_range = ceil(self.board.height/2) if self.clean_top: