EC2Bathroom / app.py
ShawXI's picture
Update app.py
fef5d7d verified
Raw
History Blame Contribute Delete
16.1 kB
import os
import json
import tempfile
import time
import uuid
import cv2
import numpy as np
import gradio as gr
import boto3
import torch
import subprocess
from botocore.exceptions import ClientError
from ultralytics import YOLO
from concurrent.futures import ThreadPoolExecutor
import threading
import sys
import traceback
from dotenv import load_dotenv
import datetime
import requests
import random
load_dotenv()
# force immediate flush
sys.stdout.reconfigure(line_buffering=True)
sys.stderr.reconfigure(line_buffering=True)
print("✅ DEBUG: Logging is now forced to flush immediately.")
print(f"DEBUG: PyTorch is using CUDA: {torch.cuda.is_available()}")
print(f"DEBUG: Available GPUs: {torch.cuda.device_count()}")
if torch.cuda.is_available():
print(f"DEBUG: Current GPU: {torch.cuda.get_device_name(0)}")
AWS_ACCESS_KEY = os.getenv('AWS_ACCESS_KEY_ID')
AWS_SECRET_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
S3_BUCKET_NAME = os.getenv('S3_BUCKET_NAME')
AWS_REGION = os.getenv('AWS_REGION', 'us-east-1')
SQS_QUEUE_URL = os.getenv("SQS_QUEUE_URL")
print("DEBUG: Checking environment variables:")
print(f" - AWS_ACCESS_KEY_ID is {'SET' if AWS_ACCESS_KEY else 'NOT SET'}")
print(f" - AWS_SECRET_ACCESS_KEY is {'SET' if AWS_SECRET_KEY else 'NOT SET'}")
print(f" - S3_BUCKET_NAME = {S3_BUCKET_NAME}")
print(f" - AWS_REGION = {AWS_REGION}")
print(f" - SQS_QUEUE_URL = {SQS_QUEUE_URL}")
try:
s3_client = boto3.client(
's3',
aws_access_key_id=AWS_ACCESS_KEY,
aws_secret_access_key=AWS_SECRET_KEY,
region_name=AWS_REGION
)
print("DEBUG: Successfully created boto3 S3 client.")
except Exception as e:
print(f"DEBUG: Error creating boto3 S3 client: {e}")
s3_client = None
try:
sqs_client = boto3.client(
'sqs',
aws_access_key_id=AWS_ACCESS_KEY,
aws_secret_access_key=AWS_SECRET_KEY,
region_name=AWS_REGION
)
print("DEBUG: Successfully created boto3 SQS client.")
except Exception as e:
print(f"DEBUG: Error creating boto3 SQS client: {e}")
sqs_client = None
def run_ffmpeg_with_fallback(input_path, output_path, resize_filter, fps, codec_name):
if not torch.cuda.is_available():
raise RuntimeError("CUDA is required for this operation")
if not os.path.exists(input_path):
raise RuntimeError(f"Input file does not exist: {input_path}")
clamp = '-vf "scale=min(2032\\,iw):min(2032\\,ih)"'
nvenc_cmd = (
f'ffmpeg -y -i "{input_path}" {clamp} '
f'-pix_fmt yuv420p -color_range tv '
f'-color_primaries bt709 -color_trc bt709 -colorspace bt709 '
f'-c:v h264_nvenc -preset p7 -tune hq -rc:v vbr_hq '
f'-cq:v 19 -qmin:v 19 -qmax:v 21 -b:v 0 '
f'-maxrate:v 130M -bufsize:v 130M '
f'-profile:v high -r {fps} -c:a aac -b:a 192k '
f'"{output_path}"'
)
print("Running GPU encoding:\n", nvenc_cmd)
res = subprocess.run(nvenc_cmd, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
print("NVENC STDERR:", res.stderr)
if res.returncode != 0:
raise RuntimeError(f"GPU encoding failed (exit {res.returncode}): {res.stderr.strip()}")
if not os.path.exists(output_path) or os.path.getsize(output_path) == 0:
raise RuntimeError("GPU encoding produced no output or output is empty")
fixed = output_path.replace(".mp4", "_fixed.mp4")
bsf_cmd = (
f'ffmpeg -y -i "{output_path}" -c copy '
f'-bsf:v h264_metadata=video_full_range_flag=0:'
f'colour_primaries=1:transfer_characteristics=1:matrix_coefficients=1 '
f'"{fixed}"'
)
print("Patching VUI with bitstream filter:\n", bsf_cmd)
res2 = subprocess.run(bsf_cmd, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
print("BSF STDERR:", res2.stderr)
if res2.returncode != 0:
raise RuntimeError(f"Failed to patch metadata (exit {res2.returncode}): {res2.stderr.strip()}")
os.replace(fixed, output_path)
print("✅ GPU encoding succeeded and Rec.709 VUI injected")
def get_video_properties(video_path):
cmd = [
'ffprobe', '-v', 'error', '-select_streams', 'v:0',
'-show_entries', 'stream=codec_name,width,height,r_frame_rate,bit_rate',
'-of', 'json', video_path
]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
info = json.loads(result.stdout)
stream = info['streams'][0]
codec_name = stream['codec_name']
width = stream['width']
height = stream['height']
# avoid eval; parse fractional fps safely
r = stream['r_frame_rate']
if isinstance(r, str) and '/' in r:
num, den = r.split('/')
num = int(num) if num else 0
den = int(den) if den else 1
fps = num / den if den else float(num)
else:
fps = float(r)
bit_rate = stream.get('bit_rate', None)
return codec_name, fps, width, height, int(bit_rate) if bit_rate else None
def upload_to_s3(file_path, s3_key, extra_metadata=None):
try:
metadata = {str(k).replace(" ", "-").replace("_", "-").lower(): str(v)
for k, v in (extra_metadata or {}).items()} or None
extra_args = {"Metadata": metadata} if metadata else {}
print(f"DEBUG: Uploading '{file_path}' to S3 as '{s3_key}'...")
s3_client.upload_file(file_path, S3_BUCKET_NAME, s3_key, ExtraArgs=extra_args)
url = f"https://{S3_BUCKET_NAME}.s3.{AWS_REGION}.amazonaws.com/{s3_key}"
print("DEBUG: S3 upload successful.")
return url
except Exception as e:
print(f"ERROR: S3 upload failed: {e}")
traceback.print_exc()
return None
def s3_upload_with_timeout(file_path, s3_key, extra_metadata, timeout=60):
with ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(upload_to_s3, file_path, s3_key, extra_metadata)
try:
return future.result(timeout=timeout)
except Exception as e:
print("ERROR: S3 upload timed out:", e)
return None
def load_model():
print("DEBUG: Loading YOLO model...")
return YOLO("bests.pt").to("cuda" if torch.cuda.is_available() else "cpu")
def download_file_from_url(url, local_path):
try:
print(f"DEBUG: Downloading from URL: {url}")
r = requests.get(url, stream=True)
r.raise_for_status()
with open(local_path, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
print(f"✅ DEBUG: Download complete: {local_path}")
return True
except Exception as e:
print(f"❌ ERROR: Failed to download: {e}")
return False
def detect_ppe_video(video_file, original_key):
"""
Reads frames, tracks with YOLO, dedupes static objects, draws annotations,
pipes through FFmpeg, uploads video + JSON, and returns URLs + summary.
"""
if not video_file:
return None, "No video file provided."
if "annotated" in os.path.basename(original_key).lower():
return None, "Already annotated"
if not torch.cuda.is_available():
return None, "CUDA GPU is required"
video_path = video_file.name
model = load_model()
names = model.names # e.g., {0: 'Rebars', 1: 'Graafwerken'} — depends on your model
start_time = time.time()
cap = cv2.VideoCapture(video_path)
fps_cv = cap.get(cv2.CAP_PROP_FPS)
w_cv = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h_cv = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# Prepare output paths
parts = original_key.split('/')
parts[0] = "progress"
if "default" in parts:
parts[parts.index("default")] = "annotated"
parts[-1] = "annotated.mp4"
processed_s3_key = "/".join(parts)
final_output_path = "/tmp/annotated.mp4"
# Colors & counters (focus on Graafwerken / Rebars, fallback color for anything else)
preferred_colors = {
'Graafwerken': (0, 255, 255),
'Rebars': (0, 255, 0),
}
def color_for(label: str):
return preferred_colors.get(
label,
(random.randint(20, 255), random.randint(20, 255), random.randint(20, 255))
)
counts = {k: 0 for k in preferred_colors} # only count the two classes of interest
annotations = []
seen = set()
with tempfile.TemporaryDirectory() as td:
pipe_path = os.path.join(td, "pipe.mp4")
# Start FFmpeg pipe for intermediate
vf_chain = "format=yuv420p,scale=in_range=full:out_range=limited"
pipe_cmd = (
f'ffmpeg -y -f rawvideo -pix_fmt bgr24 -s {w_cv}x{h_cv} '
f'-r {fps_cv} -i pipe: -vf "{vf_chain}" '
f'-c:v libx264 -preset ultrafast -pix_fmt yuv420p "{pipe_path}"'
)
proc = subprocess.Popen(pipe_cmd, stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
shell=True, bufsize=10**8)
threading.Thread(
target=lambda p: [print(l.decode().rstrip()) for l in iter(p.stderr.readline, b'')],
args=(proc,), daemon=True
).start()
def process_batch(frames, start_frame_index):
# Run tracker for this batch
results = model.track(frames, conf=0.4, iou=0.3, persist=True)
for i, out_frame in enumerate(frames):
frame_no = start_frame_index + i
if results is None or len(results) <= i or results[i] is None or results[i].boxes is None:
proc.stdin.write(out_frame.tobytes())
continue
boxes = results[i].boxes
xyxy = boxes.xyxy.cpu().numpy()
cls = boxes.cls.cpu().numpy().astype(int)
ids_t = getattr(boxes, "id", None)
if ids_t is None:
tids = [None] * len(xyxy)
else:
tids = ids_t.cpu().numpy().astype(int).tolist()
for (box, cls_idx, tid) in zip(xyxy, cls, tids):
x1, y1, x2, y2 = map(int, box)
lbl = names.get(cls_idx, str(cls_idx))
cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
# dedupe: use track ID if present, else rounded center
key = (lbl, tid if tid is not None else (round(cx), round(cy)))
if key not in seen:
seen.add(key)
annotations.append({
"frame": frame_no,
"label": lbl,
"id": tid,
"bbox": [x1, y1, x2, y2],
"center": [cx, cy]
})
if lbl in counts:
counts[lbl] += 1
col = color_for(lbl)
cv2.rectangle(out_frame, (x1, y1), (x2, y2), col, 2)
cv2.putText(out_frame, lbl, (x1, y1 - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2)
proc.stdin.write(out_frame.tobytes())
# Read frames and process in batches, including the tail
batch, frame_count = [], 0
while True:
ret, frame = cap.read()
if not ret:
break
frame_count += 1
batch.append(frame)
if len(batch) == 5:
process_batch(batch, start_frame_index=frame_count - len(batch) + 1)
batch.clear()
if batch:
process_batch(batch, start_frame_index=frame_count - len(batch) + 1)
proc.stdin.close()
proc.wait(timeout=30)
cap.release()
# final GPU pass + Rec.709 injection (left as in your original)
codec_name, fps, w, h, _ = get_video_properties(video_path)
rf = ''
if w > 2032 or h > 2032:
rf = '-vf scale=min(2032\\,iw):min(2032\\,ih)'
try:
run_ffmpeg_with_fallback(pipe_path, final_output_path, rf, fps, codec_name)
except Exception as e:
traceback.print_exc()
return None, f"FFmpeg processing failed: {e}"
# dump JSON and upload
json_path = os.path.join(td, "annotations.json")
with open(json_path, "w") as jf:
json.dump({"counts": counts, "annotations": annotations}, jf, indent=2)
json_s3_key = processed_s3_key.rsplit(".", 1)[0] + ".json"
s3_json_url = s3_upload_with_timeout(json_path, json_s3_key, None)
# upload annotated video
s3_url = s3_upload_with_timeout(final_output_path, processed_s3_key, counts)
if not s3_url:
return None, "Upload failed"
summary = "\n".join(f"{c}: {n}" for c, n in counts.items() if n > 0) or "No detections"
print(f"✅ Uploaded video: {s3_url}")
print(f"✅ Uploaded JSON: {s3_json_url}")
print(f"Processing completed in {time.time()-start_time:.2f}s")
return {"video_url": s3_url, "json_url": s3_json_url, "summary": summary}
def submit_job(video_file, original_key):
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(video_file.name)[1]) as tmp:
tmp.write(video_file.read())
tmp_path = tmp.name
s3_input_key = f"input/{os.path.basename(tmp_path)}"
s3_url = upload_to_s3(tmp_path, s3_input_key)
if not s3_url:
return "S3 upload failed."
job_message = {"bucket": S3_BUCKET_NAME, "object_key": s3_input_key, "presigned_url": s3_url}
response = sqs_client.send_message(QueueUrl=SQS_QUEUE_URL, MessageBody=json.dumps(job_message))
print("DEBUG: Sent job to SQS:", response)
return "Job submitted successfully!"
def sqs_worker():
while True:
resp = sqs_client.receive_message(
QueueUrl=SQS_QUEUE_URL, MaxNumberOfMessages=10, WaitTimeSeconds=20
)
messages = resp.get('Messages', [])
if not messages:
print("DEBUG: No messages in queue right now.")
else:
for message in messages:
try:
job = json.loads(message['Body'])
print("DEBUG: Received job payload:", job)
bucket = job.get("bucket", S3_BUCKET_NAME)
key = job.get("object_key") or (job.get("presigned_url") or "").split("amazonaws.com/")[-1].split("?")[0]
if not key:
print("ERROR: missing object_key, skipping.")
continue
local_path = f"/tmp/{os.path.basename(key)}"
if download_file_from_url(job.get("presigned_url"), local_path):
with open(local_path, "rb") as f:
result = detect_ppe_video(f, key)
print("DEBUG: detect_ppe_video result:", result)
else:
print("ERROR: failed to download", job.get("presigned_url"))
except Exception as e:
print("❌ ERROR processing job:", e)
traceback.print_exc()
finally:
sqs_client.delete_message(
QueueUrl=SQS_QUEUE_URL, ReceiptHandle=message['ReceiptHandle']
)
time.sleep(10)
with gr.Blocks() as demo:
gr.Markdown("# YOLOv8 PPE Detection with S3 Metadata")
video_input = gr.File(label="Upload Video", file_types=[".mp4", ".avi", ".mov"])
original_key_input = gr.Textbox(label="S3 Key (original path)")
detect_button = gr.Button("Submit Job (Fire-and-Forget)")
job_id_output = gr.Textbox(label="Job ID")
detect_button.click(fn=submit_job, inputs=[ video_input, original_key_input], outputs=[job_id_output])
if __name__ == "__main__":
threading.Thread(target=sqs_worker, daemon=True).start()
demo.launch(server_name="0.0.0.0", server_port=7860)