Explorar o código

feat: basic analyzers.

LeoDu030314 %!s(int64=4) %!d(string=hai) anos
pai
achega
a45c1c5fab

+ 16 - 0
backend_refactor/Dockerfile

@@ -0,0 +1,16 @@
+FROM python:3.9.13-bullseye
+
+RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' > /etc/timezone
+
+WORKDIR /pose
+
+COPY requirements.txt .
+RUN pip3 install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
+RUN python -m pip install detectron2 --no-cache-dir -f https://dl.fbaipublicfiles.com/detectron2/wheels/cpu/torch1.10/index.html
+
+# RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list
+
+COPY . .
+
+CMD ["gunicorn", "app:app"]
+

+ 213 - 0
backend_refactor/gunicorn.conf.py

@@ -0,0 +1,213 @@
+# Sample Gunicorn configuration file.
+
+#
+# Server socket
+#
+#   bind - The socket to bind.
+#
+#       A string of the form: 'HOST', 'HOST:PORT', 'unix:PATH'.
+#       An IP is a valid HOST.
+#
+#   backlog - The number of pending connections. This refers
+#       to the number of clients that can be waiting to be
+#       served. Exceeding this number results in the client
+#       getting an error when attempting to connect. It should
+#       only affect servers under significant load.
+#
+#       Must be a positive integer. Generally set in the 64-2048
+#       range.
+#
+
+bind = '0.0.0.0:80'
+backlog = 2048
+
+#
+# Worker processes
+#
+#   workers - The number of worker processes that this server
+#       should keep alive for handling requests.
+#
+#       A positive integer generally in the 2-4 x $(NUM_CORES)
+#       range. You'll want to vary this a bit to find the best
+#       for your particular application's work load.
+#
+#   worker_class - The type of workers to use. The default
+#       sync class should handle most 'normal' types of work
+#       loads. You'll want to read
+#       http://docs.gunicorn.org/en/latest/design.html#choosing-a-worker-type
+#       for information on when you might want to choose one
+#       of the other worker classes.
+#
+#       A string referring to a Python path to a subclass of
+#       gunicorn.workers.base.Worker. The default provided values
+#       can be seen at
+#       http://docs.gunicorn.org/en/latest/settings.html#worker-class
+#
+#   worker_connections - For the eventlet and gevent worker classes
+#       this limits the maximum number of simultaneous clients that
+#       a single process can handle.
+#
+#       A positive integer generally set to around 1000.
+#
+#   timeout - If a worker does not notify the master process in this
+#       number of seconds it is killed and a new worker is spawned
+#       to replace it.
+#
+#       Generally set to thirty seconds. Only set this noticeably
+#       higher if you're sure of the repercussions for sync workers.
+#       For the non sync workers it just means that the worker
+#       process is still communicating and is not tied to the length
+#       of time required to handle a single request.
+#
+#   keepalive - The number of seconds to wait for the next request
+#       on a Keep-Alive HTTP connection.
+#
+#       A positive integer. Generally set in the 1-5 seconds range.
+#
+
+workers = 5
+worker_class = 'sync'
+worker_connections = 1000
+timeout = 15
+keepalive = 2
+
+#
+#   spew - Install a trace function that spews every line of Python
+#       that is executed when running the server. This is the
+#       nuclear option.
+#
+#       True or False
+#
+
+spew = False
+
+#
+# Server mechanics
+#
+#   daemon - Detach the main Gunicorn process from the controlling
+#       terminal with a standard fork/fork sequence.
+#
+#       True or False
+#
+#   raw_env - Pass environment variables to the execution environment.
+#
+#   pidfile - The path to a pid file to write
+#
+#       A path string or None to not write a pid file.
+#
+#   user - Switch worker processes to run as this user.
+#
+#       A valid user id (as an integer) or the name of a user that
+#       can be retrieved with a call to pwd.getpwnam(value) or None
+#       to not change the worker process user.
+#
+#   group - Switch worker process to run as this group.
+#
+#       A valid group id (as an integer) or the name of a user that
+#       can be retrieved with a call to pwd.getgrnam(value) or None
+#       to change the worker processes group.
+#
+#   umask - A mask for file permissions written by Gunicorn. Note that
+#       this affects unix socket permissions.
+#
+#       A valid value for the os.umask(mode) call or a string
+#       compatible with int(value, 0) (0 means Python guesses
+#       the base, so values like "0", "0xFF", "0022" are valid
+#       for decimal, hex, and octal representations)
+#
+#   tmp_upload_dir - A directory to store temporary request data when
+#       requests are read. This will most likely be disappearing soon.
+#
+#       A path to a directory where the process owner can write. Or
+#       None to signal that Python should choose one on its own.
+#
+
+daemon = False
+raw_env = []
+pidfile = None
+umask = 0
+user = None
+group = None
+tmp_upload_dir = None
+
+#
+#   Logging
+#
+#   logfile - The path to a log file to write to.
+#
+#       A path string. "-" means log to stdout.
+#
+#   loglevel - The granularity of log output
+#
+#       A string of "debug", "info", "warning", "error", "critical"
+#
+
+errorlog = '-'
+loglevel = 'info'
+accesslog = '-'
+access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
+
+#
+# Process naming
+#
+#   proc_name - A base to use with setproctitle to change the way
+#       that Gunicorn processes are reported in the system process
+#       table. This affects things like 'ps' and 'top'. If you're
+#       going to be running more than one instance of Gunicorn you'll
+#       probably want to set a name to tell them apart. This requires
+#       that you install the setproctitle module.
+#
+#       A string or None to choose a default of something like 'gunicorn'.
+#
+
+proc_name = None
+
+#
+# Server hooks
+#
+#   post_fork - Called just after a worker has been forked.
+#
+#       A callable that takes a server and worker instance
+#       as arguments.
+#
+#   pre_fork - Called just prior to forking the worker subprocess.
+#
+#       A callable that accepts the same arguments as after_fork
+#
+#   pre_exec - Called just prior to forking off a secondary
+#       master process during things like config reloading.
+#
+#       A callable that takes a server instance as the sole argument.
+#
+
+def post_fork(server, worker):
+    server.log.info("Worker spawned (pid: %s)", worker.pid)
+
+def pre_fork(server, worker):
+    pass
+
+def pre_exec(server):
+    server.log.info("Forked child, re-executing.")
+
+def when_ready(server):
+    server.log.info("Server is ready. Spawning workers")
+
+def worker_int(worker):
+    worker.log.info("worker received INT or QUIT signal")
+
+    ## get traceback info
+    import threading, sys, traceback
+    id2name = {th.ident: th.name for th in threading.enumerate()}
+    code = []
+    for threadId, stack in sys._current_frames().items():
+        code.append("\n# Thread: %s(%d)" % (id2name.get(threadId, ""),
+                                            threadId))
+        for filename, lineno, name, line in traceback.extract_stack(stack):
+            code.append('File: "%s", line %d, in %s' % (filename,
+                                                        lineno, name))
+            if line:
+                code.append("  %s" % (line.strip()))
+    worker.log.debug("\n".join(code))
+
+def worker_abort(worker):
+    worker.log.info("worker received SIGABRT signal")

