Premier commit du programme sur la nouvelle instance Gitea

Import des fichiers :

Modifications qui seront validées :
	nouveau fichier : MOT20-02.mp4
	nouveau fichier : README.md
	nouveau fichier : autoinstall.sh
	nouveau fichier : autostart.sh
	nouveau fichier : checking-camera.py
	nouveau fichier : main.py
	nouveau fichier : models/yolo11n.pt
	nouveau fichier : models/yolo12n.pt
	nouveau fichier : requirements.txt
	nouveau fichier : track/botsort.yaml
	nouveau fichier : track/bytetrack.yaml
This commit is contained in:
2025-10-19 11:53:24 +02:00
commit 18e8d6eef1
11 changed files with 566 additions and 0 deletions

BIN
MOT20-02.mp4 Normal file

Binary file not shown.

100
README.md Normal file
View File

@@ -0,0 +1,100 @@
# Vision par ordinateur (Édition Python)
---
## Prérequis
- Ordinateur fixe ou portable, architecture de processeur x86/AMD64
- Système d'exploitation Linux
- Python 3.13.2
```bash
sudo apt install -y make build-essential libssl-dev zlib1g-dev \
libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \
libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev \
libffi-dev liblzma-dev tk-dev libncurses5-dev libncursesw5-dev
# Où
sudo dnf install gcc zlib-devel bzip2 bzip2-devel readline-devel \
sqlite sqlite-devel openssl-devel xz xz-devel libffi-devel \
findutils tk-devel ncurses-devel
```
```bash
curl -fsSL https://pyenv.run | bash
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init - bash)"' >> ~/.bashrc
echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bashrc
pyenv install 3.13.2
cd répertoire/du/projet
pyenv local 3.13.2
```
---
Créer un environnement virtuel Python.
```bash
python -m venv venv
```
Activer l'environnement virtuel au terminal courant.
```bash
source venv/bin/activate
```
Mettre à jour Pip (pour éviter les erreurs d'installation de librairies).
```bash
pip install --upgrade pip
```
Installer les librairies nécessaires au déroulement du programme.
```bash
pip install -r requirements.txt
```
Exécuter le dialogue d'aide sur le programme principale.
```bash
> python main.py --help
usage: main.py [-h] [-i INPUT] [-o OUTPUT] [-f] [-u API_URL] [-m MODELNAME] [-v]
options:
-i, --input INPUT Chemin vers la vidéo à lire ('0' est à utiliser pour la webcam par défaut)
-o, --output OUTPUT Emplacement pour enregistrer le processus dans une vidéo (Attention au RGPD, désactivé si aucun)
-f, --force Forcer le redimensionnement du flux d'entrée.
-u, --api-url API_URL
Chemin réseau vers l'API (désactivé si aucun)
-m, --model MODELNAME
Nom complet (ou chemin) du modèle de détection à utiliser
-v, --verbose Activer la sortie verbeuse du programme.
```
---
## Démarrage automatique
Dans le répertoire source du projet, il y a un script bash permettant l'installation et le démarrage automatique.
Il faut d'abord essayer le programme manuellement (avec les instructions précédentes). Si le programme est satisfaisant, le script d'auto-installation est pertinent.
Exécuter le script
```bash
./auto-install.sh
```
Le script propose quels paramètres sont à activer automatiquement (pour l'exécution à l'ouverture de la session)
```bash
> ./autoinstall.sh
Voulez-vous indiquer un flux d'entrée ? (oui/non): oui
Entrez la valeur d'entrée : 4
Voulez-vous une verbosité accrue ? (oui/non): non
Voulez-vous forcer le redimenssionnement de l'image ? (oui/non): oui
Voulez-vous renseigner l'URL de l'API ? (oui/non): oui
Entrez l'URL de l'API : http://localhost:1880
```
Redémarrer l'ordinateur hôte.
```bash
sudo reboot now
```
En cas d'erreurs ou de doutes, regarder la journalisation.
```bash
less ~/vision-par-ordinateur-python/main.log
```

69
autoinstall.sh Executable file
View File

@@ -0,0 +1,69 @@
#!/bin/bash
# Initialize variables
START_FILE="$HOME/.config/autostart/vision-par-ordinateur-python.desktop"
PROJECT_DIR="$HOME/vision-par-ordinateur-python"
INPUT=""
VERBOSE=""
FORCE=""
API_URL=""
# Load PyEnv for user
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - bash)"
eval "$(pyenv virtualenv-init -)"
# Prompt for INPUT
read -p "Voulez-vous indiquer un flux d'entrée ? (oui/non): " input_choice
if [[ "$input_choice" == "oui" || "$input_choice" == "o" ]]; then
read -p "Entrez la valeur d'entrée : " INPUT
INPUT="-i $INPUT"
fi
# Prompt for VERBOSE
read -p "Voulez-vous une verbosité accrue ? (oui/non): " verbose_choice
if [[ "$verbose_choice" == "oui" || "$verbose_choice" == "o" ]]; then
VERBOSE="-v"
fi
# Prompt for FORCE
read -p "Voulez-vous forcer le redimenssionnement de l'image ? (oui/non): " force_choice
if [[ "$force_choice" == "oui" || "$force_choice" == "o" ]]; then
FORCE="-f"
fi
# Prompt for API_URL
read -p "Voulez-vous renseigner l'URL de l'API ? (oui/non): " api_choice
if [[ "$api_choice" == "oui" || "$api_choice" == "o" ]]; then
read -p "Entrez l'URL de l'API : " API_URL
API_URL="--api-url $API_URL"
fi
# Construct the final command
COMMAND="python main.py $INPUT $VERBOSE $FORCE $API_URL >> $HOME/vision-par-ordinateur-python/main.log 2>&1"
# Output the command to the autostart.sh file
echo -e "$COMMAND" >> autostart.sh
# Make the script executable
chmod +x autostart.sh
# Make the project directory and copy content
mkdir "${PROJECT_DIR}"
cp -r main.py autostart.sh requirements.txt models track "${PROJECT_DIR}"
# Install venv and requirements
cd "${PROJECT_DIR}"
pyenv local 3.13.2
python -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
# Auto start program settings
mkdir -p ~/.config/autostart
touch "${START_FILE}"
echo -e "[Desktop Entry]\nType=Application\nExec=${HOME}/vision-par-ordinateur-python/autostart.sh\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true\nName=Vision par ordinateur\nComment=Lance le script Python pour la vision par ordinateur" >> "${START_FILE}"
echo -e "L'installation du programme et du démarrage automatique est terminé.\nVeuillez redémarrer l'ordinateur pour démarrer la vision par ordinateur automatiquement."

