diff --git a/README.md b/README.md index b105c65..c2110c2 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,21 @@ If you find our work useful, please cite the following paper: Our codebase is built using multiple opensource contributions, please see [ACKNOWLEDGEMENTS](ACKNOWLEDGEMENTS) for more details. +## Web Interface (One-Click Setup) + +For a simple web interface where you can upload images and download 3D Gaussians: + +**macOS:** Double-click `start.command` in Finder. + +This will automatically: +- Create the conda environment if it doesn't exist +- Install all dependencies if needed +- Start the web server at http://localhost:8000 + +![](data/web-dark.png) +![](data/web-light.png) + + ## License Please check out the repository [LICENSE](LICENSE) before using the provided code and diff --git a/data/web-dark.png b/data/web-dark.png new file mode 100644 index 0000000..b246544 Binary files /dev/null and b/data/web-dark.png differ diff --git a/data/web-light.png b/data/web-light.png new file mode 100644 index 0000000..bad0847 Binary files /dev/null and b/data/web-light.png differ diff --git a/detailed-setup.md b/detailed-setup.md new file mode 100644 index 0000000..b6bec0c --- /dev/null +++ b/detailed-setup.md @@ -0,0 +1,479 @@ +# Detailed Setup Guide for SHARP - Beginner's Edition + +Welcome! This guide will walk you through setting up the SHARP 3D Prediction project on your computer, step by step. Don't worry if you're new to programming or command-line tools—we'll explain everything you need to know. + +## What is SHARP? + +SHARP is a tool that can take a single photograph and create a 3D representation of it, allowing you to view it from different angles. This project includes a user-friendly web interface where you can upload images and download the 3D results. + +## Table of Contents + +1. [Prerequisites - What You Need](#prerequisites---what-you-need) +2. [Step 1: Install Conda](#step-1-install-conda) +3. [Step 2: Download the Project](#step-2-download-the-project) +4. [Step 3: Set Up the Project](#step-3-set-up-the-project) +5. [Step 4: Start the Web Interface](#step-4-start-the-web-interface) +6. [Troubleshooting](#troubleshooting) +7. [Alternative Setup Methods](#alternative-setup-methods) + +--- + +## Prerequisites - What You Need + +Before we begin, here's what you'll need: + +### Hardware Requirements +- A computer running **macOS** (for Windows or Linux, see [Alternative Setup Methods](#alternative-setup-methods)) +- At least **8 GB of RAM** (16 GB or more recommended) +- At least **5 GB of free disk space** +- **Internet connection** for downloading software and dependencies + +### What You Don't Need +- You don't need to know how to code +- You don't need prior experience with Python or machine learning +- You don't need to understand how the AI works (but you can learn if you want!) + +--- + +## Step 1: Install Conda + +Conda is a package manager that helps organize Python and its libraries. Think of it as an app store for Python tools. + +### What is Conda? + +Conda creates isolated "environments" for different projects, so they don't interfere with each other. We'll use it to install Python and all the tools SHARP needs. + +### Download and Install Miniconda + +1. **Go to the Miniconda download page:** + - Open your web browser and visit: https://docs.conda.io/en/latest/miniconda.html + +2. **Download the macOS installer:** + - Look for the **macOS** section + - Download the **latest Python 3.x** installer for your Mac: + - If you have an **M1/M2/M3 Mac** (Apple Silicon): Choose the `Apple M1` or `arm64` version + - If you have an **Intel Mac**: Choose the `Intel x86_64` version + - The file will be named something like `Miniconda3-latest-MacOSX-arm64.pkg` or `Miniconda3-latest-MacOSX-x86_64.pkg` + +3. **Install Miniconda:** + - Double-click the downloaded `.pkg` file + - Follow the installation wizard: + - Click "Continue" through the introduction screens + - Accept the license agreement + - Choose "Install for me only" (recommended) + - Click "Install" and enter your password when prompted + - When installation completes, click "Close" + +4. **Verify the installation:** + - Open **Terminal** (you can find it in Applications → Utilities → Terminal) + - Type the following command and press Enter: + ```bash + conda --version + ``` + - You should see something like `conda X.X.X` (the exact version number may vary) + - **If you get an error** saying "conda: command not found": + - Close Terminal completely and open it again + - Try the command again + - If it still doesn't work, see [Troubleshooting](#troubleshooting) + +--- + +## Step 2: Download the Project + +Now we need to download the SHARP project files to your computer. + +### Option A: Download via GitHub (Easiest for Beginners) + +1. **Go to the GitHub repository:** + - Visit the repository URL where this project is hosted (check the address bar or the project README for the correct URL) + +2. **Download the ZIP file:** + - Click the green **"Code"** button + - Click **"Download ZIP"** + - The file will be saved to your Downloads folder + +3. **Extract the ZIP file:** + - Go to your Downloads folder + - Double-click the `ml-sharp-main.zip` (or similar name) file + - macOS will automatically extract the folder + +4. **Move to a convenient location (optional but recommended):** + - Create a folder in your Documents called `Projects` + - Drag the extracted `ml-sharp-main` folder into `Projects` + - Rename it to just `ml-sharp` to make it simpler + +### Option B: Using Git (If You Have It Installed) + +If you're comfortable with Git or have it installed: + +```bash +cd ~/Documents +git clone +cd ml-sharp +``` + +(Replace `` with the actual Git URL from the GitHub repository) + +--- + +## Step 3: Set Up the Project + +Now we'll set up the Python environment and install all the required libraries. + +### Using Terminal + +1. **Open Terminal** (Applications → Utilities → Terminal) + +2. **Navigate to the project folder:** + - Type `cd` followed by a space + - Drag the `ml-sharp` folder from Finder onto the Terminal window + - Press Enter + - Your command should look something like: `cd /Users/YourName/Documents/Projects/ml-sharp` + +3. **Initialize conda in your terminal:** + ```bash + conda init bash + ``` + - Close Terminal and open it again + +4. **Create the Python environment:** + ```bash + conda create -n sharp python=3.13 -y + ``` + - This creates a special environment named "sharp" with Python 3.13 + - The process may take a few minutes + - Wait for it to complete + +5. **Activate the environment:** + ```bash + conda activate sharp + ``` + - You should see `(sharp)` appear at the beginning of your command prompt + +6. **Install the main project dependencies:** + ```bash + pip install -r requirements.txt + ``` + - This installs all the machine learning libraries SHARP needs + - **This will take 5-15 minutes** depending on your internet speed + - You'll see a lot of text scrolling by—this is normal + - Be patient and let it finish + +7. **Install the web interface dependencies:** + ```bash + pip install -r src/sharp/web/requirements.txt + ``` + - This installs the web server components + - This should be faster, taking about 1-2 minutes + +8. **Verify the installation:** + ```bash + sharp --help + ``` + - If successful, you'll see the SHARP help menu + - This means everything is installed correctly! + +--- + +## Step 4: Start the Web Interface + +Now for the exciting part—starting the web interface! + +### If start.command Works (Try This First) + +1. **In Finder, navigate to the ml-sharp folder** + +2. **Double-click the `start.command` file** + +3. **If macOS blocks it with a security warning:** + - Right-click (or Control-click) on `start.command` + - Select **"Open"** from the menu + - Click **"Open"** in the dialog that appears + - macOS will remember your choice for this file + + OR + + - Go to **System Settings → Privacy & Security** + - Scroll down to find a message about `start.command` being blocked + - Click **"Open Anyway"** + - Right-click on `start.command` again and choose **"Open"** + +4. **A Terminal window will open** with the SHARP logo + +5. **The script will automatically:** + - Check your conda installation + - Create or activate the environment + - Install any missing dependencies + - Start the web server + +6. **When you see "Starting Sharp Web Interface":** + - Open your web browser (Safari, Chrome, Firefox, etc.) + - Go to: **http://localhost:8000** + - You should see the SHARP web interface! + +### If start.command Doesn't Work (Manual Method) + +If the automatic script doesn't work, don't worry! Here's how to start it manually: + +1. **Open Terminal** + +2. **Navigate to the project folder:** + ```bash + cd /path/to/ml-sharp + ``` + (Replace with your actual path, or drag the folder onto Terminal) + +3. **Activate the conda environment:** + ```bash + conda activate sharp + ``` + +4. **Start the web server:** + ```bash + python src/sharp/web/app.py + ``` + +5. **Open your web browser and go to:** + - **http://localhost:8000** + +6. **To stop the server:** + - Press **Control + C** in the Terminal window + +--- + +## Troubleshooting + +### "conda: command not found" + +**Problem:** Terminal doesn't recognize the `conda` command. + +**Solutions:** + +1. **Initialize conda:** + ```bash + ~/miniconda3/bin/conda init bash + ``` + Then close and reopen Terminal. + +2. **Add conda to your PATH manually:** + ```bash + echo 'export PATH="$HOME/miniconda3/bin:$PATH"' >> ~/.bash_profile + source ~/.bash_profile + ``` + +3. **If you installed Anaconda instead of Miniconda:** + ```bash + echo 'export PATH="$HOME/anaconda3/bin:$PATH"' >> ~/.bash_profile + source ~/.bash_profile + ``` + +### "Permission denied" when running start.command + +**Problem:** The script doesn't have permission to run. + +**Solution:** + +1. Open Terminal +2. Navigate to the project folder +3. Make the script executable: + ```bash + chmod +x start.command + ``` +4. Try double-clicking it again + +### "Failed to create conda environment" + +**Problem:** Error creating the Python environment. + +**Solutions:** + +1. **Make sure you have enough disk space** (at least 5 GB free) + +2. **Update conda:** + ```bash + conda update conda + ``` + +3. **Try creating the environment with a different Python version:** + ```bash + conda create -n sharp python=3.11 -y + ``` + +### Installation is Taking Forever / Stuck + +**Problem:** `pip install` seems frozen or very slow. + +**Solutions:** + +1. **Be patient:** The first installation can take 10-20 minutes, especially for PyTorch +2. **Check your internet connection** +3. **If truly stuck (no progress for 30+ minutes):** + - Press Control + C to cancel + - Try again: + ```bash + pip install -r requirements.txt --no-cache-dir + ``` + +### "Port 8000 is already in use" + +**Problem:** Another application is using port 8000. + +**Solution:** + +1. **Find and stop the process using port 8000:** + ```bash + lsof -ti:8000 | xargs kill -9 + ``` + +2. **Or modify the port in the app.py file:** + - Open `src/sharp/web/app.py` in a text editor + - Look for the line near the end: `uvicorn.run(app, host="0.0.0.0", port=8000)` + (Tip: Search for `port=` to find it quickly) + - Change `port=8000` to `port=8080` (or another available port number) + - Save the file and start the server again + - Then access it at: http://localhost:8080 (or your chosen port) + +### The Web Interface Won't Load + +**Problem:** Browser shows an error when accessing http://localhost:8000 + +**Solutions:** + +1. **Make sure the server is actually running** (check Terminal for errors) +2. **Try a different browser** +3. **Clear your browser cache** +4. **Check if you're using the correct URL:** http://localhost:8000 (not https) +5. **Look for error messages in the Terminal** and search for them online or see below + +### "ModuleNotFoundError: No module named 'sharp'" + +**Problem:** Python can't find the SHARP module. + +**Solution:** + +1. Make sure you're in the correct directory +2. Make sure the conda environment is activated (you should see `(sharp)` in the prompt) +3. Try installing again: + ```bash + pip install -r requirements.txt + ``` + +### Out of Memory Errors + +**Problem:** Your computer runs out of RAM. + +**Solutions:** + +1. **Close other applications** to free up memory +2. **Restart your computer** and try again +3. **Consider using the CLI** instead of the web interface for large batches of images + +--- + +## Alternative Setup Methods + +### For Windows Users + +The `start.command` script is macOS-specific, but you can follow these steps on Windows: + +1. **Install Miniconda for Windows:** + - Download from: https://docs.conda.io/en/latest/miniconda.html + - Choose the Windows installer + +2. **Open Anaconda Prompt** (search for it in the Start menu) + +3. **Follow steps 3 and 4** from above, using the same commands + +### For Linux Users + +The setup is very similar to macOS: + +1. **Install Miniconda for Linux:** + ```bash + wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh + bash Miniconda3-latest-Linux-x86_64.sh + ``` + +2. **Follow the macOS instructions** using Terminal + +### Using the Command Line Interface (CLI) + +If you prefer not to use the web interface, you can use SHARP directly from the command line: + +1. **Activate the environment:** + ```bash + conda activate sharp + ``` + +2. **Run a prediction:** + ```bash + sharp predict -i /path/to/input/images -o /path/to/output/gaussians + ``` + +3. **See all options:** + ```bash + sharp --help + ``` + +--- + +## What's Next? + +Once you have the web interface running: + +1. **Upload an image** using the web interface +2. **Wait for processing** (usually takes a few seconds per image) +3. **Download the 3D Gaussian file** (.ply format) +4. **View it** using a 3D Gaussian viewer (the web interface may include a viewer) + +For more advanced usage, check out the [README.md](README.md) file in the project folder. + +--- + +## Getting Help + +If you're still stuck after trying these solutions: + +1. **Check the project's GitHub Issues page** to see if others have had similar problems +2. **Read the main [README.md](README.md)** for additional technical details +3. **Create a new GitHub Issue** describing your problem: + - Include your operating system version + - Include any error messages you see + - Describe what you've already tried + +--- + +## Tips for Success + +- **Be patient:** The first setup takes time, but subsequent runs will be much faster +- **Read error messages:** They often tell you exactly what's wrong +- **Google is your friend:** Copy error messages and search for them +- **Keep your terminal open:** Don't close Terminal while the server is running +- **Save your work:** The web interface processes images but doesn't permanently store them + +--- + +## Summary of Commands + +Here's a quick reference of the key commands: + +```bash +# Navigate to project +cd /path/to/ml-sharp + +# Activate environment +conda activate sharp + +# Start web interface (manual method) +python src/sharp/web/app.py + +# Use CLI +sharp predict -i /path/to/images -o /path/to/output + +# Deactivate environment when done +conda deactivate +``` + +--- + +**Congratulations!** 🎉 You now have SHARP set up and running. Enjoy creating 3D representations from your photos! diff --git a/src/sharp/web/README.md b/src/sharp/web/README.md new file mode 100644 index 0000000..017f545 --- /dev/null +++ b/src/sharp/web/README.md @@ -0,0 +1,33 @@ +# Sharp Web Interface + +This is a web interface for the Sharp 3D prediction model. + +## Prerequisites + +Make sure you have the `sharp` package installed (see root README). +Install the web dependencies: + +```bash +pip install -r requirements.txt +``` + +## Running the Server + +Run the following command from the `web` directory: + +```bash +python app.py +``` + +Or using uvicorn directly: + +```bash +uvicorn app:app --reload --host 0.0.0.0 --port 8000 +``` + +## Usage + +1. Open your browser and navigate to `http://localhost:8000`. +2. Drag and drop images or click to select them. +3. Click "Predict 3D Gaussians". +4. A zip file containing the resulting `.ply` files will be downloaded automatically. diff --git a/src/sharp/web/app.py b/src/sharp/web/app.py new file mode 100644 index 0000000..c23c967 --- /dev/null +++ b/src/sharp/web/app.py @@ -0,0 +1,184 @@ +import sys +from pathlib import Path +import logging +import shutil +import tempfile +import zipfile +import io as python_io +import base64 + +from fastapi import FastAPI, Request, UploadFile, File +from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +import torch +import numpy as np + +# Add src to path so we can import sharp +sys.path.append(str(Path(__file__).parent.parent / "src")) + +from sharp.models import ( + PredictorParams, + RGBGaussianPredictor, + create_predictor, +) +from sharp.utils import io as sharp_io +from sharp.utils.gaussians import save_ply +from sharp.cli.predict import predict_image, DEFAULT_MODEL_URL + +# Configure logging +logging.basicConfig(level=logging.INFO) +LOGGER = logging.getLogger(__name__) + +app = FastAPI() + +# Mount static files if needed (we created the dir) +app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static") + +templates = Jinja2Templates(directory=Path(__file__).parent / "templates") + +# Global variables for the model +predictor: RGBGaussianPredictor = None +device: torch.device = None + +@app.on_event("startup") +async def startup_event(): + global predictor, device + + # Determine device + if torch.cuda.is_available(): + device_str = "cuda" + elif torch.mps.is_available(): + device_str = "mps" + else: + device_str = "cpu" + + device = torch.device(device_str) + LOGGER.info(f"Using device: {device}") + + # Load model + LOGGER.info("Loading model...") + try: + # Try to load from cache or download + state_dict = torch.hub.load_state_dict_from_url(DEFAULT_MODEL_URL, progress=True, map_location=device) + + predictor = create_predictor(PredictorParams()) + predictor.load_state_dict(state_dict) + predictor.eval() + predictor.to(device) + LOGGER.info("Model loaded successfully.") + except Exception as e: + LOGGER.error(f"Failed to load model: {e}") + raise e + +@app.get("/", response_class=HTMLResponse) +async def read_root(request: Request): + return templates.TemplateResponse("index.html", {"request": request}) + +@app.post("/predict") +async def predict(files: list[UploadFile] = File(...)): + """Process images and return PLY data for viewing or download.""" + if not predictor: + return JSONResponse({"error": "Model not loaded"}, status_code=500) + + # Create a temporary directory to process files + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + results = [] + + for file in files: + try: + # Save uploaded file + file_path = temp_path / file.filename + with open(file_path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + LOGGER.info(f"Processing {file.filename}") + + # Load image using sharp's IO to get focal length and handle rotation + image, _, f_px = sharp_io.load_rgb(file_path) + + # Run prediction + gaussians = predict_image(predictor, image, f_px, device) + + # Save PLY + ply_filename = f"{file_path.stem}.ply" + ply_path = temp_path / ply_filename + + height, width = image.shape[:2] + save_ply(gaussians, f_px, (height, width), ply_path) + + # Read PLY file and encode as base64 + with open(ply_path, "rb") as f: + ply_data = base64.b64encode(f.read()).decode("utf-8") + + results.append({ + "filename": file.filename, + "ply_filename": ply_filename, + "ply_data": ply_data, + "width": width, + "height": height, + "focal_length": f_px, + }) + + except Exception as e: + LOGGER.error(f"Error processing {file.filename}: {e}") + results.append({ + "filename": file.filename, + "error": str(e), + }) + + return JSONResponse({"results": results}) + + +@app.post("/predict/download") +async def predict_download(files: list[UploadFile] = File(...)): + """Process images and return a ZIP file for download.""" + if not predictor: + return HTMLResponse("Model not loaded", status_code=500) + + # Create a temporary directory to process files + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + output_zip = python_io.BytesIO() + + with zipfile.ZipFile(output_zip, "w") as zf: + for file in files: + try: + # Save uploaded file + file_path = temp_path / file.filename + with open(file_path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + LOGGER.info(f"Processing {file.filename}") + + # Load image using sharp's IO to get focal length and handle rotation + image, _, f_px = sharp_io.load_rgb(file_path) + + # Run prediction + gaussians = predict_image(predictor, image, f_px, device) + + # Save PLY + ply_filename = f"{file_path.stem}.ply" + ply_path = temp_path / ply_filename + + height, width = image.shape[:2] + save_ply(gaussians, f_px, (height, width), ply_path) + + # Add to zip + zf.write(ply_path, ply_filename) + + except Exception as e: + LOGGER.error(f"Error processing {file.filename}: {e}") + continue + + output_zip.seek(0) + return StreamingResponse( + output_zip, + media_type="application/zip", + headers={"Content-Disposition": "attachment; filename=gaussians.zip"} + ) + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/src/sharp/web/requirements.txt b/src/sharp/web/requirements.txt new file mode 100644 index 0000000..0911ec8 --- /dev/null +++ b/src/sharp/web/requirements.txt @@ -0,0 +1,4 @@ +fastapi +uvicorn +python-multipart +jinja2 diff --git a/src/sharp/web/static/css/styles.css b/src/sharp/web/static/css/styles.css new file mode 100644 index 0000000..394c2f3 --- /dev/null +++ b/src/sharp/web/static/css/styles.css @@ -0,0 +1,724 @@ +:root { + /* Dark mode (default) */ + --bg-primary: #0a0a0f; + --bg-secondary: #1a1a2e; + --bg-tertiary: #0f0f1a; + --text-primary: #ffffff; + --text-secondary: rgba(255, 255, 255, 0.9); + --text-muted: rgba(255, 255, 255, 0.6); + --text-dim: rgba(255, 255, 255, 0.4); + --text-dimmer: rgba(255, 255, 255, 0.3); + --card-bg: rgba(255, 255, 255, 0.03); + --card-border: rgba(255, 255, 255, 0.08); + --card-highlight: rgba(255, 255, 255, 0.05); + --upload-bg: rgba(255, 255, 255, 0.02); + --upload-border: rgba(255, 255, 255, 0.15); + --file-item-bg: rgba(255, 255, 255, 0.05); + --loader-bg: rgba(255, 255, 255, 0.03); + --particle-connection: rgba(255, 255, 255, 0.02); + --glow-color: rgba(99, 102, 241, 0.1); + --shadow-color: rgba(0, 0, 0, 0.3); +} + +[data-theme="light"] { + --bg-primary: #f8fafc; + --bg-secondary: #e2e8f0; + --bg-tertiary: #f1f5f9; + --text-primary: #0f172a; + --text-secondary: #1e293b; + --text-muted: #64748b; + --text-dim: #94a3b8; + --text-dimmer: #cbd5e1; + --card-bg: rgba(255, 255, 255, 0.8); + --card-border: rgba(0, 0, 0, 0.08); + --card-highlight: rgba(255, 255, 255, 0.5); + --upload-bg: rgba(99, 102, 241, 0.02); + --upload-border: rgba(99, 102, 241, 0.2); + --file-item-bg: rgba(99, 102, 241, 0.05); + --loader-bg: rgba(99, 102, 241, 0.05); + --particle-connection: rgba(99, 102, 241, 0.08); + --glow-color: rgba(99, 102, 241, 0.15); + --shadow-color: rgba(99, 102, 241, 0.1); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + min-height: 100vh; + background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 50%, var(--bg-tertiary) 100%); + color: var(--text-primary); + overflow-x: hidden; + transition: background 0.3s ease, color 0.3s ease; +} + +#particleCanvas { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 0; + pointer-events: none; +} + +.app-container { + position: relative; + z-index: 1; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 2rem; +} + +.header { + text-align: center; + margin-bottom: 2.5rem; +} + +.logo { + font-size: 3rem; + font-weight: 700; + background: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 0.5rem; + letter-spacing: -0.02em; +} + +.tagline { + font-size: 1.1rem; + color: var(--text-muted); + font-weight: 400; +} + +.theme-toggle { + position: fixed; + top: 1.5rem; + right: 1.5rem; + z-index: 100; + width: 48px; + height: 48px; + border-radius: 50%; + border: 1px solid var(--card-border); + background: var(--card-bg); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + box-shadow: 0 4px 12px var(--shadow-color); +} + +.theme-toggle:hover { + transform: scale(1.1); + border-color: #a855f7; +} + +.theme-toggle svg { + width: 22px; + height: 22px; + transition: all 0.3s ease; +} + +.theme-toggle .sun-icon { + stroke: #f59e0b; + display: none; +} + +.theme-toggle .moon-icon { + stroke: var(--text-muted); +} + +[data-theme="light"] .theme-toggle .sun-icon { + display: block; +} + +[data-theme="light"] .theme-toggle .moon-icon { + display: none; +} + +.card { + background: var(--card-bg); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid var(--card-border); + border-radius: 24px; + padding: 2.5rem; + width: 100%; + max-width: 520px; + box-shadow: + 0 4px 24px var(--shadow-color), + 0 0 80px var(--glow-color), + inset 0 1px 0 var(--card-highlight); + transition: background 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; +} + +[data-theme="light"] .card { + background: rgba(255, 255, 255, 0.12); + border: 1px solid rgba(255, 255, 255, 0.3); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + box-shadow: + 0 8px 32px rgba(99, 102, 241, 0.1), + 0 0 60px rgba(99, 102, 241, 0.06), + 0 0 0 1px rgba(255, 255, 255, 0.3) inset; +} + +.upload-zone { + border: 2px dashed var(--upload-border); + border-radius: 16px; + padding: 3rem 2rem; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + background: var(--upload-bg); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + position: relative; + overflow: hidden; +} + +[data-theme="light"] .upload-zone { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(99, 102, 241, 0.25); + box-shadow: + 0 4px 16px rgba(99, 102, 241, 0.08), + 0 0 0 1px rgba(255, 255, 255, 0.25) inset; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +[data-theme="light"] .upload-zone:hover { + background: rgba(255, 255, 255, 0.18); + border-color: rgba(99, 102, 241, 0.4); + box-shadow: + 0 8px 24px rgba(99, 102, 241, 0.12), + 0 0 0 1px rgba(255, 255, 255, 0.35) inset; +} + +[data-theme="light"] .upload-zone.has-files { + background: rgba(34, 197, 94, 0.06); + border-color: rgba(34, 197, 94, 0.35); + box-shadow: + 0 4px 16px rgba(34, 197, 94, 0.08), + 0 0 0 1px rgba(255, 255, 255, 0.25) inset; +} + +.upload-zone::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); + opacity: 0; + transition: opacity 0.3s ease; +} + +.upload-zone:hover { + border-color: rgba(99, 102, 241, 0.5); + transform: translateY(-2px); +} + +.upload-zone:hover::before { + opacity: 1; +} + +.upload-zone.drag-over { + border-color: #a855f7; + background: rgba(168, 85, 247, 0.1); +} + +.upload-zone.has-files { + border-color: rgba(34, 197, 94, 0.5); + background: rgba(34, 197, 94, 0.05); +} + +.upload-icon { + width: 64px; + height: 64px; + margin: 0 auto 1.25rem; + position: relative; + z-index: 1; +} + +.upload-icon svg { + width: 100%; + height: 100%; + stroke: var(--text-dim); + transition: stroke 0.3s ease; +} + +.upload-zone:hover .upload-icon svg { + stroke: #a855f7; +} + +.upload-text { + position: relative; + z-index: 1; +} + +.upload-text h3 { + font-size: 1.1rem; + font-weight: 500; + margin-bottom: 0.5rem; + color: var(--text-secondary); +} + +.upload-text p { + font-size: 0.875rem; + color: var(--text-dim); +} + +.file-list { + margin-top: 1.5rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.file-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + background: var(--file-item-bg); + border-radius: 10px; + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateX(-10px); + } + + to { + opacity: 1; + transform: translateX(0); + } +} + +.file-item .file-icon { + width: 36px; + height: 36px; + border-radius: 8px; + background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.file-item .file-icon svg { + width: 18px; + height: 18px; + stroke: white; +} + +.file-item .file-info { + flex: 1; + min-width: 0; +} + +.file-item .file-name { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.file-item .file-size { + font-size: 0.75rem; + color: var(--text-dim); +} + +.file-item .file-preview { + width: 36px; + height: 36px; + border-radius: 8px; + object-fit: cover; +} + +.submit-btn { + width: 100%; + margin-top: 1.5rem; + padding: 1rem 1.5rem; + font-size: 1rem; + font-weight: 600; + font-family: inherit; + color: white; + background: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%); + border: none; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.submit-btn::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, #4f46e5 0%, #9333ea 50%, #db2777 100%); + opacity: 0; + transition: opacity 0.3s ease; +} + +.submit-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 8px 30px rgba(99, 102, 241, 0.4); +} + +.submit-btn:hover:not(:disabled)::before { + opacity: 1; +} + +.submit-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.submit-btn span { + position: relative; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.loader-container { + display: none; + flex-direction: column; + align-items: center; + margin-top: 2rem; + padding: 2rem; + background: var(--loader-bg); + border-radius: 16px; +} + +.loader-container.active { + display: flex; +} + +.loader { + width: 48px; + height: 48px; + position: relative; +} + +.loader::before, +.loader::after { + content: ''; + position: absolute; + inset: 0; + border-radius: 50%; + border: 3px solid transparent; +} + +.loader::before { + border-top-color: #6366f1; + animation: spin 1s linear infinite; +} + +.loader::after { + border-right-color: #a855f7; + animation: spin 1.5s linear infinite reverse; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.loader-text { + margin-top: 1rem; + font-size: 0.875rem; + color: var(--text-muted); +} + +.loader-subtext { + font-size: 0.75rem; + color: var(--text-dimmer); + margin-top: 0.25rem; +} + +.results { + margin-top: 1.5rem; +} + +.result-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1rem 1.25rem; + background: rgba(34, 197, 94, 0.1); + border: 1px solid rgba(34, 197, 94, 0.2); + border-radius: 12px; + animation: fadeIn 0.5s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: scale(0.95); + } + + to { + opacity: 1; + transform: scale(1); + } +} + +.result-item .success-icon { + width: 40px; + height: 40px; + background: rgba(34, 197, 94, 0.2); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.result-item .success-icon svg { + width: 20px; + height: 20px; + stroke: #22c55e; +} + +.result-item .result-text { + flex: 1; +} + +.result-item .result-title { + font-weight: 600; + color: #22c55e; + font-size: 0.9375rem; +} + +.result-item .result-desc { + font-size: 0.8125rem; + color: var(--text-muted); + margin-top: 0.125rem; +} + +.footer { + margin-top: 2rem; + text-align: center; + font-size: 0.8125rem; + color: var(--text-dimmer); +} + +.footer a { + color: var(--text-muted); + text-decoration: none; + transition: color 0.2s ease; +} + +.footer a:hover { + color: #a855f7; +} + +/* Viewer styles */ +.viewer-container { + display: none; + flex-direction: column; + width: 100%; + max-width: 900px; + margin-top: 1.5rem; +} + +.viewer-container.active { + display: flex; +} + +.viewer-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.25rem; + background: var(--card-bg); + border: 1px solid var(--card-border); + border-radius: 16px 16px 0 0; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} + +.viewer-title { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.viewer-title h3 { + font-size: 1rem; + font-weight: 600; + color: var(--text-secondary); +} + +.viewer-title .file-badge { + font-size: 0.75rem; + padding: 0.25rem 0.75rem; + background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(168, 85, 247, 0.2)); + border-radius: 20px; + color: var(--text-muted); +} + +.viewer-actions { + display: flex; + gap: 0.5rem; +} + +.viewer-btn { + padding: 0.5rem 1rem; + font-size: 0.875rem; + font-weight: 500; + font-family: inherit; + color: var(--text-secondary); + background: var(--file-item-bg); + border: 1px solid var(--card-border); + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.viewer-btn:hover { + background: rgba(99, 102, 241, 0.15); + border-color: rgba(99, 102, 241, 0.3); +} + +.viewer-btn svg { + width: 16px; + height: 16px; +} + +.viewer-btn.primary { + background: linear-gradient(135deg, #6366f1, #a855f7); + border: none; + color: white; +} + +.viewer-btn.primary:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); +} + +.viewer-canvas-container { + position: relative; + width: 100%; + height: 500px; + background: #000; + border-left: 1px solid var(--card-border); + border-right: 1px solid var(--card-border); + overflow: hidden; +} + +.viewer-canvas-container canvas { + width: 100% !important; + height: 100% !important; +} + +.viewer-controls-hint { + position: absolute; + bottom: 1rem; + left: 50%; + transform: translateX(-50%); + padding: 0.5rem 1rem; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(10px); + border-radius: 8px; + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.7); + pointer-events: none; + transition: opacity 0.3s ease; +} + +.viewer-footer { + display: flex; + align-items: center; + justify-content: center; + padding: 0.75rem; + background: var(--card-bg); + border: 1px solid var(--card-border); + border-radius: 0 0 16px 16px; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} + +.viewer-footer span { + font-size: 0.75rem; + color: var(--text-dim); +} + +.viewer-footer a { + color: var(--text-secondary); + text-decoration: none; + transition: color 0.2s ease; +} + +.viewer-footer a:hover { + color: var(--text-primary); + text-decoration: underline; +} + +.back-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.25rem; + font-size: 0.875rem; + font-weight: 500; + font-family: inherit; + color: var(--text-muted); + background: transparent; + border: 1px solid var(--card-border); + border-radius: 10px; + cursor: pointer; + transition: all 0.2s ease; + margin-bottom: 1rem; +} + +.back-btn:hover { + color: var(--text-secondary); + border-color: var(--text-dim); +} + +.back-btn svg { + width: 18px; + height: 18px; +} + +/* Hide card when viewing */ +.card.hidden { + display: none; +} + +.header.minimized { + margin-bottom: 1rem; +} + +.header.minimized .logo { + font-size: 2rem; +} + +.header.minimized .tagline { + display: none; +} \ No newline at end of file diff --git a/src/sharp/web/static/js/main.js b/src/sharp/web/static/js/main.js new file mode 100644 index 0000000..69bc75e --- /dev/null +++ b/src/sharp/web/static/js/main.js @@ -0,0 +1,331 @@ +// Global state for PLY data (on window so module script can access) +window.currentPlyData = null; +window.currentPlyFilename = null; + +// Particle system for background +const canvas = document.getElementById('particleCanvas'); +const ctx = canvas.getContext('2d'); +let particles = []; +let animationId; +let mouseX = 0, mouseY = 0; + +function resizeCanvas() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; +} + +class Particle { + constructor() { + this.reset(); + } + + reset() { + this.x = Math.random() * canvas.width; + this.y = Math.random() * canvas.height; + this.z = Math.random() * 1000; + this.baseSize = Math.random() * 2 + 0.5; + this.color = this.getColor(); + this.vx = (Math.random() - 0.5) * 0.3; + this.vy = (Math.random() - 0.5) * 0.3; + this.vz = (Math.random() - 0.5) * 2; + } + + getColor() { + const colors = [ + { r: 99, g: 102, b: 241 }, // indigo + { r: 168, g: 85, b: 247 }, // purple + { r: 236, g: 72, b: 153 }, // pink + { r: 59, g: 130, b: 246 }, // blue + { r: 139, g: 92, b: 246 }, // violet + ]; + return colors[Math.floor(Math.random() * colors.length)]; + } + + update() { + // Mouse interaction + const dx = mouseX - this.x; + const dy = mouseY - this.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist < 150) { + const force = (150 - dist) / 150; + this.vx -= (dx / dist) * force * 0.5; + this.vy -= (dy / dist) * force * 0.5; + } + + this.x += this.vx; + this.y += this.vy; + this.z += this.vz; + + // Damping + this.vx *= 0.99; + this.vy *= 0.99; + + // Wrap around + if (this.x < 0) this.x = canvas.width; + if (this.x > canvas.width) this.x = 0; + if (this.y < 0) this.y = canvas.height; + if (this.y > canvas.height) this.y = 0; + if (this.z < 0 || this.z > 1000) this.vz *= -1; + } + + draw() { + const perspective = 1000 / (1000 + this.z); + const size = this.baseSize * perspective * 3; + const isLightMode = document.documentElement.getAttribute('data-theme') === 'light'; + const alpha = perspective * (isLightMode ? 0.4 : 0.6); + + ctx.beginPath(); + const gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, size * 2); + gradient.addColorStop(0, `rgba(${this.color.r}, ${this.color.g}, ${this.color.b}, ${alpha})`); + gradient.addColorStop(1, `rgba(${this.color.r}, ${this.color.g}, ${this.color.b}, 0)`); + ctx.fillStyle = gradient; + ctx.arc(this.x, this.y, size * 2, 0, Math.PI * 2); + ctx.fill(); + } +} + +function initParticles() { + particles = []; + const count = Math.min(200, Math.floor((canvas.width * canvas.height) / 8000)); + for (let i = 0; i < count; i++) { + particles.push(new Particle()); + } +} + +function animate() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + particles.forEach(p => { + p.update(); + p.draw(); + }); + + // Draw connections + const isLightMode = document.documentElement.getAttribute('data-theme') === 'light'; + ctx.strokeStyle = isLightMode ? 'rgba(99, 102, 241, 0.08)' : 'rgba(255, 255, 255, 0.02)'; + ctx.lineWidth = 0.5; + for (let i = 0; i < particles.length; i++) { + for (let j = i + 1; j < particles.length; j++) { + const dx = particles[i].x - particles[j].x; + const dy = particles[i].y - particles[j].y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist < 100) { + ctx.beginPath(); + ctx.moveTo(particles[i].x, particles[i].y); + ctx.lineTo(particles[j].x, particles[j].y); + ctx.stroke(); + } + } + } + + animationId = requestAnimationFrame(animate); +} + +window.addEventListener('resize', () => { + resizeCanvas(); + initParticles(); +}); + +document.addEventListener('mousemove', (e) => { + mouseX = e.clientX; + mouseY = e.clientY; +}); + +resizeCanvas(); +initParticles(); +animate(); + +// Theme toggle functionality +const themeToggle = document.getElementById('themeToggle'); +const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); + +function setTheme(theme) { + document.documentElement.setAttribute('data-theme', theme); + localStorage.setItem('theme', theme); +} + +function getPreferredTheme() { + const stored = localStorage.getItem('theme'); + if (stored) return stored; + return prefersDark.matches ? 'dark' : 'light'; +} + +// Initialize theme +setTheme(getPreferredTheme()); + +themeToggle.addEventListener('click', () => { + const current = document.documentElement.getAttribute('data-theme'); + setTheme(current === 'light' ? 'dark' : 'light'); +}); + +// Listen for system theme changes +prefersDark.addEventListener('change', (e) => { + if (!localStorage.getItem('theme')) { + setTheme(e.matches ? 'dark' : 'light'); + } +}); + +// Upload functionality +const dropZone = document.getElementById('dropZone'); +const fileInput = document.getElementById('fileInput'); +const fileList = document.getElementById('fileList'); +const form = document.getElementById('uploadForm'); +const loaderContainer = document.getElementById('loaderContainer'); +const results = document.getElementById('results'); +const submitBtn = document.getElementById('submitBtn'); + +dropZone.addEventListener('click', () => fileInput.click()); + +dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('drag-over'); +}); + +dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('drag-over'); +}); + +dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('drag-over'); + fileInput.files = e.dataTransfer.files; + updateFileList(); +}); + +fileInput.addEventListener('change', updateFileList); + +function formatFileSize(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; +} + +function updateFileList() { + fileList.innerHTML = ''; + + if (fileInput.files.length > 0) { + dropZone.classList.add('has-files'); + } else { + dropZone.classList.remove('has-files'); + } + + for (const file of fileInput.files) { + const div = document.createElement('div'); + div.className = 'file-item'; + + // Create preview + const reader = new FileReader(); + reader.onload = (e) => { + const preview = div.querySelector('.file-preview'); + if (preview) preview.src = e.target.result; + }; + reader.readAsDataURL(file); + + div.innerHTML = ` + +
+
${file.name}
+
${formatFileSize(file.size)}
+
+ `; + fileList.appendChild(div); + } +} + +form.addEventListener('submit', async (e) => { + e.preventDefault(); + if (fileInput.files.length === 0) return; + + submitBtn.disabled = true; + dropZone.style.display = 'none'; + fileList.style.display = 'none'; + submitBtn.style.display = 'none'; + loaderContainer.classList.add('active'); + results.innerHTML = ''; + + const formData = new FormData(); + for (const file of fileInput.files) { + formData.append('files', file); + } + + try { + const response = await fetch('/predict', { + method: 'POST', + body: formData + }); + + if (response.ok) { + const data = await response.json(); + + if (data.results && data.results.length > 0) { + const result = data.results[0]; + + if (result.error) { + showError(result.error); + } else { + // Store the PLY data and show the viewer + window.currentPlyData = result.ply_data; + window.currentPlyFilename = result.ply_filename; + + // Show viewer (check if module loaded) + if (typeof window.showViewer === 'function') { + window.showViewer(result); + } else { + // Fallback: offer download if viewer module failed to load + showError('3D viewer failed to load. Click the download button to get your PLY file.'); + // Trigger download + downloadPly(result.ply_data, result.ply_filename); + } + } + } + } else { + const error = await response.text(); + showError(error); + // Restore upload UI on error + dropZone.style.display = ''; + fileList.style.display = ''; + submitBtn.style.display = ''; + } + } catch (err) { + showError(err.message); + // Restore upload UI on error + dropZone.style.display = ''; + fileList.style.display = ''; + submitBtn.style.display = ''; + } finally { + submitBtn.disabled = false; + loaderContainer.classList.remove('active'); + } +}); + +function showError(message) { + results.innerHTML = ` +
+
+ + + + +
+
+
Error
+
${message}
+
+
+ `; +} + +function downloadPly(plyData, filename) { + const binaryString = atob(plyData); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + const blob = new Blob([bytes], { type: 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} diff --git a/src/sharp/web/static/js/viewer.js b/src/sharp/web/static/js/viewer.js new file mode 100644 index 0000000..418416a --- /dev/null +++ b/src/sharp/web/static/js/viewer.js @@ -0,0 +1,175 @@ +import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d'; +import * as THREE from 'three'; + +// Make THREE available globally for the viewer +window.THREE = THREE; +window.GaussianSplats3D = GaussianSplats3D; + +// Viewer state +let viewer = null; +let initialCameraPosition = null; +let initialCameraTarget = null; + +// DOM elements +const card = document.querySelector('.card'); +const header = document.querySelector('.header'); +const viewerContainer = document.getElementById('viewerContainer'); +const viewerCanvasContainer = document.getElementById('viewerCanvasContainer'); +const viewerFilename = document.getElementById('viewerFilename'); +const backBtn = document.getElementById('backBtn'); +const resetViewBtn = document.getElementById('resetViewBtn'); +const downloadBtn = document.getElementById('downloadBtn'); +const controlsHint = document.getElementById('controlsHint'); + +// Expose showViewer to global scope +window.showViewer = async function (result) { + // Update filename badge + viewerFilename.textContent = result.ply_filename; + + // Hide card and show viewer + card.classList.add('hidden'); + header.classList.add('minimized'); + viewerContainer.classList.add('active'); + + // Decode base64 PLY data + const binaryString = atob(result.ply_data); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + const plyBlob = new Blob([bytes], { type: 'application/octet-stream' }); + const plyUrl = URL.createObjectURL(plyBlob); + + // Clean up previous viewer if exists + if (viewer) { + viewer.dispose(); + viewer = null; + // Clear the container + while (viewerCanvasContainer.firstChild) { + if (viewerCanvasContainer.firstChild.id !== 'controlsHint') { + viewerCanvasContainer.removeChild(viewerCanvasContainer.firstChild); + } else { + break; + } + } + } + + // Wait for container to be visible and sized + await new Promise(resolve => setTimeout(resolve, 100)); + + try { + // Create the Gaussian Splat viewer + viewer = new GaussianSplats3D.Viewer({ + cameraUp: [0, -1, 0], + initialCameraPosition: [0, 0, -3], + initialCameraLookAt: [0, 0, 0], + rootElement: viewerCanvasContainer, + sharedMemoryForWorkers: false, + dynamicScene: false, + sceneRevealMode: GaussianSplats3D.SceneRevealMode.Instant, + antialiased: true, + }); + + // Load the PLY file - specify format since blob URLs don't have extensions + await viewer.addSplatScene(plyUrl, { + splatAlphaRemovalThreshold: 5, + showLoadingUI: false, + progressiveLoad: false, + format: GaussianSplats3D.SceneFormat.Ply, + }); + + viewer.start(); + + // Store initial camera state for reset + if (viewer.camera) { + initialCameraPosition = viewer.camera.position.clone(); + initialCameraTarget = new THREE.Vector3(0, 0, 0); + } + + // Hide controls hint after a few seconds + setTimeout(() => { + controlsHint.style.opacity = '0'; + }, 5000); + + // Cleanup blob URL after loading + URL.revokeObjectURL(plyUrl); + + } catch (error) { + console.error('Error loading Gaussian Splat:', error); + viewerCanvasContainer.innerHTML = ` +
+
+

