Skip to content
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

Added sidecar (.exe) build script with pyinstaller #12

Merged
merged 5 commits into from
Apr 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ clean:
demo:
python demo.py

build:
pyinstaller -c -F --clean --name main-x86_64-pc-windows-msvc --distpath dist src/index.py
b:
pyinstaller -c -F --clean --name sidecar --specpath dist --distpath dist examples/fastapi-pyinstaller/server.py
36 changes: 36 additions & 0 deletions examples/fastapi-pyinstaller/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# FastAPI + Juxtapose to .exe using Pyinstaller

## How to compile the sidecar

```bash
git clone https://github.com/ziqinyeow/juxtapose
cd juxtapose
pip install .
pip install uninstall juxtapose
pip install pyinstaller fastapi uvicorn[standard] python-multipart juxtematics
pyinstaller -c -F --clean --name sidecar --specpath dist --distpath dist examples/fastapi-pyinstaller/server.py
```

## How to run the exe

Double click or run terminal `./dist/sidecar`.

<div align="center">
<p>
<a align="center" href="" target="_blank">
<img
width="850"
src="https://raw.githubusercontent.com/ziqinyeow/juxtapose/main/asset/fastapi-pyinstaller-demo.png"
>
</a>
</p>
</div>

It takes some time to load, open for PR to optimize this with `pyinstaller --one dir` or `cython`.

## Reason to git clone ultralytics & yapf

Once compiled using pyinstaller to `.exe` file, you will defo face error of couldn't import files.