11
autostart.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
cd ~
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - bash)"
eval "$(pyenv virtualenv-init -)"
cd ~/vision-par-ordinateur-python
source venv/bin/activate

4
checking-camera.py Normal file
View File

@@ -0,0 +1,4 @@
from cv2_enumerate_cameras import enumerate_cameras
for camera_info in enumerate_cameras():
print(f'{camera_info.index}: {camera_info.name}')

285
main.py Normal file
View File

@@ -0,0 +1,285 @@
import time
import json
import asyncio
import aiohttp
import ssl
import requests
from datetime import date
from collections import defaultdict
import cv2
import numpy as np
import argparse
import sys
from ultralytics import YOLO
# Declare video file to read with parameter
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--input", dest="input", default=0, help="Chemin vers la vidéo à lire ('0' est à utiliser pour la webcam par défaut)")
parser.add_argument("-o", "--output", dest="output", default="", help="Emplacement pour enregistrer le processus dans une vidéo (Attention au RGPD, désactivé si aucun)")
parser.add_argument("-f", "--force", action='store_true', help="Forcer le redimensionnement du flux d'entrée")
parser.add_argument("-u", "--api-url", dest="API_URL", default="", help="Chemin réseau vers l'API (désactivé si aucun)")
parser.add_argument("-m", "--model", dest="modelname", default="yolo12n.pt", help="Nom complet du modèle de détection à utiliser (répertoire 'models/')")
parser.add_argument('-v', '--verbose', action='store_true', help='Activer la sortie verbeuse du programme.')
args = parser.parse_args()
# Initialize verbose mode
if args.verbose:
print("Mode verbeux activé")
time_mid_list = []
time_end_list = []
fps_list = []
# Initialize API variables
API_URL_EVENT = args.API_URL + "/api/c_auto"
API_URL_POS = args.API_URL + "/api/p_auto"
API_URL_STATUS = args.API_URL + "/api/status"
# Declare interval variable to get status
STATUS_CHECK_INTERVAL = 10
# Define function to get API event status
async def get_status(session):
try:
async with session.get(API_URL_STATUS) as response:
response.raise_for_status()
data = await response.text()
return int(data)
except aiohttp.ClientError as e:
print(f"Impossible d'obtenir l'état: {e}")
# Define event API requests function
async def send_event(session, action, people_count, person_id, verbose=False):
payload = [{"action": action, "people": people_count}, {"id": person_id}]
try:
async with session.post(API_URL_EVENT, json=payload) as response:
response.raise_for_status()
if verbose:
print(f"Requête action {action} pour l'id:{person_id} avec {people_count} visitors")
except aiohttp.ClientError as e:
print(f"Impossible d'envoyer l'action {action} de l'id:{person_id}: {e}")
# Define position API requests function
async def send_position(session, positions, verbose=False):
try:
async with session.post(API_URL_POS, json=positions) as response:
response.raise_for_status()
if verbose:
print(f"Positions envoyées : {len(positions)}")
except aiohttp.ClientError as e:
print(f"Impossible d'envoyer les positions: {e}")
# Store the track history
track_history = defaultdict(lambda: [])
# Create asyncio event loop
async def main_loop():
# Initialize FPS measurement
if args.verbose:
prev_time_fps = time.time()
# Define for the first time last_position_sent
try:
last_position_sent
except NameError:
last_position_sent = int(time.time())
# Patch public variable
current_people = set()
# Define for the first time last_status_check
try:
last_status_check
except NameError:
last_status_check = int(time.time()) - STATUS_CHECK_INTERVAL
# Variable for status and det/track by default
if args.API_URL:
status = 0
else:
status = 1
model = None
cap = None
video_writer = None
async with aiohttp.ClientSession() as session:
while True:
# Check status at the defined interval
if args.API_URL:
if time.time() - last_status_check >= STATUS_CHECK_INTERVAL:
status = await get_status(session)
last_status_check = time.time()
if args.verbose:
print(f"État mis à jour: {status}")
# Clear everything and wait for other status
if status == 0 or not status:
if cap:
cap.release()
cap = None
if video_writer:
video_writer.release()
video_writer = None
cv2.destroyAllWindows()
if args.verbose:
print("Ressources libérées/détruites. Dans l'attente de l'état 1 pour redémarrer.")
# Prevent CPU overload
await asyncio.sleep(1)
continue
# Skip if status is 0
if status == 2:
if current_people and args.API_URL:
for person_id in current_people:
await send_event(session, "exit", 0, person_id, args.verbose)
current_people.clear()
if args.verbose:
print("État 2 reçu: Envoi de l'action de sortie pour tous les ID encore actifs.")
await asyncio.sleep(1)
continue
# Initialize model and video capture if status is 1
if status == 1 and not cap:
model = YOLO(f"models/{args.modelname}")
if args.verbose:
print(f"Modèle YOLO chargé: models/{args.modelname}")
try:
input_value = int(args.input)
cap = cv2.VideoCapture(input_value)
except ValueError:
cap = cv2.VideoCapture(args.input, cv2.CAP_FFMPEG)
if not cap.isOpened():
print("Erreur: Impossible d'ouvrir le flux vidéo.")
sys.exit(1)
if args.force:
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
if args.output:
w, h, fps = (int(cap.get(x)) for x in (cv2.CAP_PROP_FRAME_WIDTH, cv2.CAP_PROP_FRAME_HEIGHT, cv2.CAP_PROP_FPS))
video_writer = cv2.VideoWriter(args.output, cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h))
if args.verbose:
print("Capture vidéo initialisée.")
# Skip if no video capture is open
if not cap:
await asyncio.sleep(1)
continue
# Read frame and process
success, frame = cap.read()
if not success:
print("Erreur: Impossible de lire le flux vidéo. Libération des ressources.")
status = 0 # Force reset
continue
# declare start timing variable
if args.verbose:
time_start = time.time()
# Run YOLO tracking on the frame
results = model.track(frame, persist=True, classes=[0], tracker="track/botsort.yaml", verbose=False)
annotated_frame = results[0].plot()
# declare mid timing variable
if args.verbose:
time_mid = time.time()
new_people = set()
tasks = []
pos_tasks = []
if results[0].boxes.id is not None:
boxes = results[0].boxes.xywh.cpu()
track_ids = results[0].boxes.id.int().cpu().tolist()
for box, track_id in zip(boxes, track_ids):
x, y, w, h = box
track = track_history[track_id]
track.append((float(x), float(y)))
if len(track) > 30:
track.pop(0)
new_people.add(track_id)
# Detect entries and exits
entered = new_people - current_people
exited = current_people - new_people
# Send event to API for each ID
if args.API_URL and status == 1:
for person_id in entered:
tasks.append(send_event(session, "enter", len(new_people), person_id, args.verbose))
for person_id in exited:
tasks.append(send_event(session, "exit", len(new_people), person_id, args.verbose))
# Send position to API for each ID
if args.API_URL and status == 1:
if int(time.time()) >= (last_position_sent+1):
payload_positions = []
height_img, width_img = annotated_frame.shape[:2]
boxes_n = results[0].boxes.xywh.cpu()
for box, track_id in zip(boxes_n, track_ids):
x, y, w, h = [float(coord.item()) for coord in box]
x = (x-(w/2)) / width_img
y = (y-(h/2)) / height_img
w = w / width_img
h = h / height_img
payload_positions.append([{"pos_x": round(x, 4), "pos_y": round(y, 4), "w": round(w, 4), "h": round(h, 4)}, {"id": track_id}])
last_position_sent = int(time.time())
pos_tasks.append(send_position(session, payload_positions, args.verbose))
current_people = new_people
else:
# No people detected, send exit event for all remaining
if current_people and status == 1:
if args.API_URL:
for person_id in current_people:
tasks.append(send_event(session, "exit", 0, person_id, args.verbose))
current_people = set()
# Await all API calls concurrently
if tasks:
await asyncio.gather(*tasks)
if pos_tasks:
await asyncio.gather(*pos_tasks)
# Math FPS
if args.verbose:
time_end = time.time()
fps_display = int(1 / (time_end - prev_time_fps))
prev_time_fps = time_end
# Math MS
time_mid_temp = round((time_mid - time_start)*1000 , 1)
time_end_temp = round((time_end - time_mid)*1000 , 1)
time_mid_list.append(time_mid_temp)
time_end_list.append(time_end_temp)
fps_list.append(fps_display)
# Print iteration measurements
print(f"Temps de réponse détection/suivi: {time_mid_temp}\nTemps de réponse dessin: {time_end_temp}\nTemps de travail total: {round(time_mid_temp + time_end_temp, 4)}\nImages par seconde: {fps_display}\n-------------------------")
# Prepare OpenCV to print
cv2.putText(annotated_frame, f"Nombre visiteurs: {len(current_people)}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
cv2.imshow("Vision par ordinateur - Python", annotated_frame)
if args.output:
video_writer.write(annotated_frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
if cap:
cap.release()
if video_writer:
video_writer.release()
cv2.destroyAllWindows()
break
# Run the async main loop
asyncio.run(main_loop())
# Print Summary
if args.verbose and time_mid_list:
print(f"Rapport de performances:\nTemps de réponse moyen détection/suivi: {round(sum(time_mid_list) / len(time_mid_list), 1)}\nTemps de réponse moyen dessin: {round(sum(time_end_list) / len(time_end_list), 1)}\nTemps de réponse moyen: {round((sum(time_mid_list) / len(time_mid_list)) + (sum(time_end_list) / len(time_end_list)), 1)}\nMoyenne images par seconde: {round(sum(fps_list) / len(fps_list), 2)}")

BIN
models/yolo11n.pt Normal file

Binary file not shown.

BIN
models/yolo12n.pt Normal file

Binary file not shown.

62
requirements.txt Normal file
View File

@@ -0,0 +1,62 @@
aiohappyeyeballs==2.6.1
aiohttp==3.12.7
aiosignal==1.3.2
attrs==25.3.0
certifi==2025.4.26
charset-normalizer==3.4.2
contourpy==1.3.2
cycler==0.12.1
filelock==3.18.0
fonttools==4.58.1
frozenlist==1.6.0
fsspec==2025.5.1
idna==3.10
Jinja2==3.1.6
kiwisolver==1.4.8
lap==0.5.12
MarkupSafe==3.0.2
matplotlib==3.10.3
mpmath==1.3.0
multidict==6.4.4
networkx==3.5
numpy==2.2.6
nvidia-cublas-cu12==12.6.4.1
nvidia-cuda-cupti-cu12==12.6.80
nvidia-cuda-nvrtc-cu12==12.6.77
nvidia-cuda-runtime-cu12==12.6.77
nvidia-cudnn-cu12==9.5.1.17
nvidia-cufft-cu12==11.3.0.4
nvidia-cufile-cu12==1.11.1.6
nvidia-curand-cu12==10.3.7.77
nvidia-cusolver-cu12==11.7.1.2
nvidia-cusparse-cu12==12.5.4.2
nvidia-cusparselt-cu12==0.6.3
nvidia-nccl-cu12==2.26.2
nvidia-nvjitlink-cu12==12.6.85
nvidia-nvtx-cu12==12.6.77
opencv-python==4.11.0.86
packaging==25.0
pandas==2.2.3
pillow==11.2.1
propcache==0.3.1
psutil==7.0.0
py-cpuinfo==9.0.0
pyparsing==3.2.3
python-dateutil==2.9.0.post0
pytz==2025.2
PyYAML==6.0.2
requests==2.32.3
scipy==1.15.3
setuptools==80.9.0
six==1.17.0
sympy==1.14.0
torch==2.7.0
torchvision==0.22.0
tqdm==4.67.1
triton==3.3.0
typing_extensions==4.14.0
tzdata==2025.2
ultralytics==8.3.148
ultralytics-thop==2.0.14
urllib3==2.4.0
yarl==1.20.0

21
track/botsort.yaml Normal file
View File

@@ -0,0 +1,21 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Default Ultralytics settings for BoT-SORT tracker when using mode="track"
# For documentation and examples see https://docs.ultralytics.com/modes/track/
# For BoT-SORT source code see https://github.com/NirAharon/BoT-SORT
tracker_type: botsort # tracker type, ['botsort', 'bytetrack']
track_high_thresh: 0.25 # threshold for the first association
track_low_thresh: 0.1 # threshold for the second association
new_track_thresh: 0.25 # threshold for init new track if the detection does not match any tracks
track_buffer: 30 # buffer to calculate the time when to remove tracks
match_thresh: 0.8 # threshold for matching tracks
fuse_score: True # Whether to fuse confidence scores with the iou distances before matching
# min_box_area: 10 # threshold for min box areas(for tracker evaluation, not used for now)
# BoT-SORT settings
gmc_method: sparseOptFlow # method of global motion compensation
# ReID model related thresh (not supported yet)
proximity_thresh: 0.5
appearance_thresh: 0.25
with_reid: False

14
track/bytetrack.yaml Normal file
View File

@@ -0,0 +1,14 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Default Ultralytics settings for ByteTrack tracker when using mode="track"
# For documentation and examples see https://docs.ultralytics.com/modes/track/
# For ByteTrack source code see https://github.com/ifzhang/ByteTrack
tracker_type: bytetrack # tracker type, ['botsort', 'bytetrack']
track_high_thresh: 0.25 # threshold for the first association
track_low_thresh: 0.1 # threshold for the second association
new_track_thresh: 0.25 # threshold for init new track if the detection does not match any tracks
track_buffer: 30 # buffer to calculate the time when to remove tracks
match_thresh: 0.8 # threshold for matching tracks
fuse_score: True # Whether to fuse confidence scores with the iou distances before matching
# min_box_area: 10 # threshold for min box areas(for tracker evaluation, not used for now)