Spaces:
Sleeping
Sleeping
python codebase + dockerfile + makefile
Browse files- .dockerignore +6 -0
- .gitignore +4 -0
- Dockerfile +20 -0
- Makefile +22 -0
- requirements.txt +8 -0
- src/app.py +71 -0
- src/utils.py +72 -0
- videos/classroom.mp4 +3 -0
.dockerignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Docker related
|
| 2 |
+
Dockerfile
|
| 3 |
+
docker-compose.yml
|
| 4 |
+
.dockerignore
|
| 5 |
+
|
| 6 |
+
./postgres_data/*
|
.gitignore
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.output
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
RUN apt-get update && apt-get install -y \
|
| 6 |
+
libglib2.0-0 \
|
| 7 |
+
libsm6 \
|
| 8 |
+
libxext6 \
|
| 9 |
+
libxrender-dev \
|
| 10 |
+
libgl1-mesa-glx
|
| 11 |
+
|
| 12 |
+
COPY requirements.txt .
|
| 13 |
+
|
| 14 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 15 |
+
|
| 16 |
+
COPY . .
|
| 17 |
+
|
| 18 |
+
EXPOSE 5000-5100
|
| 19 |
+
|
| 20 |
+
CMD ["python", "src/app.py"]
|
Makefile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
IMAGE_NAME := mock-video-feed
|
| 2 |
+
CONTAINER_NAME := mock-video-feed-container
|
| 3 |
+
DOCKERFILE := Dockerfile
|
| 4 |
+
|
| 5 |
+
build:
|
| 6 |
+
docker build -t $(IMAGE_NAME) -f $(DOCKERFILE) .
|
| 7 |
+
|
| 8 |
+
run:
|
| 9 |
+
docker stop $(CONTAINER_NAME) || true
|
| 10 |
+
docker rm $(CONTAINER_NAME) || true
|
| 11 |
+
docker run --name $(CONTAINER_NAME) -d -p 5000-5100:5000-5100 $(IMAGE_NAME)
|
| 12 |
+
|
| 13 |
+
stop:
|
| 14 |
+
docker stop $(CONTAINER_NAME) || true
|
| 15 |
+
docker rm $(CONTAINER_NAME) || true
|
| 16 |
+
|
| 17 |
+
clean:
|
| 18 |
+
docker rmi $(IMAGE_NAME) || true
|
| 19 |
+
|
| 20 |
+
rebuild: stop clean build
|
| 21 |
+
|
| 22 |
+
.PHONY: build run stop clean rebuild
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
opencv-python==4.11.0.86
|
| 2 |
+
ultralytics==8.3.133
|
| 3 |
+
Flask==3.1.0
|
| 4 |
+
flask-cors==5.0.1
|
| 5 |
+
PyThreadKiller==3.0.6
|
| 6 |
+
vidgear==0.3.3
|
| 7 |
+
selenium==4.32.0
|
| 8 |
+
webdriver_manager==4.0.2
|
src/app.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from vidgear.gears import CamGear
|
| 2 |
+
from flask import Flask
|
| 3 |
+
import threading
|
| 4 |
+
from utils import start_html_stream
|
| 5 |
+
import os
|
| 6 |
+
import time
|
| 7 |
+
import cv2
|
| 8 |
+
import socket
|
| 9 |
+
|
| 10 |
+
def get_video_file():
|
| 11 |
+
return os.path.join("videos", "classroom.mp4")
|
| 12 |
+
|
| 13 |
+
video_file = get_video_file()
|
| 14 |
+
|
| 15 |
+
cap = cv2.VideoCapture(video_file)
|
| 16 |
+
framerate = cap.get(cv2.CAP_PROP_FPS)
|
| 17 |
+
cap.release()
|
| 18 |
+
|
| 19 |
+
stream = CamGear(source=video_file).start()
|
| 20 |
+
app = Flask(__name__)
|
| 21 |
+
output_frame = [None]
|
| 22 |
+
lock = threading.Lock()
|
| 23 |
+
|
| 24 |
+
def get_available_port(start_port):
|
| 25 |
+
port = start_port
|
| 26 |
+
while True:
|
| 27 |
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
| 28 |
+
if s.connect_ex(('localhost', port)) != 0:
|
| 29 |
+
return port
|
| 30 |
+
port += 1
|
| 31 |
+
|
| 32 |
+
PORT = get_available_port(5000)
|
| 33 |
+
|
| 34 |
+
@app.route('/')
|
| 35 |
+
def html_stream():
|
| 36 |
+
return start_html_stream(output_frame, lock)
|
| 37 |
+
|
| 38 |
+
def start_flask():
|
| 39 |
+
app.run(host="0.0.0.0", port=PORT, debug=True, use_reloader=False)
|
| 40 |
+
|
| 41 |
+
def process_stream():
|
| 42 |
+
global stream
|
| 43 |
+
while True:
|
| 44 |
+
start_time = time.time()
|
| 45 |
+
frame = stream.read()
|
| 46 |
+
if frame is None:
|
| 47 |
+
stream.stop()
|
| 48 |
+
stream = CamGear(source=video_file).start()
|
| 49 |
+
continue
|
| 50 |
+
|
| 51 |
+
with lock:
|
| 52 |
+
output_frame[0] = frame.copy()
|
| 53 |
+
|
| 54 |
+
elapsed_time = time.time() - start_time
|
| 55 |
+
sleep_time = max(1.0 / framerate - elapsed_time, 0)
|
| 56 |
+
time.sleep(sleep_time)
|
| 57 |
+
|
| 58 |
+
def main():
|
| 59 |
+
flask_thread = threading.Thread(target=start_flask)
|
| 60 |
+
flask_thread.daemon = True
|
| 61 |
+
flask_thread.start()
|
| 62 |
+
|
| 63 |
+
stream_thread = threading.Thread(target=process_stream)
|
| 64 |
+
stream_thread.daemon = True
|
| 65 |
+
stream_thread.start()
|
| 66 |
+
|
| 67 |
+
flask_thread.join()
|
| 68 |
+
stream_thread.join()
|
| 69 |
+
|
| 70 |
+
if __name__ == "__main__":
|
| 71 |
+
main()
|
src/utils.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
from selenium import webdriver
|
| 4 |
+
from selenium.webdriver.chrome.service import Service
|
| 5 |
+
from webdriver_manager.chrome import ChromeDriverManager
|
| 6 |
+
from selenium.webdriver.chrome.options import Options
|
| 7 |
+
import time
|
| 8 |
+
from flask import Response
|
| 9 |
+
import cv2
|
| 10 |
+
import time
|
| 11 |
+
|
| 12 |
+
def start_html_stream(output_frame, lock):
|
| 13 |
+
def generate():
|
| 14 |
+
while True:
|
| 15 |
+
with lock:
|
| 16 |
+
if output_frame[0] is None:
|
| 17 |
+
# TODO: Investigate why if remove this print or time.sleep (but not both at the same time), the stream does not work
|
| 18 |
+
# time.sleep(0.5)
|
| 19 |
+
print(f"{time.time()} - Frame is None.")
|
| 20 |
+
continue
|
| 21 |
+
(flag, encoded_image) = cv2.imencode(".jpg", output_frame[0])
|
| 22 |
+
if not flag:
|
| 23 |
+
continue
|
| 24 |
+
yield(b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + bytearray(encoded_image) + b'\r\n')
|
| 25 |
+
return Response(generate(), mimetype="multipart/x-mixed-replace; boundary=frame")
|
| 26 |
+
|
| 27 |
+
def capture_webpage(driver):
|
| 28 |
+
screenshot = driver.get_screenshot_as_png()
|
| 29 |
+
image = np.frombuffer(screenshot, dtype=np.uint8)
|
| 30 |
+
image = cv2.imdecode(image, cv2.IMREAD_COLOR)
|
| 31 |
+
|
| 32 |
+
height, width, _ = image.shape
|
| 33 |
+
new_width = 1920
|
| 34 |
+
new_height = int(new_width * height / width)
|
| 35 |
+
|
| 36 |
+
image = cv2.resize(image, (new_width, new_height))
|
| 37 |
+
|
| 38 |
+
if new_height > 1080:
|
| 39 |
+
start_y = (new_height - 1080) // 2
|
| 40 |
+
image = image[start_y:start_y + 1080, :]
|
| 41 |
+
|
| 42 |
+
return image
|
| 43 |
+
|
| 44 |
+
def get_webpage_frames(stream_url):
|
| 45 |
+
chrome_options = Options()
|
| 46 |
+
chrome_options.add_argument("--headless")
|
| 47 |
+
chrome_options.add_argument("--window-size=1920x1080")
|
| 48 |
+
|
| 49 |
+
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options)
|
| 50 |
+
driver.get(stream_url)
|
| 51 |
+
|
| 52 |
+
frames_per_second = 10
|
| 53 |
+
frame_interval = 1 / frames_per_second
|
| 54 |
+
|
| 55 |
+
try:
|
| 56 |
+
while True:
|
| 57 |
+
image = capture_webpage(driver)
|
| 58 |
+
yield image
|
| 59 |
+
time.sleep(frame_interval)
|
| 60 |
+
finally:
|
| 61 |
+
driver.quit()
|
| 62 |
+
|
| 63 |
+
def get_mjpeg_frames(stream_url):
|
| 64 |
+
cap = cv2.VideoCapture(stream_url)
|
| 65 |
+
|
| 66 |
+
while True:
|
| 67 |
+
ret, frame = cap.read()
|
| 68 |
+
if not ret:
|
| 69 |
+
break
|
| 70 |
+
yield frame
|
| 71 |
+
|
| 72 |
+
cap.release()
|
videos/classroom.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a69bd5e39ff0e74286d13bcbfdadddd307b85decd2d9853db7518e0028785e31
|
| 3 |
+
size 13548133
|