Skip to content

Commit 813409e

Browse files
authored
Camera CRUDs, Area Gets, Refactor of endpoint roots (#11)
1 parent 6b17b4f commit 813409e

File tree

4 files changed

+162
-42
lines changed

4 files changed

+162
-42
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ docker run -it -p HOST_PORT:8000 --rm neuralet/smart-social-distancing:latest-we
116116

117117
#### Processor
118118

119+
Please note that in order to correctly visualize the processor in the frontend. The `HOST_PORT` used to run the **Processor** should be the same than the one stored in `config-frontend.ini
120+
119121
##### Optional Parameters
120122
This is a list of optional parameters for the `docker run` commands.
121123
They are included in the examples of this section.

api/processor_api.py

Lines changed: 157 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -199,17 +199,22 @@ def map_to_config_file_format(config_dto):
199199
config_dict["Area_" + str(count)] = map_to_area_file_format(area)
200200
return config_dict
201201

202-
def extract_config():
202+
def extract_config(config_type='all'):
203203
sections = self.config.get_sections()
204+
if config_type == 'cameras':
205+
sections = [x for x in sections if x.startswith("Source")]
206+
elif config_type == 'areas':
207+
sections = [x for x in sections if x.startswith("Area")]
204208
config = {}
209+
205210
for section in sections:
206211
config[section] = self.config.get_section_dict(section)
207212
return config
208213

209214
def verify_path(base, camera_id):
210215
dir_path = os.path.join(base, camera_id)
211216
if not os.path.exists(dir_path):
212-
raise HTTPException(status_code=404, detail=f'Camera with id "{camera_id}" does not exist')
217+
raise HTTPException(status_code=404, detail=f'The camera: {camera_id} does not exist')
213218
return dir_path
214219

215220
def update_config_file(config_dict):
@@ -239,8 +244,7 @@ def enable_slack(token_config):
239244
logger.info("Enabling slack notification on processor's config")
240245
config_dict = dict()
241246
config_dict["App"] = dict({"EnableSlackNotifications": "yes", "SlackChannel": token_config.channel})
242-
self.config.update_config(config_dict)
243-
success = restart_processor()
247+
success = update_and_restart_config(config_dict)
244248

245249
return handle_config_response(config_dict, success)
246250

@@ -257,9 +261,8 @@ def add_slack_channel_to_config(channel):
257261
logger.info("Adding slack's channel on processor's config")
258262
config_dict = dict()
259263
config_dict["App"] = dict({"SlackChannel": channel})
260-
self.config.update_config(config_dict)
261-
success = restart_processor()
262264

265+
success = update_and_restart_config(config_dict)
263266
return handle_config_response(config_dict, success)
264267

265268
def handle_config_response(config, success):
@@ -274,6 +277,57 @@ def handle_config_response(config, success):
274277
)
275278
return JSONResponse(content=humps.decamelize(config))
276279

280+
def get_areas():
281+
config = extract_config(config_type='areas')
282+
return [map_area(x, config) for x in config.keys()]
283+
284+
def reestructure_areas(config_dict):
285+
"""Ensure that all [Area_0, Area_1, ...] are consecutive"""
286+
area_names = [x for x in config_dict.keys() if x.startswith("Area")]
287+
area_names.sort()
288+
for index, area_name in enumerate(area_names):
289+
if f'Area_{index}' != area_name:
290+
config_dict[f'Area_{index}'] = config_dict[area_name]
291+
config_dict.pop(area_name)
292+
return config_dict
293+
294+
def reestructure_cameras(config_dict):
295+
"""Ensure that all [Source_0, Source_1, ...] are consecutive"""
296+
source_names = [x for x in config_dict.keys() if x.startswith("Source")]
297+
source_names.sort()
298+
for index, source_name in enumerate(source_names):
299+
if f'Source_{index}' != source_name:
300+
config_dict[f'Source_{index}'] = config_dict[source_name]
301+
config_dict.pop(source_name)
302+
return config_dict
303+
304+
def delete_camera_from_areas(camera_id, config_dict):
305+
areas = {key: config_dict[key] for key in config_dict.keys() if key.startswith("Area")}
306+
for key, area in areas.items():
307+
cameras = area['Cameras'].split(',')
308+
if camera_id in cameras:
309+
cameras.remove(camera_id)
310+
if len(cameras) == 0:
311+
logger.warning(f'After removing the camera "{camera_id}", the area "{area["Id"]} - {area["Name"]}" was left with no cameras and deleted')
312+
config_dict.pop(key)
313+
else:
314+
config_dict[key]['Cameras'] = ",".join(cameras)
315+
316+
config_dict = reestructure_areas(config_dict)
317+
318+
return config_dict
319+
320+
def get_cameras(options):
321+
config = extract_config(config_type='cameras')
322+
return [map_camera(x, config, options) for x in config.keys()]
323+
324+
def update_and_restart_config(config_dict):
325+
update_config_file(config_dict)
326+
327+
# TODO: Restart only when necessary, and only the threads that are necessary (for instance to load a new video)
328+
success = restart_processor()
329+
return success
330+
277331
@app.get("/process-video-cfg")
278332
async def process_video_cfg():
279333
logger.info("process-video-cfg requests on api")
@@ -298,12 +352,24 @@ async def get_config(options: Optional[str] = ""):
298352
@app.put("/config")
299353
async def update_config(config: ConfigDTO):
300354
config_dict = map_to_config_file_format(config)
301-
update_config_file(config_dict)
302-
# TODO: Restart only when necessary, and only the threads that are necessary (for instance to load a new video)
303-
success = restart_processor()
355+
356+
success = update_and_restart_config(config_dict)
304357
return handle_config_response(config_dict, success)
305358

306-
@app.post('/area')
359+
@app.get("/areas")
360+
async def list_areas():
361+
return {
362+
"areas": get_areas()
363+
}
364+
365+
@app.get("/areas/{area_id}")
366+
async def get_area(area_id):
367+
area = next((area for area in get_areas() if area['id'] == area_id), None)
368+
if not area:
369+
raise HTTPException(status_code=404, detail=f'The area: {area_id} does not exist')
370+
return area
371+
372+
@app.post('/areas')
307373
async def create_area(new_area: AreaConfigDTO):
308374
config_dict = extract_config()
309375
areas_name = [x for x in config_dict.keys() if x.startswith("Area")]
@@ -312,49 +378,42 @@ async def create_area(new_area: AreaConfigDTO):
312378
raise HTTPException(status_code=400, detail="Area already exists")
313379

314380
cameras = [x for x in config_dict.keys() if x.startswith("Source")]
315-
cameras = [map_camera(x, config_dict, { }) for x in cameras]
381+
cameras = [map_camera(x, config_dict, []) for x in cameras]
316382
camera_ids = [camera['id'] for camera in cameras]
317383
if not all(x in camera_ids for x in new_area.cameras.split(',')):
318384
non_existent_cameras = set(new_area.cameras.split(',')) - set(camera_ids)
319385
raise HTTPException(status_code=404, detail=f'The cameras: {non_existent_cameras} do not exist')
320386

321387
config_dict[f'Area_{len(areas)}'] = map_to_area_file_format(new_area)
322-
self.config.update_config(config_dict)
323-
self.config.reload()
324388

325-
# TODO: Restart only when necessary, and only the threads that are necessary (for instance to load a new video)
326-
success = restart_processor()
389+
success = update_and_restart_config(config_dict)
327390
return handle_config_response(config_dict, success)
328391

329-
@app.put('/area/{area_id}')
392+
@app.put('/areas/{area_id}')
330393
async def edit_area(area_id, edited_area: AreaConfigDTO):
331394
edited_area.id = area_id
332395
config_dict = extract_config()
333-
areas_name = [x for x in config_dict.keys() if x.startswith("Area")]
334-
areas = [map_area(x, config_dict) for x in areas_name]
396+
area_names = [x for x in config_dict.keys() if x.startswith("Area")]
397+
areas = [map_area(x, config_dict) for x in area_names]
335398
areas_ids = [area['id'] for area in areas]
336399
try:
337400
index = areas_ids.index(area_id)
338401
except ValueError:
339-
raise HTTPException(status_code=404, detail="Area does not exist")
402+
raise HTTPException(status_code=404, detail=f'The area: {area_id} does not exist')
340403

341404
cameras = [x for x in config_dict.keys() if x.startswith("Source")]
342-
cameras = [map_camera(x, config_dict, { }) for x in cameras]
405+
cameras = [map_camera(x, config_dict, []) for x in cameras]
343406
camera_ids = [camera['id'] for camera in cameras]
344407
if not all(x in camera_ids for x in edited_area.cameras.split(',')):
345408
non_existent_cameras = set(edited_area.cameras.split(',')) - set(camera_ids)
346409
raise HTTPException(status_code=404, detail=f'The cameras: {non_existent_cameras} do not exist')
347410

348411
config_dict[f"Area_{index}"] = map_to_area_file_format(edited_area)
349412

350-
self.config.update_config(config_dict)
351-
self.config.reload()
352-
353-
# TODO: Restart only when necessary, and only the threads that are necessary (for instance to load a new video)
354-
success = restart_processor()
413+
success = update_and_restart_config(config_dict)
355414
return handle_config_response(config_dict, success)
356415

357-
@app.delete('/area/{area_id}')
416+
@app.delete('/areas/{area_id}')
358417
async def delete_area(area_id):
359418
config_dict = extract_config()
360419
areas_name = [x for x in config_dict.keys() if x.startswith("Area")]
@@ -363,17 +422,77 @@ async def delete_area(area_id):
363422
try:
364423
index = areas_ids.index(area_id)
365424
except ValueError:
366-
raise HTTPException(status_code=404, detail="Area does not exist")
425+
raise HTTPException(status_code=404, detail=f'The area: {area_id} does not exist')
367426

368427
config_dict.pop(f'Area_{index}')
369-
self.config.update_config(config_dict)
370-
self.config.reload()
428+
config_dict = reestructure_areas((config_dict))
371429

372-
# TODO: Restart only when necessary, and only the threads that are necessary (for instance to load a new video)
373-
success = restart_processor()
430+
success = update_and_restart_config(config_dict)
374431
return handle_config_response(config_dict, success)
375432

376-
@app.get("/{camera_id}/image", response_model=ImageModel)
433+
@app.get("/cameras")
434+
async def list_cameras(options: Optional[str] = ""):
435+
return {
436+
"cameras": get_cameras(options)
437+
}
438+
439+
@app.get("/cameras/{camera_id}")
440+
async def get_camera(camera_id):
441+
camera = next((camera for camera in get_cameras(['withImage']) if camera['id'] == camera_id), None)
442+
if not camera:
443+
raise HTTPException(status_code=404, detail=f'The camera: {camera_id} does not exist')
444+
return camera
445+
446+
@app.post("/cameras")
447+
async def create_camera(new_camera: SourceConfigDTO):
448+
config_dict = extract_config()
449+
cameras_name = [x for x in config_dict.keys() if x.startswith("Source")]
450+
cameras = [map_camera(x, config_dict, []) for x in cameras_name]
451+
if new_camera.id in [camera['id'] for camera in cameras]:
452+
raise HTTPException(status_code=400, detail="Camera already exists")
453+
454+
config_dict[f'Source_{len(cameras)}'] = map_to_camera_file_format(new_camera)
455+
456+
success = update_and_restart_config(config_dict)
457+
return handle_config_response(config_dict, success)
458+
459+
@app.put("/cameras/{camera_id}")
460+
async def edit_camera(camera_id, edited_camera: SourceConfigDTO):
461+
edited_camera.id = camera_id
462+
config_dict = extract_config()
463+
camera_names = [x for x in config_dict.keys() if x.startswith("Source")]
464+
cameras = [map_camera(x, config_dict, []) for x in camera_names]
465+
cameras_ids = [camera['id'] for camera in cameras]
466+
try:
467+
index = cameras_ids.index(camera_id)
468+
except ValueError:
469+
raise HTTPException(status_code=404, detail=f'The camera: {camera_id} does not exist')
470+
471+
config_dict[f"Source_{index}"] = map_to_camera_file_format(edited_camera)
472+
473+
success = update_and_restart_config(config_dict)
474+
return handle_config_response(config_dict, success)
475+
476+
@app.delete("/cameras/{camera_id}")
477+
async def delete_camera(camera_id):
478+
config_dict = extract_config()
479+
camera_names = [x for x in config_dict.keys() if x.startswith("Source")]
480+
cameras = [map_area(x, config_dict) for x in camera_names]
481+
cameras_ids = [camera['id'] for camera in cameras]
482+
try:
483+
index = cameras_ids.index(camera_id)
484+
except ValueError:
485+
raise HTTPException(status_code=404, detail=f'The camera: {camera_id} does not exist')
486+
487+
config_dict = delete_camera_from_areas(camera_id, config_dict)
488+
489+
config_dict.pop(f'Source_{index}')
490+
config_dict = reestructure_cameras((config_dict))
491+
492+
success = update_and_restart_config(config_dict)
493+
return handle_config_response(config_dict, success)
494+
495+
@app.get("/cameras/{camera_id}/image", response_model=ImageModel)
377496
async def get_camera_image(camera_id):
378497
dir_path = verify_path(self.config.get_section_dict("App")["ScreenshotsDirectory"], camera_id)
379498
with open(f'{dir_path}/default.jpg', "rb") as image_file:
@@ -382,7 +501,7 @@ async def get_camera_image(camera_id):
382501
"image": encoded_string
383502
}
384503

385-
@app.put("/{camera_id}/image")
504+
@app.put("/cameras/{camera_id}/image")
386505
async def replace_camera_image(camera_id, body: ImageModel):
387506
dir_path = verify_path(self.config.get_section_dict("App")["ScreenshotsDirectory"], camera_id)
388507
try:
@@ -393,23 +512,20 @@ async def replace_camera_image(camera_id, body: ImageModel):
393512
except Exception:
394513
return HTTPException(status_code=400, detail="Invalid image format")
395514

396-
@app.post("/{camera_id}/homography_matrix")
515+
@app.post("/cameras/{camera_id}/homography_matrix")
397516
async def config_calibrated_distance(camera_id, body: ConfigHomographyMatrix):
398-
sources = self.config.get_video_sources()
399-
dir_source = list(filter(lambda source: source['id'] == camera_id, sources))
400-
if dir_source is None or len(dir_source) != 1:
401-
raise HTTPException(status_code=404, detail=f'Camera with id "{camera_id}" does not exist')
402-
dir_source = dir_source[0]
517+
dir_source = next((source for source in self.config.get_video_sources() if source['id'] == camera_id), None)
518+
if not dir_source:
519+
raise HTTPException(status_code=404, detail=f'The camera: {camera_id} does not exist')
403520
dir_path = get_camera_calibration_path(self.config, camera_id)
404521
compute_and_save_inv_homography_matrix(points=body, destination=dir_path)
405522
sections = self.config.get_sections()
406523
config_dict = {}
407524
for section in sections:
408525
config_dict[section] = self.config.get_section_dict(section)
409526
config_dict[dir_source['section']]['DistMethod'] = 'CalibratedDistance'
410-
update_config_file(config_dict)
411-
success = restart_processor()
412527

528+
success = update_and_restart_config(config_dict)
413529
return handle_config_response(config_dict, success)
414530

415531
@app.get("/slack/is-enabled")

config-frontend.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ Port: 8000
55
[Processor]
66
; The IP and Port on which your Processor node is runnning (according to your docker run's -p HOST_PORT:8000 ... for processor's docker run command)
77
Host: 0.0.0.0
8-
Port: 8001
8+
Port: 8300

libs/config_engine.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ def set_config_file(self, path):
3333
self.lock.release()
3434

3535
def _load(self):
36+
self.config = configparser.ConfigParser()
37+
self.config.optionxform = str
3638
self.config.read(self.config_file_path)
3739
for section in self.config.sections():
3840
self.section_options_dict[section] = {}

0 commit comments

Comments
 (0)