diff --git a/.gitignore b/.gitignore index de88a2b3adcdd604db49c8ff544b4427e4f935e9..a17152a7e7ce9d3f563aa4472d25aa817a7d29ef 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ node_modules/ build/ env/ .env -__pycache__ \ No newline at end of file +__pycache__ +cameras.py \ No newline at end of file diff --git a/README.md b/README.md index 7ab3905e020cdd5faa01bc6f7e9d5f5fbd07dd7e..c8107378dada63bf4e092718fbe13b49905a3a2f 100644 --- a/README.md +++ b/README.md @@ -46,13 +46,9 @@ Navigate to [http://localhost:3000](http://localhost:3000) # TODO ## Coté algo -- Faire tournez le script de comptage que pendant les créneaux d'ouvertures du RU associé -- Mettre en place le script pour compter les caisses ouvertes -- Mettre en place le couplage des caméras (implique de pouvoir définir les masques proprement) dans le script de comptage de personnes - Accéder à d'autre infos telle que l'API des cours sur demande à la DISI pour intégrer ça aux prédictions (ex: cours en promo complète juste implique plus d'attente) ## Coté dev -- Améliorer l'interface utilisateur avec le timer - Protéger l'interface OpenAPI et mettre en place une interface admin pour les news et potentiellement modération (avec authentification) - Permettre de définir les masques proprement et de manière à pouvoir généraliser à d'autre RU - Accorder la charte graphique si le service est intégré à d'autres appli (bordeau/blanc service de CS, charte graphique de VR) diff --git a/backend/cameras.py.template b/backend/cameras.py.template new file mode 100644 index 0000000000000000000000000000000000000000..5d197e572134b2e9d63518bcbcd5a227ee42bde0 --- /dev/null +++ b/backend/cameras.py.template @@ -0,0 +1,43 @@ +restaurants = [ + { + "restaurant": "local", + "a_factor": 30, + "b_factor": 120, + "cameras": + [ + { + "IP": "", + "user": "", + "password": "", + "stream": "stream1", + "mask_points": + [ + [ + [70, 370], + [420, 720], + [1280, 720], + [1280, 250], + [930, 215], + [450, 550], + [130, 350] + ] + ], + "caisses": + [ + { + "x1": 380, + "x2": 435, + "y1": 740, + "y2": 780 + }, + { + "x1": 300, + "x2": 350, + "y1": 830, + "y2": 880 + } + ] + } + ] + } +] diff --git a/backend/db/crud.py b/backend/db/crud.py index 64a1a8fcbd5950eaf1b41c43d368537466ae1655..853195a6872997706fa95c64a7d6d4b9ba37a8fb 100644 --- a/backend/db/crud.py +++ b/backend/db/crud.py @@ -505,7 +505,6 @@ def update_collaborative_record(user: schemas.User, db: Session): if last_record.date >= datetime.combine(date, time_slot.open_time) and not last_record.waiting_time: last_record.waiting_time = current_date - \ pytz.timezone("Europe/Paris").localize(last_record.date) - print(last_record.waiting_time) db.add(last_record) db.commit() db.refresh(last_record) @@ -514,6 +513,12 @@ def update_collaborative_record(user: schemas.User, db: Session): raise HTTPException(status_code=406, detail="Client already registered") +def cancel_collaborative_record(user_id: int, db: Session): + db.query(models.CollaborativeRecords).filter(models.CollaborativeRecords.user_id == user_id).delete() + db.commit() + return + + def delete_collaborative_record(id: int, db: Session): if id == 0: db.query(models.CollaborativeRecords).delete() diff --git a/backend/db/models.py b/backend/db/models.py index 2f719cd94ba667400bb233ca1d3d73562ba99d04..1e6ad00d5a1b93a377d7a48501f89194ffa0f781 100644 --- a/backend/db/models.py +++ b/backend/db/models.py @@ -12,7 +12,7 @@ class CollaborativeRecords(Base): __tablename__ = "collection" id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id")) + user_id = Column(Integer, ForeignKey("users.id", ondelete='SET NULL')) place = Column(String(30)) date = Column(DateTime) waiting_time = Column(Interval) diff --git a/backend/db/schemas.py b/backend/db/schemas.py index 2042101f98e167167989f2ea0d52e7d918163951..757bfbd0dd438b3a0b7252fc301f97400a155621 100644 --- a/backend/db/schemas.py +++ b/backend/db/schemas.py @@ -33,7 +33,7 @@ class RecordRead(BaseModel): class CollaborativeRecords(BaseModel): """CollaborativeRecords schema""" - user_id: int = Field(..., title="Id of the user timed") + user_id: Optional[int] = Field(default=None, title="Id of the user timed") place: str = Field(..., title="Name of the RU corresponding the given record") date: datetime = Field(..., title="Date of the record") waiting_time: Optional[timedelta] = Field(default=None, title="Caculated waiting time for timed person") diff --git a/backend/main.py b/backend/main.py index 8a5b83204da713064c31881b4c61c5e3b3590164..82239f43f180564d4e73c65a4260c755e2aecf07 100644 --- a/backend/main.py +++ b/backend/main.py @@ -6,6 +6,7 @@ from fastapi.openapi.utils import get_openapi from sqlalchemy.orm import Session from dotenv import load_dotenv from threading import Thread +from asyncio import run import os from db import database, models, crud @@ -35,7 +36,7 @@ app.add_middleware( async def on_startup(): # Database creation models.Base.metadata.create_all(bind=database.engine) - t = Thread(target=handle_cameras) + t = Thread(target=run, args=(handle_cameras(),)) t.start() diff --git a/backend/routers/records.py b/backend/routers/records.py index f6f603ac15e7bf7a8b87f5f4ee881f27edafc86f..04e4444b446a51a7b491fbd803a5b91c8385e819 100644 --- a/backend/routers/records.py +++ b/backend/routers/records.py @@ -54,6 +54,16 @@ async def end_new_record(response: Response, connect_id: str = Cookie(...), db: return db_record +@router.post("/collection/cancel", response_model=None, tags=["data collection"]) +async def cancel_new_record(response: Response, connect_id: str = Cookie(...), db: Session = Depends(get_db)): + user = crud.get_user(connect_id, db) + db_record = crud.cancel_collaborative_record(user.id, db) + if not user.username: + crud.delete_user(user.cookie, db) + response.delete_cookie(key="connect_id") + return db_record + + @router.delete('/collection', response_model=None, tags=["data collection"]) async def delete_record(id: int, db: Session = Depends(get_db)): return crud.delete_collaborative_record(id, db) diff --git a/backend/video_capture.py b/backend/video_capture.py index 9806146f1aad3258b8e71d350cf91b4c6093d04f..08cf8533470937c824551fdf2c7e28441e7858e8 100644 --- a/backend/video_capture.py +++ b/backend/video_capture.py @@ -3,75 +3,76 @@ from datetime import datetime, timedelta import numpy as np import keras from utils.preprocessing import fix_singular_shape, norm_by_imagenet -from dotenv import load_dotenv import json -import os +import time +from cameras import restaurants from db import models from db.database import SessionLocal from routers.websocket import manager -def handle_cameras(): +async def handle_cameras(): model = keras.models.load_model('assets', compile=False) db = SessionLocal() - load_dotenv() - camera_number = int(os.getenv('CAM_NUMBER')) - cameras = [] - for i in range(camera_number): - camera = {} - camera["place"] = os.getenv(f'CAM_{i}_PLACE') - camera["IP"] = os.getenv(f'CAM_{i}_IP') - camera["user"] = os.getenv(f'CAM_{i}_USER') - camera["password"] = os.getenv(f'CAM_{i}_PASSWORD') - camera["stream"] = os.getenv(f'CAM_{i}_STREAM') - camera["a_factor"] = int(os.getenv(f'CAM_{i}_A_FACTOR')) - camera["b_factor"] = int(os.getenv(f'CAM_{i}_B_FACTOR')) - camera["framegap"] = int(os.getenv(f'CAM_{i}_FRAMEGAP')) - camera["count"] = 0 - camera["cap"] = cv2.VideoCapture( - f'rtsp://{camera["user"]}:{camera["password"]}@{camera["IP"]}/{camera["stream"]}') - mask_length = int(os.getenv(f'CAM_{i}_POINTS_NB')) - mask_points = [] - for j in range(mask_length): - point = os.getenv(f'CAM_{i}_POINT_{j}') - mask_points.append(list(map(int, point.split(',')))) - mask = np.zeros((720, 1280, 3), dtype=np.float32) - cv2.fillPoly(mask, [np.array(mask_points)], (255, 255, 255)) - camera["mask"] = mask - cameras.append(camera) + for restaurant in restaurants: + for camera in restaurant["cameras"]: + mask = np.zeros((720, 1280, 3), dtype=np.float32) + cv2.fillPoly(mask, np.array(camera["mask_points"]), (255, 255, 255)) + camera["mask"] = mask while True: - for camera in cameras: - if camera['cap'].isOpened(): - ret, frame = camera['cap'].read() - if ret and camera['count'] % camera['framegap'] == 0: - current_time = datetime.now() - masked_img = cv2.bitwise_and( - frame.astype(np.float32), camera["mask"]) - treated_img = fix_singular_shape(masked_img, 16) - input_image = np.expand_dims( - np.squeeze( - norm_by_imagenet( - np.array( - [treated_img]))), - axis=0) - pred_map = np.squeeze(model.predict(input_image)) - count_prediction = np.sum(pred_map) + + current_date = datetime.now() + weekday, current_time = current_date.weekday(), current_date.time() + + for restaurant in restaurants: + + is_open = db.query( + models.OpeningHours).filter( + models.OpeningHours.place == restaurant["restaurant"], + models.OpeningHours.day == weekday, + models.OpeningHours.open_time <= current_time, + models.OpeningHours.close_time >= current_time).first() is not None + + if is_open: + count_prediction = 0 + open_checkouts = 0 + cams_working = True + + for camera in restaurant["cameras"]: + cap = cv2.VideoCapture(f'rtsp://{camera["user"]}:{camera["password"]}@{camera["IP"]}/{camera["stream"]}') + if cams_working and cap.isOpened(): + _, frame = cap.read() + masked_img = cv2.bitwise_and( + frame.astype(np.float32), camera["mask"]) + treated_img = fix_singular_shape(masked_img, 16) + input_image = np.expand_dims( + np.squeeze( + norm_by_imagenet( + np.array( + [treated_img]))), + axis=0) + pred_map = np.squeeze(model.predict(input_image, verbose=0)) + count_prediction += np.sum(pred_map) + for caisse in camera["caisses"]: + if np.sum(pred_map[caisse["x1"] // 2:caisse["x2"] // 2, caisse["y1"] // 2:caisse["y2"] // 2]) > 0.5: + open_checkouts += 1 + else: + cams_working = False + cap.release() + + if cams_working and open_checkouts: waiting_time = timedelta( - seconds=camera['b_factor'] + - int(count_prediction) * - camera['a_factor']) + seconds=restaurant['b_factor'] + + int(count_prediction * + restaurant['a_factor'] / open_checkouts)) db_record = models.Records( - place=camera['place'], - date=current_time, + place=restaurant['restaurant'], + date=current_date, density=int(count_prediction), waiting_time=waiting_time) db.add(db_record) db.commit() - manager.broadcast(json.dumps({"type": "data"})) - camera['count'] += 1 - else: - camera["cap"] = cv2.VideoCapture( - f"rtsp://{camera['user']}:{camera['password']}@{camera['IP']}/{camera['stream']}") - print("tentative de reconnexion") + await manager.broadcast(json.dumps({"type": "data"})) + time.sleep(max(0, 60 - (datetime.now() - current_date).total_seconds())) diff --git a/frontend/src/styles/restaurant.css b/frontend/src/styles/restaurant.css index 639835729cd42edf8daed25f00d902c2948d236b..45fe9e89524ecd4a49310a82210501e428f180bb 100644 --- a/frontend/src/styles/restaurant.css +++ b/frontend/src/styles/restaurant.css @@ -12,43 +12,19 @@ overflow: hidden; } -#restaurant-start-button { - width: fit-content; - border: none; - border-radius: 5px; - padding: 0.1rem; - padding-left: 0.3rem; - padding-right: 0.3rem; - margin-top: 2rem; -} - -.restaurant-button-disabled { - background-color: rgb(66, 75, 83); - color: #b9b9b9; -} - -.restaurant-button-active { +.restaurant-button { background-color: rgb(33, 37, 41); color: white; -} - -.restaurant-button-active:hover { - box-shadow: 0px 0px 5px white; -} - -#restaurant-end-button { - width: fit-content; - background-color: #83000d; - color: white; + width: 7rem; border: none; border-radius: 5px; - padding: 0.1rem; + padding: 0.2rem; padding-left: 0.3rem; padding-right: 0.3rem; margin-top: 2rem; } -#restaurant-end-button:hover { +.restaurant-button:hover { box-shadow: 0px 0px 5px rgb(33, 37, 41); } diff --git a/frontend/src/views/Restaurant.js b/frontend/src/views/Restaurant.js index f4113205874ed501e30af255efe95cdf3a7a884a..fa5701876ad200f5998d841408938c5bfabc996a 100644 --- a/frontend/src/views/Restaurant.js +++ b/frontend/src/views/Restaurant.js @@ -12,7 +12,7 @@ import "../styles/restaurant.css"; export default function RestaurantPage({ selection, lastMessage }) { // const [started, setStarted] = useState(false); - // const [disabled, setDisabled] = useState(false); + // const [timed, setTimed] = useState(false); // const Start = () => { // instance @@ -21,7 +21,7 @@ export default function RestaurantPage({ selection, lastMessage }) { // setStarted(true); // }) // .catch((e) => { - // setDisabled(true); + // setTimed(true); // alert( // "Il semblerait que tu aies déjà renseigné un temps d'attente sur ce créneau. Merci de ta participation, n'hésite pas à te chronométrer de nouveau la prochaine fois !", // ); @@ -34,10 +34,10 @@ export default function RestaurantPage({ selection, lastMessage }) { // .post("collection/stop") // .then(() => { // setStarted(false); - // setDisabled(true); + // setTimed(true); // }) // .catch((e) => { - // setDisabled(true); + // setTimed(true); // alert( // "Il semblerait que tu aies déjà renseigné un temps d'attente sur ce créneau. Merci de ta participation, n'hésite pas à te chronométrer de nouveau la prochaine fois !", // ); @@ -45,6 +45,20 @@ export default function RestaurantPage({ selection, lastMessage }) { // }); // }; + // const Cancel = () => { + // instance + // .post("collection/cancel") + // .then(() => { + // setStarted(false); + // setTimed(false); + // }) + // .catch((e) => { + // setStarted(false); + // setTimed(false); + // console.log(e); + // }); + // }; + return ( <> {selection && ( @@ -52,18 +66,26 @@ export default function RestaurantPage({ selection, lastMessage }) { <Comments place={selection.name} lastMessage={lastMessage} infos /> <div className="restaurant-container" id="restaurant-main-page"> <WaitingTime place={selection.name} lastMessage={lastMessage} /> - <Graph place={selection.name} type="current" lastMessage={lastMessage} /> - {/* <button - id={`restaurant-${started ? "end" : "start"}-button`} - onClick={started ? End : Start} - className={disabled ? "restaurant-button-disabled" : "restaurant-button-active"} - disabled={disabled} - > - {started ? "Fini !!" : "Départ !!"} - </button> */} + <Graph place={selection.name} lastMessage={lastMessage} /> + {/* <div style={{ fontSize: "1.2rem", paddingLeft: "10%", paddingRight: "10%" }}> + {timed + ? "Merci de ta participation, n'hésite pas à te chronométrer de nouveau la prochaine fois !" + : "Aide nous à récupérer des données en te chronométrant dans la queue du self !"} + </div> + {!timed && ( + <> + <button onClick={started ? End : Start} className="restaurant-button"> + {started ? "Fini !" : "Démarrer !"} + </button> + {started && ( + <button onClick={Cancel} className="restaurant-button"> + Annuler + </button> + )} + </> + )} */} </div> <Comments place={selection.name} lastMessage={lastMessage} /> - {/*<Graph place={selection.name} type="avg" />*/} </div> )} </> diff --git a/photo_camera/1er_etage_milieu.jpg b/photo_camera/1er_etage_milieu.jpg new file mode 100644 index 0000000000000000000000000000000000000000..df32ba4b2d5d01b9c87b482cb41dceae006c36da Binary files /dev/null and b/photo_camera/1er_etage_milieu.jpg differ diff --git a/photo_camera/1er_etage_milieu_2.jpg b/photo_camera/1er_etage_milieu_2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..68613e1e5ae106df19af90c63fc98f05dba30c1a Binary files /dev/null and b/photo_camera/1er_etage_milieu_2.jpg differ diff --git a/photo_camera/1er_etage_milieu_retourne.jpg b/photo_camera/1er_etage_milieu_retourne.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4d93f4c1c17cb1d2de91bda94bd8ebecf286aa02 Binary files /dev/null and b/photo_camera/1er_etage_milieu_retourne.jpg differ diff --git a/photo_camera/2eme_etage_angle.jpg b/photo_camera/2eme_etage_angle.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bc7e04e4d12c009e0229d24004bd2c44cd4a027b Binary files /dev/null and b/photo_camera/2eme_etage_angle.jpg differ diff --git a/photo_camera/2eme_etage_milieu.jpg b/photo_camera/2eme_etage_milieu.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2963d2f25080038a2e92fd8cbc302edd542a7b30 Binary files /dev/null and b/photo_camera/2eme_etage_milieu.jpg differ diff --git a/photo_camera/2eme_etage_milieu_2.jpg b/photo_camera/2eme_etage_milieu_2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..397477651cd0d370e737a5ed47012d28257fcf17 Binary files /dev/null and b/photo_camera/2eme_etage_milieu_2.jpg differ