1. ultralytics - DEFAULT.yaml file - to resolve this (modify in [utils file](./ultralytics//utils/__init__.py)) to self import the yaml.
2. yapf - GRAMMAR.txt and PATTERNGRAMMAR.txt - to resolve this (modify in [grammar file](./yapf_third_party/_ylib2to3//pgen2/grammar.py)) to self import the grammar txt file.
3 changes: 3 additions & 0 deletions examples/fastapi-pyinstaller/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fastapi
uvicorn[standard]
python-multipart
266 changes: 266 additions & 0 deletions examples/fastapi-pyinstaller/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import os
import sys
import time

start = time.time()

sys.path.insert(0, "src")
sys.path.insert(1, "examples/fastapi-pyinstaller")

import json
import uvicorn
from pathlib import Path
from typing import Dict

import numpy as np
from juxtapose import RTM, RTMPose
from juxtapose.singletap import Tapnet
from juxtapose.detectors import get_detector

from fastapi import FastAPI, UploadFile, File, Form, Body
from fastapi.responses import StreamingResponse
from juxtematics.human_profile import HumanProfile
from juxtematics.constants import BODY_JOINTS_MAP
from fastapi.middleware.cors import CORSMiddleware
from tempfile import NamedTemporaryFile

importing_time = time.time()

port = 8000

app = FastAPI(title="Juxt API", docs_url="/api/docs", openapi_url="/api/openapi.json")

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)


@app.get("/")
def ok():
return {"status": "ok"}


@app.get("/dir")
def dir():
dirname, filename = os.path.split(os.path.abspath(__file__))
return {"dir": dirname, "file": filename}


@app.get("/model")
def model():
path = "model"
has_model = os.path.isdir(path)
if has_model:
model_dir = [p.split(".")[0].split("_")[0] for p in os.listdir(path)]
else:
model_dir = []
return {"has_model": has_model, "model_dir": model_dir}


@app.post("/model/download")
def download_model(
type: str = Body(..., embed=True), name: str = Body(..., embed=True)
):
if type == "detector":
get_detector(name, captions="")
elif type == "pose":
RTMPose(name.split("-")[1])
elif type == "tapnet":
Tapnet()

return {"status": "ok"}


@app.post("/api/stream")
async def stream(
config: str = Form(...),
file: UploadFile = File(...),
):
config = json.loads(config)
model = RTM(
det=config["det"],
tracker=config["tracker"],
pose=config["pose"],
captions=config["detectorPrompt"],
)

def _stream(model, file):
for i, res in enumerate(
model(
file,
show=False,
save=False,
stream=True,
zones=config["zones"],
framestamp=config["framestamp"],
)
):
yield json.dumps({"frame": i, "persons": res.persons})
os.remove(file)

try:
try:
suffix = Path(file.filename).suffix
temp = NamedTemporaryFile(suffix=suffix, delete=False)
contents = file.file.read()
with temp as f:
f.write(contents)
except Exception:
return {"message": "There was an error uploading the file", "path": ""}
finally:
file.file.close()
return StreamingResponse(_stream(model, temp.name))

except Exception:
return {"message": "There was an error processing the file", "path": ""}


@app.post("/api/tapnetstream")
async def tapnet(
config: str = Form(...),
file: UploadFile = File(...),
):
config = json.loads(config)
model = Tapnet(config["points"])

def _stream(model: Tapnet, file):
for i, res in enumerate(
model(
file,
show=False,
save=False,
stream=True,
startFrame=config["startFrame"],
# zones=config["zones"],
# framestamp=config["framestamp"],
)
):
yield json.dumps({"frame": i, "tracks": res.tracks})
os.remove(file)

try:
try:
suffix = Path(file.filename).suffix
temp = NamedTemporaryFile(suffix=suffix, delete=False)
contents = file.file.read()
with temp as f:
f.write(contents)
except Exception:
return {"message": "There was an error uploading the file", "path": ""}
finally:
file.file.close()
return StreamingResponse(_stream(model, temp.name))

except Exception:
return {"message": "There was an error processing the file", "path": ""}


def get_valid_joint_name(joint_name):
if joint_name in BODY_JOINTS_MAP:
return BODY_JOINTS_MAP[joint_name]
return joint_name


@app.post("/api/humans")
def humans(
humans: Dict,
preprocess_interpolate: bool = False,
preprocess_filter: bool = False,
preprocess_smoothing: bool = False,
postcalculate_filter: bool = False,
postcalculate_smoothing: bool = False,
):
humans = humans["humans"]

# Humans is array of {id: 1, body_joints: [[[1,2],[3,4],...]]}
result_humans = []
for individual_human in humans:
if individual_human["id"] == "":
continue
human_profile = HumanProfile(human_idx=int(individual_human["id"]))
human_profile.init_with_data(np.array(individual_human["body_joints"]))
human_profile.compute(
preprocess_interpolate_on=preprocess_interpolate,
preprocess_filter_on=preprocess_filter,
preprocess_smoothing_on=preprocess_smoothing,
postcalculate_filter_on=postcalculate_filter,
postcalculate_smoothing_on=postcalculate_smoothing,
)
metrics = human_profile.get_metrics()
result_humans.append({"id": individual_human["id"], "metrics": metrics})
# human_profile.export_csv("output")

# SPECIAL PROCESSING JUST FOR FRONTEND
output_array = []

for entry in result_humans:
id_value = entry["id"]
metrics = entry["metrics"]
output_object = {}

for metric_category, body_part_data in metrics.items():
if metric_category not in ["body_joints_metrics", "custom_metrics"]:
continue # Skip irrelevant keys

for body_part, metric_data in body_part_data.items():
# Create metric if not exists
for metric_name, data in metric_data.items():
if metric_name not in output_object:
output_object[metric_name] = {
"name": metric_name,
"body_joint": [],
}
# If metric exists, append data into the existing metric
output_object[metric_name]["body_joint"].append(
{
"name": (
get_valid_joint_name(body_part)
if metric_category == "body_joints_metrics"
else body_part
),
"data": data,
"type": "line",
}
)

output_array.append({"id": id_value, "transformedMetrics": output_object})
# export output_array
# with open("output.json", "w") as outfile:
# json.dump(output_array, outfile)
return {"status": "ok", "results": output_array}


@app.post("/api/human")
def human(
human: Dict,
preprocess_interpolate_on=False,
preprocess_filter_on=False,
preprocess_smoothing_on=True,
postcalculate_filter_on=True,
postcalculate_smoothing_on=True,
):
human = human["human"]
human_profile = HumanProfile()
human_profile.init_with_data(np.array(human["body_joints"]))
human_profile.compute(
preprocess_interpolate_on=preprocess_interpolate_on,
preprocess_filter_on=preprocess_filter_on,
preprocess_smoothing_on=preprocess_smoothing_on,
postcalculate_filter_on=postcalculate_filter_on,
postcalculate_smoothing_on=postcalculate_smoothing_on,
)
metrics = human_profile.get_metrics()
return {"status": "ok", "results": metrics}


if __name__ == "__main__":
total_time = time.time()
print(f"running ok on localhost:{port}")
print(
f"loaded package in: {importing_time - start}, total time used: {total_time - start}"
)
uvicorn.run(app, host="localhost", port=port, reload=False)
27 changes: 27 additions & 0 deletions examples/fastapi-pyinstaller/ultralytics/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Ultralytics YOLO 🚀, AGPL-3.0 license

__version__ = "8.1.47"

from ultralytics.data.explorer.explorer import Explorer
from ultralytics.models import RTDETR, SAM, YOLO, YOLOWorld
from ultralytics.models.fastsam import FastSAM
from ultralytics.models.nas import NAS
from ultralytics.utils import ASSETS, SETTINGS
from ultralytics.utils.checks import check_yolo as checks
from ultralytics.utils.downloads import download

settings = SETTINGS
__all__ = (
"__version__",
"ASSETS",
"YOLO",
"YOLOWorld",
"NAS",
"SAM",
"FastSAM",
"RTDETR",
"checks",
"download",
"settings",
"Explorer",
)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading