Spaces:
Build error
Build error
import os | |
from pathlib import Path | |
import shutil | |
import tempfile | |
from typing import List | |
import gradio as gr | |
import uuid | |
from typing_extensions import TypedDict, Tuple | |
from fastapi import FastAPI | |
app = FastAPI() | |
# create a static directory to store the static files | |
GS_DIR = Path(str(tempfile.gettempdir())) / "gaussian_splatting_gradio" | |
GS_DIR.mkdir(parents=True, exist_ok=True) | |
StateDict = TypedDict("StateDict", { | |
"uuid": str, | |
}) | |
# http://localhost:7860/file=/tmp/gradio/c2110a7de804b39754d229de426dc9307bc03aea/page.svelte | |
HOST = "localhost" | |
PORT = 7860 | |
home_markdown = """ | |
... | |
""" | |
step1_markdown = """ | |
# Step 1 - Split Video into Frames | |
In the journey of transforming a video into a 3D Gaussian Splatting, the initial step is the conversion of the video into individual frames. You can here provide a **video file** and specify how much image you want to extract per second (*fps*). The application will then automatically extract the frames from the video and prepare them for the next step in the process. | |
However, you can also do this step manually and upload the frames directory by yourself in the next step. In this case, you can skip this step and go directly to the next step. | |
Please not that blurry frames will mostlikely result in a bad 3D model. So, make sure that the video is clear enough. | |
""" | |
step2_markdown = """ | |
# Step 2 - SfM using Colmap | |
In this step we use Colmap (https://github.com/colmap/colmap). This process utilizes the frames extracted from the uploaded video to generate camera parameters and a point cloud, which are essential components for the 3D Gaussian Splatting process. | |
This step could take a while depending on the number of frames and the resolution. So, please be patient. | |
You might want to do this step manually and upload the frames directory by yourself in the next step. In this case, you can skip this step and go directly to the next step. | |
""" | |
step3_markdown = """ | |
# Step 3 - 3D Gaussian Splatting | |
In this final step we use the 3D Gaussian Splatting Cuda implementation by MrNeRF (https://twitter.com/janusch_patas): https://github.com/MrNeRF/gaussian-splatting-cuda. | |
As it's quite rapid to train, you can easily use a high number of iterations. | |
""" | |
def getPlyFile(session_state_value: StateDict) -> str: | |
return f"/tmp/gaussian_splatting_gradio/{session_state_value['uuid']}/output/final_point_cloud.ply" | |
def getCamerasFile(session_state_value: StateDict) -> str: | |
return f"/tmp/gaussian_splatting_gradio/{session_state_value['uuid']}/output/cameras.json" | |
def getZipFile(session_state_value: StateDict) -> str: | |
return f"/tmp/gaussian_splatting_gradio/{session_state_value['uuid']}/result.zip" | |
def makeResult(session_state_value: StateDict) -> tuple[str, str, str]: | |
ply_file = getPlyFile(session_state_value) | |
cameras_file = getCamerasFile(session_state_value) | |
zip_file = getZipFile(session_state_value) | |
return [ply_file, cameras_file, zip_file] | |
# Utility functions | |
def createStateSession(previous_session: StateDict) -> StateDict: | |
if previous_session["uuid"] is None: | |
# Create new session | |
session_uuid = str(uuid.uuid4()) | |
print("Creating new session: ", session_uuid) | |
session_tmpdirname = GS_DIR / str(session_uuid) | |
session_tmpdirname.mkdir(parents=True, exist_ok=True) | |
print('Created temporary directory: ', session_tmpdirname) | |
session = StateDict( | |
uuid=session_uuid, | |
) | |
else: | |
# Use previous session | |
session = previous_session | |
return session | |
def removeStateSession(session_state_value: StateDict): | |
# Clean up previous session | |
session_uuid = session_state_value["uuid"] | |
session_tmpdirname = GS_DIR / str(session_uuid) | |
print('Removing temporary directory: ', session_tmpdirname) | |
shutil.rmtree(session_tmpdirname) | |
return StateDict( | |
uuid=None, | |
) | |
def makeButtonVisible(btn_value: str) -> gr.Button: | |
return gr.Button(btn_value, visible=True) | |
# Process functions | |
def process_ffmpeg( | |
session_state_value: StateDict, | |
ffmpeg_input: str, | |
ffmpeg_fps: int, | |
ffmpeg_qscale: int, | |
) -> list[str]: | |
# Ensure that a session is active | |
if session_state_value["uuid"] is None: | |
return | |
# Set up session directory | |
session_path = GS_DIR / str(session_state_value['uuid']) | |
logfile_path = Path(session_path) / "ffmpeg_log.txt" | |
logfile_path.touch() | |
try: | |
from services.ffmpeg import ffmpeg_run | |
with logfile_path.open("w") as log_file: | |
ffmpeg_run( | |
video_path = Path(ffmpeg_input), | |
output_path = session_path, | |
fps = int(ffmpeg_fps), | |
qscale = int(ffmpeg_qscale), | |
stream_file=log_file | |
) | |
print("Done with ffmpeg") | |
except Exception as e: | |
print(f"Error - {e}") | |
# print('Error - Removing temporary directory', session_path) | |
# shutil.rmtree(session_path) | |
# Get the list of all the file of (session_path / "input") | |
list_of_jpgs = [str(f) for f in (session_path / "input").glob("*.jpg")] | |
return list_of_jpgs | |
def processColmap( | |
session_state_value: StateDict, | |
colmap_inputs: List[tempfile.NamedTemporaryFile], | |
colmap_camera: str, | |
enable_rerun: bool | |
) -> Tuple[str, str]: | |
# Ensure that a session is active | |
if session_state_value["uuid"] is None: | |
return "", "" | |
# Set up session directory | |
session_path = GS_DIR / str(session_state_value['uuid']) | |
logfile_path = Path(session_path) / "colmap_log.txt" | |
logfile_path.touch() | |
rerunfile_path = Path(session_path) / "rerun_page.html" | |
rerunfile_path.touch() | |
(session_path / "input").mkdir(parents=True, exist_ok=True) | |
for file in colmap_inputs: | |
print("copying", file.name, "to", session_path / "input") | |
shutil.copy(file.name, session_path / "input") | |
try: | |
from services.colmap import colmap | |
with logfile_path.open("w") as log_file: | |
colmap( | |
source_path=session_path, | |
camera=str(colmap_camera), | |
stream_file=log_file | |
) | |
print("Done with colmap") | |
if enable_rerun: | |
from services.rerun import read_and_log_sparse_reconstruction | |
html = read_and_log_sparse_reconstruction( | |
exp_name = str(session_state_value['uuid']), | |
dataset_path = session_path, | |
) | |
print("Done with rerun") | |
else: | |
html = "Rerun was disable !" | |
with rerunfile_path.open("w") as rerunfile: | |
rerunfile.write(html) | |
except Exception as e: | |
print(f"Error - {e}") | |
# print('Error - Removing temporary directory', session_path) | |
# shutil.rmtree(session_path) | |
# zip the session_path folder | |
archive = shutil.make_archive("result", 'zip', GS_DIR, session_path) | |
print('Created zip file', archive) | |
return archive, rerunfile_path | |
def processGaussianSplattingCuda( | |
session_state_value: StateDict, | |
gs_input: tempfile.NamedTemporaryFile, | |
gs_iterations: int, | |
gs_convergence_rate: float, | |
gs_resolution: int, | |
) -> Tuple[str, str]: | |
# Ensure that a session is active | |
if session_state_value["uuid"] is None: | |
return | |
# Set up session directory | |
session_path = GS_DIR / str(session_state_value['uuid']) | |
logfile_path = Path(session_path) / "gaussian_splatting_cuda_log.txt" | |
logfile_path.touch() | |
# Unzip the gs_input file to the session_path | |
shutil.unpack_archive(gs_input.name, session_path) | |
# Copy the gs_input directory to the session_path | |
# shutil.copytree(gs_input, session_path) | |
try: | |
from services.gaussian_splatting_cuda import gaussian_splatting_cuda | |
with logfile_path.open("w") as log_file: | |
gaussian_splatting_cuda( | |
data_path = session_path, | |
output_path = session_path / "output", | |
gs_command = str(Path(__file__).parent.absolute() / "build" / 'gaussian_splatting_cuda'), | |
iterations = int(gs_iterations), | |
convergence_rate = float(gs_convergence_rate), | |
resolution = int(gs_resolution), | |
enable_cr_monitoring = False, | |
force = False, | |
empty_gpu_cache = False, | |
stream_file = log_file | |
) | |
print("Done with gaussian_splatting_cuda") | |
# Create a zip of the session_path folder | |
archive = shutil.make_archive("result", 'zip', GS_DIR, session_path) | |
print('Created zip file', archive) | |
# Move the zip file to the session_path folder | |
shutil.move(archive, session_path) | |
except Exception as e: | |
print(f"Error - {e}") | |
# print('Error - Removing temporary directory', session_path) | |
# shutil.rmtree(session_path) | |
return ( | |
session_path / "output" / "final_point_cloud.ply", | |
session_path / "output" / "cameras.json", | |
) | |
def updateLog(logname:str, session_state_value: StateDict) -> str: | |
if session_state_value["uuid"] is None: | |
return "" | |
log_file = GS_DIR / str(session_state_value['uuid']) / f"{logname}.txt" | |
if not log_file.exists(): | |
return "" | |
with log_file.open("r") as log_file: | |
logs = log_file.read() | |
return logs | |
def bindStep1Step2(step1_output: list[tempfile.NamedTemporaryFile]) -> list[str]: | |
return [file.name for file in step1_output] | |
def bindStep2Step3(step2_output: tempfile.NamedTemporaryFile) -> str: | |
return step2_output.name | |
def makeRerunIframe(rerun_html : tempfile.NamedTemporaryFile) -> str: | |
# If rerun_html is bigger than 300MB, then we don't show it | |
print(f"Rerun file size: {os.stat(rerun_html.name).st_size}") | |
if os.stat(rerun_html.name).st_size > 100_000_000: | |
print("Rerun file is too big, not showing it") | |
return "" | |
filepath = rerun_html.name | |
print("filepath", filepath) | |
return f"""<iframe src="/file={filepath}" width="100%"; height="1080px"></iframe>""" | |
with gr.Blocks() as demo: | |
############################# | |
########## State ############ | |
############################# | |
session_state = gr.State({ | |
"uuid": None, | |
}) | |
############################# | |
###### UI Components ######## | |
############################# | |
gr.Markdown("# Gaussian Splatting Kit") | |
gr.Markdown("Click on the **Duplicate** button to create a new instance of this app.") | |
duplicate_button = gr.DuplicateButton() | |
gr.Markdown(value=home_markdown) | |
with gr.Tab("Slit Video into Frames"): | |
step1_description = gr.Markdown(step1_markdown) | |
# Video Frames | |
with gr.Row(): | |
# Video Frames - Inputs | |
with gr.Column(): | |
# Video Frames - Inputs - Video File | |
step1_input = gr.PlayableVideo( | |
format="mp4", | |
source="upload", | |
label="Upload a video", | |
include_audio=False | |
) | |
# Video Frames - Inputs - Parameters | |
with gr.Row(variant="panel"): | |
# Video Frames - Inputs - Parameters - FFMPEG FPS | |
step1_fps = gr.Number( | |
label="FFMPEG Fps", | |
value=1, | |
minimum=1, | |
maximum=5, | |
step=0.10, | |
) | |
# Video Frames - Inputs - Parameters - FFMPEG Qscale | |
step1_qscale = gr.Number( | |
label="FFMPEG Qscale", | |
value=1, | |
minimum=1, | |
maximum=5, | |
step=1, | |
) | |
# Video Frames - Outputs | |
with gr.Column(): | |
# Video Frames - Outputs - Video File | |
step1_output = gr.File( | |
label="Frames", | |
file_count="directory", | |
type="file", | |
interactive=False, | |
) | |
# Video Frames - Outputs - Logs | |
step1_logs = gr.Textbox( | |
label="Videos Logs", | |
interactive=False, | |
show_copy_button=True | |
) | |
# Video Frames - Process Button | |
step1_processbtn = gr.Button("Process", visible=True) | |
# Video Frames - Visualize | |
# Video Frames - Visualize - | |
# step1_visualize_gallery = gr.Gallery() | |
with gr.Tab("Colmap"): | |
step2_description = gr.Markdown(step2_markdown) | |
# Colmap | |
with gr.Row(): | |
# Colmap - Inputs | |
with gr.Column(): | |
# Colmap - Inputs - Frames Directory | |
step2_input = gr.File( | |
label="Upload a frames directory", | |
file_count="directory", | |
type="file", | |
interactive=True, | |
) | |
# Colmap - Inputs - Parameters | |
with gr.Row(variant="panel"): | |
# Colmap - Inputs - Parameters - Colmap Camera | |
step2_camera = gr.Dropdown( | |
label="COLMAP Camera", | |
value="OPENCV", | |
choices=["OPENCV", "SIMPLE_PINHOLE", "PINHOLE", "SIMPLE_RADIAL", "RADIAL"], | |
) | |
# Colmap - Inputs - Parameters - Enable Rerun | |
step2_rerun = gr.Checkbox( | |
value=True, | |
label="Enable Rerun", | |
) | |
# Colmap - Outputs | |
with gr.Column(): | |
# Colmap - Outputs - Video File | |
step2_output = gr.File( | |
label="Colmap", | |
file_count="single", | |
file_types=[".zip"], | |
type="file", | |
interactive=False, | |
) | |
# Colmap - Outputs - Logs | |
step2_logs = gr.Textbox( | |
label="Colmap Logs", | |
interactive=False, | |
show_copy_button=True | |
) | |
# Colmap - Process Button | |
step2_processbtn = gr.Button("Process", visible=True) | |
# Colmap - Visualize | |
# Colmap - Visualize - Rerun HTML File | |
step_2_visualize_html = gr.File( | |
label="Rerun HTML", | |
file_count="single", | |
file_types=[".html"], | |
type="file", | |
interactive=False, | |
visible=False | |
) | |
# Colmap - Visualize - Rerun HTML | |
step_2_visualize = gr.HTML("Rerun", visible=True) | |
with gr.Tab("Gaussian Splatting"): | |
step3_description = gr.Markdown(step3_markdown) | |
# Gaussian Splatting | |
with gr.Row(): | |
# Gaussian Splatting - Inputs | |
with gr.Column(): | |
# Gaussian Splatting - Inputs - Colmap + Frames | |
step3_input = gr.File( | |
label="Upload a colmap + frames directory", | |
file_count="single", | |
file_types=[".zip"], | |
type="file", | |
interactive=True, | |
) | |
# Gaussian Splatting - Inputs - Parameters | |
with gr.Row(variant="panel"): | |
# Gaussian Splatting - Inputs - Parameters - GS Iterations | |
step3_iterations = gr.Number( | |
label="GS Iterations", | |
value=10_000, | |
minimum=1_000, | |
maximum=50_000, | |
step=1_000, | |
) | |
# Gaussian Splatting - Inputs - Parameters - GS Convergence Rate | |
step3_convergence_rate = gr.Number( | |
label="GS Convergence Rate", | |
value=0.01, | |
minimum=0.01, | |
maximum=1, | |
step=0.01, | |
) | |
# Gaussian Splatting - Inputs - Parameters - GS Resolution | |
step3_resolution = gr.Number( | |
label="GS Resolution", | |
value=512, | |
minimum=128, | |
maximum=1024, | |
step=128, | |
) | |
# Gaussian Splatting - Outputs | |
with gr.Column(): | |
with gr.Row(): | |
# Gaussian Splatting - Outputs - PLY File | |
step3_output1 = gr.File( | |
label="PLY File", | |
file_count="single", | |
type="file", | |
interactive=False, | |
) | |
# Gaussian Splatting - Outputs - Cameras File | |
step3_output2 = gr.File( | |
label="Cameras File", | |
file_count="single", | |
type="file", | |
interactive=False, | |
) | |
# Gaussian Splatting - Outputs - Logs | |
step3_logs = gr.Textbox( | |
label="Gaussian Splatting Logs", | |
interactive=False, | |
show_copy_button=True | |
) | |
# Gaussian Splatting - Process Button | |
step3_processbtn = gr.Button("Process", visible=True) | |
# Gaussian Splatting - Visualize | |
# Gaussian Splatting - Visualize - Antimatter15 HTML | |
# step_3_visualize = gr.HTML(getAntimatter15HTML(), visible=True) | |
step_3_visualize = gr.Button("Visualize", visible=True, link="https://antimatter15.com/splat/") | |
############################# | |
########## Events ########### | |
############################# | |
### Step 1 | |
# Make the process button visible when a video is uploaded | |
step1_upload_event = step1_input.upload( | |
fn=createStateSession, | |
inputs=[session_state], | |
outputs=[session_state] | |
).success( | |
fn=makeButtonVisible, | |
inputs=[step1_processbtn], | |
outputs=[step1_processbtn], | |
) | |
# Do the processing when the process button is clicked | |
step1_processevent = step1_processbtn.click( | |
fn=process_ffmpeg, | |
inputs=[session_state, step1_input, step1_fps, step1_qscale], | |
outputs=[step1_output], | |
).success( | |
fn=bindStep1Step2, | |
inputs=[step1_output], | |
outputs=[step2_input], | |
).success( | |
fn=makeButtonVisible, | |
inputs=[step2_processbtn], | |
outputs=[step2_processbtn], | |
) | |
# Update the logs every 2 seconds | |
step1_logsevent = step1_processbtn.click( | |
fn=lambda session: updateLog("ffmpeg_log", session), | |
inputs=[session_state], | |
outputs=[step1_logs], | |
every=2, | |
) | |
## Step 2 | |
# Make the process button visible when a video is uploaded | |
step2_upload_event = step2_input.upload( | |
fn=createStateSession, | |
inputs=[session_state], | |
outputs=[session_state] | |
).success( | |
fn=makeButtonVisible, | |
inputs=[step2_processbtn], | |
outputs=[step2_processbtn], | |
) | |
# Do the processing when the process button is clicked | |
step2_processevent = step2_processbtn.click( | |
fn=processColmap, | |
inputs=[session_state, step2_input, step2_camera, step2_rerun], | |
outputs=[step2_output, step_2_visualize_html] | |
).success( | |
fn=bindStep2Step3, | |
inputs=[step2_output], | |
outputs=[step3_input], | |
).success( | |
fn=makeButtonVisible, | |
inputs=[step3_processbtn], | |
outputs=[step3_processbtn], | |
).then( | |
fn=makeRerunIframe, | |
inputs=[step_2_visualize_html], | |
outputs=[step_2_visualize], | |
) | |
# Update the logs every 2 seconds | |
step2_logsevent = step2_processbtn.click( | |
fn=lambda session: updateLog("colmap_log", session), | |
inputs=[session_state], | |
outputs=[step2_logs], | |
every=2, | |
) | |
## Step 3 | |
# Make the process button visible when a video is uploaded | |
step3_upload_event = step3_input.upload( | |
fn=createStateSession, | |
inputs=[session_state], | |
outputs=[session_state] | |
).success( | |
fn=makeButtonVisible, | |
inputs=[step3_processbtn], | |
outputs=[step3_processbtn], | |
) | |
# Do the processing when the process button is clicked | |
step3_processevent = step3_processbtn.click( | |
fn=processGaussianSplattingCuda, | |
inputs=[session_state, step3_input, step3_iterations, step3_convergence_rate, step3_resolution], | |
outputs=[step3_output1, step3_output2] | |
) | |
# .success( | |
# fn=lambda x: x, | |
# inputs=[step3_output1, step3_output2], | |
# outputs=[], | |
# ) | |
# Update the logs every 2 seconds | |
step3_logsevent = step3_processbtn.click( | |
fn=lambda session: updateLog("gaussian_splatting_cuda_log", session), | |
inputs=[session_state], | |
outputs=[step3_logs], | |
every=2, | |
) | |
# reset_button = gr.ClearButton( | |
# components=[video_input, text_log, ffmpeg_fps, ffmpeg_qscale, colmap_camera], | |
# label="Reset", | |
# visible=False, | |
# ) | |
# print(f"async (x) => {{ {getJS(url='http://0.0.0.0:7860/output/37c7ae54-7752-4e7b-8ba9-bab32c86b316/output/point_cloud/iteration_100/point_cloud.ply')} }}") | |
# show_button.click( | |
# fn=None, | |
# inputs=[], | |
# outputs=[], | |
# _js=f"async (x) => {{ {getJS(url='http://0.0.0.0:7860/output/37c7ae54-7752-4e7b-8ba9-bab32c86b316/output/point_cloud/iteration_100/point_cloud.ply')} }}" | |
# ).then( | |
# fn=None, | |
# inputs=[], | |
# outputs=[], | |
# _js=f"async (x) => {{ {getJS(url='http://0.0.0.0:7860/output/37c7ae54-7752-4e7b-8ba9-bab32c86b316/output/point_cloud/iteration_100/point_cloud.ply')} }}" | |
# ) | |
# gr.LoginButton, gr.LogoutButton | |
# gr.HuggingFaceDatasetSaver | |
# gr.OAuthProfile | |
# with gr.Tab("jsdn"): | |
# input_mic = gr.HTML(getRerunHTML()) | |
demo.queue() | |
demo.launch() | |
# mount Gradio app to FastAPI app | |
# app = gr.mount_gradio_app(app, demo, path="/") | |
# if __name__ == "__main__": | |
# uvicorn.run(app, host="0.0.0.0", port=7860, ws_max_size=16777216*1000) | |