+ 3 - 0
backend_refactor/requirements.txt

@@ -1,2 +1,5 @@
 flask==2.1.2
+gunicorn==20.1.0
 Pillow==9.1.0
+numpy==1.22.3
+torch==1.10.1

+ 0 - 0
backend_refactor/service/photo_analyze.py → backend_refactor/service/photo/BasePhotoAnalyzer.py


+ 0 - 0
backend_refactor/service/video_analyze.py → backend_refactor/service/photo/__init__.py


+ 6 - 0
backend_refactor/service/video/BaseVideoAnalyzer.py

@@ -0,0 +1,6 @@
+class BaseVideoAnalyzer:
+    def __init__(self):
+        pass
+
+    def analyze(self, filename):
+        return

+ 116 - 0
backend_refactor/service/video/Video3DAnalyzer.py

@@ -0,0 +1,116 @@
+import glob
+import os
+import subprocess as sp
+import time
+
+import numpy as np
+from detectron2 import model_zoo
+from detectron2.config import get_cfg
+from detectron2.engine import DefaultPredictor
+from detectron2.utils.logger import setup_logger
+
+from service.video.BaseVideoAnalyzer import BaseVideoAnalyzer
+
+
+def get_resolution(filename):
+    command = ['ffprobe', '-v', 'error', '-select_streams', 'v:0',
+               '-show_entries', 'stream=width,height', '-of', 'csv=p=0', filename]
+    pipe = sp.Popen(command, stdout=sp.PIPE, bufsize=-1)
+    for line in pipe.stdout:
+        w, h = line.decode().strip().split(',')
+        return int(w), int(h)
+
+
+def read_video(filename):
+    w, h = get_resolution(filename)
+
+    command = ['ffmpeg',
+               '-i', filename,
+               '-f', 'image2pipe',
+               '-pix_fmt', 'bgr24',
+               '-vsync', '0',
+               '-vcodec', 'rawvideo', '-']
+
+    pipe = sp.Popen(command, stdout=sp.PIPE, bufsize=-1)
+    while True:
+        data = pipe.stdout.read(w * h * 3)
+        if not data:
+            break
+        yield np.frombuffer(data, dtype='uint8').reshape((h, w, 3))
+
+
+def run_3d(file_name):
+    cfg = get_cfg()
+    cfg_path = 'COCO-Keypoints/keypoint_rcnn_R_101_FPN_3x.yaml'
+    cfg.merge_from_file(model_zoo.get_config_file(cfg_path))
+    cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = 0.7
+    cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url(cfg_path)
+    predictor = DefaultPredictor(cfg)
+    input_folder = file_name
+    output_folder = file_name.replace(os.path.basename(file_name), "")
+
+    if os.path.isdir(input_folder):
+        im_list = glob.iglob(input_folder + '/*.mp4')
+    else:
+        im_list = [input_folder]
+
+    for video_name in im_list:
+        out_name = os.path.join(
+            output_folder, os.path.basename(video_name)
+        )
+        print('Processing {}'.format(video_name))
+
+        boxes = []
+        segments = []
+        keypoints = []
+
+        for frame_i, im in enumerate(read_video(video_name)):
+            t = time.time()
+            outputs = predictor(im)['instances'].to('cpu')
+
+            print('Frame {} processed in {:.3f}s'.format(frame_i, time.time() - t))
+
+            has_bbox = False
+            if outputs.has('pred_boxes'):
+                bbox_tensor = outputs.pred_boxes.tensor.numpy()
+                if len(bbox_tensor) > 0:
+                    has_bbox = True
+                    scores = outputs.scores.numpy()[:, None]
+                    bbox_tensor = np.concatenate((bbox_tensor, scores), axis=1)
+            if has_bbox:
+                kps = outputs.pred_keypoints.numpy()
+                kps_xy = kps[:, :, :2]
+                kps_prob = kps[:, :, 2:3]
+                kps_logit = np.zeros_like(kps_prob)  # Dummy
+                kps = np.concatenate((kps_xy, kps_logit, kps_prob), axis=2)
+                kps = kps.transpose(0, 2, 1)
+            else:
+                kps = []
+                bbox_tensor = []
+
+            # Mimic Detectron1 format
+            cls_boxes = [[], bbox_tensor]
+            cls_keyps = [[], kps]
+
+            boxes.append(cls_boxes)
+            segments.append(None)
+            keypoints.append(cls_keyps)
+
+        # Video resolution
+        metadata = {
+            'w': im.shape[1],
+            'h': im.shape[0],
+        }
+
+        np.savez_compressed(out_name, boxes=boxes, segments=segments, keypoints=keypoints, metadata=metadata)
+
+
+class Video3DAnalyzer(BaseVideoAnalyzer):
+    """Perform inference on a single video"""
+
+    def __init__(self):
+        super().__init__()
+
+    def analyze(self, filename):
+        setup_logger()
+        return run_3d(filename)

+ 0 - 0
backend_refactor/service/video/__init__.py