Failed to load 3D viewer

+

${error.message}

+
+
+ `; + } +}; + +// Back button handler +backBtn.addEventListener('click', () => { + // Clean up viewer + if (viewer) { + viewer.dispose(); + viewer = null; + } + + // Clear the canvas container except for the hint + const hint = document.getElementById('controlsHint'); + viewerCanvasContainer.innerHTML = ''; + if (hint) { + hint.style.opacity = '1'; + viewerCanvasContainer.appendChild(hint); + } + + // Restore upload UI elements + const dropZone = document.getElementById('dropZone'); + const fileList = document.getElementById('fileList'); + const submitBtn = document.getElementById('submitBtn'); + dropZone.style.display = ''; + fileList.style.display = ''; + submitBtn.style.display = ''; + + // Show card and hide viewer + card.classList.remove('hidden'); + header.classList.remove('minimized'); + viewerContainer.classList.remove('active'); +}); + +// Reset view button handler +resetViewBtn.addEventListener('click', () => { + if (viewer && viewer.camera && initialCameraPosition) { + viewer.camera.position.copy(initialCameraPosition); + viewer.camera.lookAt(initialCameraTarget); + if (viewer.controls) { + viewer.controls.target.copy(initialCameraTarget); + viewer.controls.update(); + } + } +}); + +// Download button handler +downloadBtn.addEventListener('click', () => { + if (window.currentPlyData && window.currentPlyFilename) { + const binaryString = atob(window.currentPlyData); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + const blob = new Blob([bytes], { type: 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = window.currentPlyFilename; + a.click(); + URL.revokeObjectURL(url); + } +}); + +// Handle window resize for the viewer +window.addEventListener('resize', () => { + if (viewer && viewerContainer.classList.contains('active')) { + // The viewer should handle resize automatically + } +}); diff --git a/src/sharp/web/templates/index.html b/src/sharp/web/templates/index.html new file mode 100644 index 0000000..dc66a2b --- /dev/null +++ b/src/sharp/web/templates/index.html @@ -0,0 +1,151 @@ + + + + + + + Sharp + + + + + + + + + + + + +
+
+

Sharp

+

Transform images into Gaussian Splats

+
+ +
+
+
+
+ + + + +
+
+

Drop your image here

+

or click to browse

+
+ +
+ +
+ + +
+ +
+
+

Generating Gaussian Splats...

+

This may take a moment

+
+ +
+
+ + +
+ + +
+
+

3D Gaussian Splat Viewer

+ output.ply +
+
+ + +
+
+ +
+
+ 🖱️ Left click + drag to rotate • Scroll to zoom • Right click + drag to pan +
+
+ + +
+ + +
+ + + + + + + + + \ No newline at end of file diff --git a/start.command b/start.command new file mode 100755 index 0000000..659f191 --- /dev/null +++ b/start.command @@ -0,0 +1,94 @@ +#!/bin/bash +# Sharp Web Interface - One-Click Launcher +# Double-click this file to start the Sharp web interface + +# Change to the script's directory +cd "$(dirname "$0")" + +ENV_NAME="sharp" +PYTHON_VERSION="3.13" + +echo "======================================" +echo " Sharp 3D Prediction - Web Interface" +echo "======================================" +echo "" + +# Check if conda is available +if ! command -v conda &> /dev/null; then + echo "❌ Conda is not installed or not in PATH." + echo "" + echo "Please install Miniconda or Anaconda first:" + echo " https://docs.conda.io/en/latest/miniconda.html" + echo "" + read -p "Press Enter to exit..." + exit 1 +fi + +# Initialize conda for this shell session +eval "$(conda shell.bash hook)" + +# Check if the environment exists +if ! conda env list | grep -q "^${ENV_NAME} "; then + echo "📦 Creating conda environment '${ENV_NAME}' with Python ${PYTHON_VERSION}..." + conda create -n "$ENV_NAME" python="$PYTHON_VERSION" -y + if [ $? -ne 0 ]; then + echo "❌ Failed to create conda environment." + read -p "Press Enter to exit..." + exit 1 + fi + echo "✅ Environment created." + echo "" +fi + +# Activate the environment +echo "🔄 Activating conda environment '${ENV_NAME}'..." +conda activate "$ENV_NAME" +if [ $? -ne 0 ]; then + echo "❌ Failed to activate conda environment." + read -p "Press Enter to exit..." + exit 1 +fi + +# Check if sharp is installed by trying to import it +echo "🔍 Checking if dependencies are installed..." +if ! python -c "import sharp" 2>/dev/null; then + echo "📦 Installing project dependencies (this may take a few minutes)..." + pip install -r requirements.txt + if [ $? -ne 0 ]; then + echo "❌ Failed to install requirements." + read -p "Press Enter to exit..." + exit 1 + fi + echo "✅ Dependencies installed." + echo "" +fi + +# Check if web dependencies are installed +if ! python -c "import fastapi" 2>/dev/null; then + echo "📦 Installing web interface dependencies..." + pip install -r src/sharp/web/requirements.txt + if [ $? -ne 0 ]; then + echo "❌ Failed to install web requirements." + read -p "Press Enter to exit..." + exit 1 + fi + echo "✅ Web dependencies installed." + echo "" +fi + +echo "======================================" +echo "🚀 Starting Sharp Web Interface..." +echo "======================================" +echo "" +echo "Open your browser and go to:" +echo "" +echo " 👉 http://localhost:8000" +echo "" +echo "Press Ctrl+C to stop the server." +echo "" + +# Start the web server +python src/sharp/web/app.py + +# Keep terminal open if server stops unexpectedly +read -p "Press Enter to exit..."