-
-
Notifications
You must be signed in to change notification settings - Fork 10
Introduces Occlusion masks table and related endpoints #533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
58 commits
Select commit
Hold shift + click to select a range
cd61d64
Introduced poses and its fk (nullable before migration)
fe51 1faf81e
Created poses schema
fe51 ac1119c
Updated camaras and detections schemas
fe51 c6e081d
Created crud_pose
fe51 737fa0b
Updated crud Init.py
fe51 152d596
updated api router and dependencies
fe51 8773cc3
Created endpoints for poses
fe51 88d3e7f
updated endpoints detections
fe51 c7e7005
Updated camera endpoints
fe51 889c9f6
Updated conftest
fe51 013cb7f
Added tests for poses
fe51 e181849
Updates detections and cameras tests
fe51 fe7df71
Updated client
fe51 f3ec020
Updates db diagram
fe51 8377f01
update test end to end
fe51 c243e2a
mypy fixes
fe51 293c730
fix codacy
fe51 03e2c01
fix mypy
fe51 c38e738
fix codacy and mypy with more explicit import
fe51 693b4e1
quick fix test client
fe51 729b4b6
revert client updates
fe51 99a3ef6
Added Occlusion Mask table in model
fe51 5d8a2be
Added occlusion mask crud and schema
fe51 400f0a8
Added occlusion mask endpoint
fe51 db5fd1a
added occlusion mask to router and dependencies
fe51 b11bd73
Update poses endpoint to list occlusion mask from a pose
fe51 254aba7
Update tests and e2e tests
fe51 9828392
updates db diagram
fe51 8db4b5d
fix mypy
fe51 de99b2d
Revert "fix mypy"
fe51 8cf3d46
mypy
fe51 6d55aab
fix typos db diagram docs
fe51 4cb14ff
updates patrol_id type from str to int
fe51 7a0e806
updated tests patrol_id str to int
fe51 4e22cec
style
fe51 834cd8c
fix to respect code convention
fe51 02ff40c
wip updates client
fe51 4718795
Merge branch 'main' into poses
fe51 6c7d46f
typo
fe51 1feee8f
Specify None
fe51 bbcb127
optional pose id
fe51 e98ab4d
Merge branch 'poses' into occlusion_masks
fe51 4c207ec
Merge branch 'main' into poses
fe51 588ec27
resolve merge conflict merging main
fe51 d5d0753
client: add pose_id param optionnal in create_detection
fe51 af955a8
Merge branch 'poses' into occlusion_masks
fe51 b5f1b36
Merge branch 'main' into occlusion_masks
fe51 2d7aa51
typo
fe51 d5edfb7
ruff
MateoLostanlen bae4dea
Added scope check
fe51 d8f4365
added get occlusion mask route
fe51 457ea2a
delete conf in occlusion mask format
fe51 02c15e5
Updates tests
fe51 e6e7037
Updates client and tests
fe51 ac1d806
Merge branch 'main' into occlusion_masks
fe51 cdf9722
headers
fe51 4c7e88e
fix headers
fe51 7d5074d
fix header
fe51 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| # Copyright (C) 2025-2026, Pyronear. | ||
|
|
||
| # This program is licensed under the Apache License 2.0. | ||
| # See LICENSE or go to <https://www.apache.org/licenses/LICENSE-2.0> for full license details. | ||
| import re | ||
| from typing import cast | ||
|
|
||
| from fastapi import APIRouter, Body, Depends, HTTPException, Path, Security, status | ||
|
|
||
| from app.api.dependencies import ( | ||
| get_camera_crud, | ||
| get_jwt, | ||
| get_occlusion_mask_crud, | ||
| get_pose_crud, | ||
| ) | ||
| from app.crud import CameraCRUD | ||
| from app.crud.crud_occlusion_mask import OcclusionMaskCRUD | ||
| from app.crud.crud_pose import PoseCRUD | ||
| from app.models import Camera, OcclusionMask, Pose, UserRole | ||
| from app.schemas.login import TokenPayload | ||
| from app.schemas.occlusion_masks import ( | ||
| OcclusionMaskCreate, | ||
| OcclusionMaskRead, | ||
| OcclusionMaskUpdate, | ||
| ) | ||
| from app.services.telemetry import telemetry_client | ||
|
|
||
| router = APIRouter() | ||
|
|
||
| FLOAT_PATTERN = r"(0?\.[0-9]{1,3}|0|1)" | ||
| MASK_PATTERN = rf"^\({FLOAT_PATTERN},{FLOAT_PATTERN},{FLOAT_PATTERN},{FLOAT_PATTERN}\)$" | ||
| mask_regex = re.compile(MASK_PATTERN) | ||
|
|
||
|
|
||
| def validate_mask(mask: str) -> None: | ||
| if not mask_regex.match(mask): | ||
| raise HTTPException( | ||
| status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, | ||
| detail=("Invalid mask format. Expected: (xmin, ymin, xmax, ymax) with float values in [0,1]."), | ||
| ) | ||
|
|
||
|
|
||
| @router.post( | ||
| "/", | ||
| status_code=status.HTTP_201_CREATED, | ||
| summary="Create an occlusion mask", | ||
| ) | ||
| async def create_mask( | ||
| payload: OcclusionMaskCreate = Body(...), | ||
| poses: PoseCRUD = Depends(get_pose_crud), | ||
| cameras: CameraCRUD = Depends(get_camera_crud), | ||
| masks: OcclusionMaskCRUD = Depends(get_occlusion_mask_crud), | ||
| token_payload: TokenPayload = Security( | ||
| get_jwt, | ||
| scopes=[UserRole.ADMIN, UserRole.AGENT], | ||
| ), | ||
| ) -> OcclusionMaskRead: | ||
| # Validate mask format | ||
| validate_mask(payload.mask) | ||
|
|
||
| pose = cast(Pose, await poses.get(payload.pose_id, strict=True)) | ||
| camera = cast(Camera, await cameras.get(pose.camera_id, strict=True)) | ||
|
|
||
| if UserRole.ADMIN not in token_payload.scopes and token_payload.organization_id != camera.organization_id: | ||
| raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access forbidden.") | ||
|
|
||
| telemetry_client.capture( | ||
| token_payload.sub, | ||
| event="occlusion_masks-create", | ||
| properties={"pose_id": payload.pose_id}, | ||
| ) | ||
|
|
||
| db_obj = await masks.create(payload) | ||
| return OcclusionMaskRead(**db_obj.model_dump()) | ||
|
|
||
|
|
||
| @router.get( | ||
| "/{mask_id}", | ||
| status_code=status.HTTP_200_OK, | ||
| summary="Get info about an occlusion mask", | ||
| ) | ||
| async def get_mask( | ||
| mask_id: int = Path(..., gt=0), | ||
| masks: OcclusionMaskCRUD = Depends(get_occlusion_mask_crud), | ||
| poses: PoseCRUD = Depends(get_pose_crud), | ||
| cameras: CameraCRUD = Depends(get_camera_crud), | ||
| token_payload: TokenPayload = Security( | ||
| get_jwt, | ||
| scopes=[UserRole.ADMIN, UserRole.AGENT], | ||
| ), | ||
| ) -> OcclusionMaskRead: | ||
| mask = cast(OcclusionMask, await masks.get(mask_id, strict=True)) | ||
| pose = cast(Pose, await poses.get(mask.pose_id, strict=True)) | ||
| camera = cast(Camera, await cameras.get(pose.camera_id, strict=True)) | ||
|
|
||
| if UserRole.ADMIN not in token_payload.scopes and token_payload.organization_id != camera.organization_id: | ||
| raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access forbidden.") | ||
|
|
||
| telemetry_client.capture( | ||
| token_payload.sub, | ||
| event="occlusion_masks-get", | ||
| properties={"mask_id": mask_id}, | ||
| ) | ||
|
|
||
| return OcclusionMaskRead(**mask.model_dump()) | ||
|
|
||
|
|
||
| @router.patch( | ||
| "/{mask_id}", | ||
| status_code=status.HTTP_200_OK, | ||
| summary="Update an occlusion mask", | ||
| ) | ||
| async def update_mask( | ||
| mask_id: int = Path(..., gt=0), | ||
| payload: OcclusionMaskUpdate = Body(...), | ||
| masks: OcclusionMaskCRUD = Depends(get_occlusion_mask_crud), | ||
| poses: PoseCRUD = Depends(get_pose_crud), | ||
| cameras: CameraCRUD = Depends(get_camera_crud), | ||
| token_payload: TokenPayload = Security( | ||
| get_jwt, | ||
| scopes=[UserRole.ADMIN, UserRole.AGENT], | ||
| ), | ||
| ) -> OcclusionMaskRead: | ||
| # Validate mask format | ||
| validate_mask(payload.mask) | ||
|
|
||
| mask = cast(OcclusionMask, await masks.get(mask_id, strict=True)) | ||
| pose = cast(Pose, await poses.get(mask.pose_id, strict=True)) | ||
| camera = cast(Camera, await cameras.get(pose.camera_id, strict=True)) | ||
|
|
||
| if UserRole.ADMIN not in token_payload.scopes and token_payload.organization_id != camera.organization_id: | ||
| raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access forbidden.") | ||
|
|
||
| telemetry_client.capture( | ||
| token_payload.sub, | ||
| event="occlusion_masks-update", | ||
| properties={"mask_id": mask_id}, | ||
| ) | ||
|
|
||
| db_obj = await masks.update(mask_id, payload) | ||
| return OcclusionMaskRead(**db_obj.model_dump()) | ||
|
|
||
|
|
||
| @router.delete( | ||
| "/{mask_id}", | ||
| status_code=status.HTTP_200_OK, | ||
| summary="Delete an occlusion mask", | ||
| ) | ||
| async def delete_mask( | ||
| mask_id: int = Path(..., gt=0), | ||
| masks: OcclusionMaskCRUD = Depends(get_occlusion_mask_crud), | ||
| poses: PoseCRUD = Depends(get_pose_crud), | ||
| cameras: CameraCRUD = Depends(get_camera_crud), | ||
| token_payload: TokenPayload = Security( | ||
| get_jwt, | ||
| scopes=[UserRole.ADMIN, UserRole.AGENT], | ||
| ), | ||
| ) -> None: | ||
| mask = cast(OcclusionMask, await masks.get(mask_id, strict=True)) | ||
| pose = cast(Pose, await poses.get(mask.pose_id, strict=True)) | ||
| camera = cast(Camera, await cameras.get(pose.camera_id, strict=True)) | ||
|
|
||
| if UserRole.ADMIN not in token_payload.scopes and token_payload.organization_id != camera.organization_id: | ||
| raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access forbidden.") | ||
|
|
||
| telemetry_client.capture( | ||
| token_payload.sub, | ||
| event="occlusion_masks-delete", | ||
| properties={"mask_id": mask_id}, | ||
| ) | ||
|
|
||
| await masks.delete(mask_id) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.