Spaces:
Running on Zero
Running on Zero
Add portrait-aware webcam cropping
Browse files- README.md +1 -1
- src/demo/infer_single_image.py +11 -2
- src/demo/server_frontend/app.js +75 -10
- src/demo/server_frontend/index.html +23 -3
- src/demo/server_frontend/styles.css +108 -5
- tests/test_public_configs.py +12 -2
- tests/test_webcam_frontend.py +6 -0
README.md
CHANGED
|
@@ -116,7 +116,7 @@ See [docs/inference.md](docs/inference.md) for camera parameters, output control
|
|
| 116 |
|
| 117 |
## 🖥️ Web Demo
|
| 118 |
|
| 119 |
-
The local Gradio demo accepts uploaded images or photos captured from your webcam,
|
| 120 |
|
| 121 |
```bash
|
| 122 |
python demo.py
|
|
|
|
| 116 |
|
| 117 |
## 🖥️ Web Demo
|
| 118 |
|
| 119 |
+
The local Gradio demo accepts uploaded images or landscape/portrait photos captured from your webcam, with an option to process exactly the visible camera frame. It also provides RGB reconstruction, staged PLY and standalone HTML downloads, and an interactive Gaussian viewer. After preparing the inference environment, launch it with:
|
| 120 |
|
| 121 |
```bash
|
| 122 |
python demo.py
|
src/demo/infer_single_image.py
CHANGED
|
@@ -36,6 +36,13 @@ PROMPT_DEPTH_EPS = 1e-6
|
|
| 36 |
PROMPT_DISPARITY_MIN_DEPTH = 1e-2
|
| 37 |
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
@dataclass(frozen=True)
|
| 40 |
class ResolvedIntrinsics:
|
| 41 |
"""Resolved intrinsics for one input image.
|
|
@@ -268,8 +275,10 @@ def load_demo_image_bundle(
|
|
| 268 |
default_focal_35mm_mm=default_focal_35mm_mm,
|
| 269 |
)
|
| 270 |
|
| 271 |
-
target_height =
|
| 272 |
-
|
|
|
|
|
|
|
| 273 |
|
| 274 |
inference_image_rgb = image_rgb
|
| 275 |
inference_intrinsics = original_intrinsics
|
|
|
|
| 36 |
PROMPT_DISPARITY_MIN_DEPTH = 1e-2
|
| 37 |
|
| 38 |
|
| 39 |
+
def resolve_inference_shape(image_width: int, image_height: int) -> tuple[int, int]:
|
| 40 |
+
"""Return (height, width), preserving landscape or portrait orientation."""
|
| 41 |
+
if image_height > image_width:
|
| 42 |
+
return INFERENCE_WIDTH, INFERENCE_HEIGHT
|
| 43 |
+
return INFERENCE_HEIGHT, INFERENCE_WIDTH
|
| 44 |
+
|
| 45 |
+
|
| 46 |
@dataclass(frozen=True)
|
| 47 |
class ResolvedIntrinsics:
|
| 48 |
"""Resolved intrinsics for one input image.
|
|
|
|
| 275 |
default_focal_35mm_mm=default_focal_35mm_mm,
|
| 276 |
)
|
| 277 |
|
| 278 |
+
target_height, target_width = resolve_inference_shape(
|
| 279 |
+
image_width=original_width,
|
| 280 |
+
image_height=original_height,
|
| 281 |
+
)
|
| 282 |
|
| 283 |
inference_image_rgb = image_rgb
|
| 284 |
inference_intrinsics = original_intrinsics
|
src/demo/server_frontend/app.js
CHANGED
|
@@ -5,7 +5,12 @@ const imagePreview = document.getElementById("imagePreview");
|
|
| 5 |
const uploadSurface = document.getElementById("uploadSurface");
|
| 6 |
const chooseImageButton = document.getElementById("chooseImageButton");
|
| 7 |
const cameraButton = document.getElementById("cameraButton");
|
|
|
|
| 8 |
const cameraPreview = document.getElementById("cameraPreview");
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
const cameraActions = document.getElementById("cameraActions");
|
| 10 |
const cancelCameraButton = document.getElementById("cancelCameraButton");
|
| 11 |
const captureButton = document.getElementById("captureButton");
|
|
@@ -29,6 +34,10 @@ let running = false;
|
|
| 29 |
let toastTimer = null;
|
| 30 |
let cameraStream = null;
|
| 31 |
const supportedImageTypes = new Set(["image/jpeg", "image/png", "image/webp"]);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
const colorScheme = window.matchMedia("(prefers-color-scheme: dark)");
|
| 34 |
const requestedTheme = new URLSearchParams(window.location.search).get("__theme");
|
|
@@ -96,7 +105,7 @@ function setRunning(value) {
|
|
| 96 |
examplesGrid.toggleAttribute("inert", value);
|
| 97 |
}
|
| 98 |
|
| 99 |
-
function selectFile(file) {
|
| 100 |
if (!file || !supportedImageTypes.has(file.type)) {
|
| 101 |
showToast("Please choose a JPG, PNG, or WebP image.", true);
|
| 102 |
return;
|
|
@@ -108,6 +117,7 @@ function selectFile(file) {
|
|
| 108 |
imagePreview.src = previewUrl;
|
| 109 |
imagePreview.hidden = false;
|
| 110 |
chooseImageButton.hidden = true;
|
|
|
|
| 111 |
reconstructButton.disabled = !client;
|
| 112 |
}
|
| 113 |
|
|
@@ -115,7 +125,8 @@ function stopCamera() {
|
|
| 115 |
for (const track of cameraStream?.getTracks() || []) track.stop();
|
| 116 |
cameraStream = null;
|
| 117 |
cameraPreview.srcObject = null;
|
| 118 |
-
|
|
|
|
| 119 |
cameraActions.hidden = true;
|
| 120 |
imagePreview.hidden = !selectedFile;
|
| 121 |
chooseImageButton.hidden = Boolean(selectedFile);
|
|
@@ -123,6 +134,20 @@ function stopCamera() {
|
|
| 123 |
reconstructButton.disabled = running || !selectedFile || !client;
|
| 124 |
}
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
async function startCamera() {
|
| 127 |
if (running || cameraStream) return;
|
| 128 |
if (!navigator.mediaDevices?.getUserMedia) {
|
|
@@ -139,7 +164,9 @@ async function startCamera() {
|
|
| 139 |
cameraPreview.srcObject = cameraStream;
|
| 140 |
imagePreview.hidden = true;
|
| 141 |
chooseImageButton.hidden = true;
|
| 142 |
-
|
|
|
|
|
|
|
| 143 |
cameraActions.hidden = false;
|
| 144 |
reconstructButton.disabled = true;
|
| 145 |
await cameraPreview.play();
|
|
@@ -159,9 +186,9 @@ async function startCamera() {
|
|
| 159 |
}
|
| 160 |
|
| 161 |
async function capturePhoto() {
|
| 162 |
-
const
|
| 163 |
-
const
|
| 164 |
-
if (!cameraStream || !
|
| 165 |
showToast("The camera is still starting. Try again in a moment.", true);
|
| 166 |
return;
|
| 167 |
}
|
|
@@ -169,13 +196,49 @@ async function capturePhoto() {
|
|
| 169 |
captureButton.disabled = true;
|
| 170 |
try {
|
| 171 |
const canvas = document.createElement("canvas");
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", 0.92));
|
| 176 |
if (!blob) throw new Error("The browser could not encode the captured photo.");
|
| 177 |
stopCamera();
|
| 178 |
-
selectFile(
|
|
|
|
|
|
|
|
|
|
| 179 |
} catch (error) {
|
| 180 |
console.error(error);
|
| 181 |
showToast("Could not capture the photo. Please try again.", true);
|
|
@@ -198,6 +261,8 @@ imagePreview.addEventListener("keydown", (event) => {
|
|
| 198 |
cameraButton.addEventListener("click", startCamera);
|
| 199 |
cancelCameraButton.addEventListener("click", stopCamera);
|
| 200 |
captureButton.addEventListener("click", capturePhoto);
|
|
|
|
|
|
|
| 201 |
window.addEventListener("pagehide", stopCamera);
|
| 202 |
|
| 203 |
if (!navigator.mediaDevices?.getUserMedia) {
|
|
|
|
| 5 |
const uploadSurface = document.getElementById("uploadSurface");
|
| 6 |
const chooseImageButton = document.getElementById("chooseImageButton");
|
| 7 |
const cameraButton = document.getElementById("cameraButton");
|
| 8 |
+
const cameraStage = document.getElementById("cameraStage");
|
| 9 |
const cameraPreview = document.getElementById("cameraPreview");
|
| 10 |
+
const cameraOptions = document.getElementById("cameraOptions");
|
| 11 |
+
const cameraFormatInputs = document.querySelectorAll('input[name="cameraFormat"]');
|
| 12 |
+
const cropToFrame = document.getElementById("cropToFrame");
|
| 13 |
+
const cameraOutputSize = document.getElementById("cameraOutputSize");
|
| 14 |
const cameraActions = document.getElementById("cameraActions");
|
| 15 |
const cancelCameraButton = document.getElementById("cancelCameraButton");
|
| 16 |
const captureButton = document.getElementById("captureButton");
|
|
|
|
| 34 |
let toastTimer = null;
|
| 35 |
let cameraStream = null;
|
| 36 |
const supportedImageTypes = new Set(["image/jpeg", "image/png", "image/webp"]);
|
| 37 |
+
const cameraFormats = {
|
| 38 |
+
landscape: { width: 1536, height: 1152 },
|
| 39 |
+
portrait: { width: 1152, height: 1536 },
|
| 40 |
+
};
|
| 41 |
|
| 42 |
const colorScheme = window.matchMedia("(prefers-color-scheme: dark)");
|
| 43 |
const requestedTheme = new URLSearchParams(window.location.search).get("__theme");
|
|
|
|
| 105 |
examplesGrid.toggleAttribute("inert", value);
|
| 106 |
}
|
| 107 |
|
| 108 |
+
function selectFile(file, fromCamera = false) {
|
| 109 |
if (!file || !supportedImageTypes.has(file.type)) {
|
| 110 |
showToast("Please choose a JPG, PNG, or WebP image.", true);
|
| 111 |
return;
|
|
|
|
| 117 |
imagePreview.src = previewUrl;
|
| 118 |
imagePreview.hidden = false;
|
| 119 |
chooseImageButton.hidden = true;
|
| 120 |
+
cameraButton.textContent = fromCamera ? "Retake photo" : "Use camera";
|
| 121 |
reconstructButton.disabled = !client;
|
| 122 |
}
|
| 123 |
|
|
|
|
| 125 |
for (const track of cameraStream?.getTracks() || []) track.stop();
|
| 126 |
cameraStream = null;
|
| 127 |
cameraPreview.srcObject = null;
|
| 128 |
+
cameraStage.hidden = true;
|
| 129 |
+
cameraOptions.hidden = true;
|
| 130 |
cameraActions.hidden = true;
|
| 131 |
imagePreview.hidden = !selectedFile;
|
| 132 |
chooseImageButton.hidden = Boolean(selectedFile);
|
|
|
|
| 134 |
reconstructButton.disabled = running || !selectedFile || !client;
|
| 135 |
}
|
| 136 |
|
| 137 |
+
function selectedCameraFormat() {
|
| 138 |
+
return document.querySelector('input[name="cameraFormat"]:checked')?.value || "landscape";
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
function updateCameraFrame() {
|
| 142 |
+
const format = selectedCameraFormat();
|
| 143 |
+
const dimensions = cameraFormats[format];
|
| 144 |
+
cameraStage.dataset.format = format;
|
| 145 |
+
cameraStage.dataset.crop = cropToFrame.checked ? "frame" : "full";
|
| 146 |
+
cameraOutputSize.textContent = cropToFrame.checked
|
| 147 |
+
? `Output crop: ${dimensions.width} × ${dimensions.height}`
|
| 148 |
+
: "Output: full camera image";
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
async function startCamera() {
|
| 152 |
if (running || cameraStream) return;
|
| 153 |
if (!navigator.mediaDevices?.getUserMedia) {
|
|
|
|
| 164 |
cameraPreview.srcObject = cameraStream;
|
| 165 |
imagePreview.hidden = true;
|
| 166 |
chooseImageButton.hidden = true;
|
| 167 |
+
updateCameraFrame();
|
| 168 |
+
cameraStage.hidden = false;
|
| 169 |
+
cameraOptions.hidden = false;
|
| 170 |
cameraActions.hidden = false;
|
| 171 |
reconstructButton.disabled = true;
|
| 172 |
await cameraPreview.play();
|
|
|
|
| 186 |
}
|
| 187 |
|
| 188 |
async function capturePhoto() {
|
| 189 |
+
const sourceWidth = cameraPreview.videoWidth;
|
| 190 |
+
const sourceHeight = cameraPreview.videoHeight;
|
| 191 |
+
if (!cameraStream || !sourceWidth || !sourceHeight) {
|
| 192 |
showToast("The camera is still starting. Try again in a moment.", true);
|
| 193 |
return;
|
| 194 |
}
|
|
|
|
| 196 |
captureButton.disabled = true;
|
| 197 |
try {
|
| 198 |
const canvas = document.createElement("canvas");
|
| 199 |
+
let sourceX = 0;
|
| 200 |
+
let sourceY = 0;
|
| 201 |
+
let cropWidth = sourceWidth;
|
| 202 |
+
let cropHeight = sourceHeight;
|
| 203 |
+
|
| 204 |
+
if (cropToFrame.checked) {
|
| 205 |
+
const dimensions = cameraFormats[selectedCameraFormat()];
|
| 206 |
+
const targetRatio = dimensions.width / dimensions.height;
|
| 207 |
+
const sourceRatio = sourceWidth / sourceHeight;
|
| 208 |
+
canvas.width = dimensions.width;
|
| 209 |
+
canvas.height = dimensions.height;
|
| 210 |
+
if (sourceRatio > targetRatio) {
|
| 211 |
+
cropWidth = sourceHeight * targetRatio;
|
| 212 |
+
sourceX = (sourceWidth - cropWidth) / 2;
|
| 213 |
+
} else {
|
| 214 |
+
cropHeight = sourceWidth / targetRatio;
|
| 215 |
+
sourceY = (sourceHeight - cropHeight) / 2;
|
| 216 |
+
}
|
| 217 |
+
} else {
|
| 218 |
+
canvas.width = sourceWidth;
|
| 219 |
+
canvas.height = sourceHeight;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
const context = canvas.getContext("2d");
|
| 223 |
+
if (!context) throw new Error("The browser could not create an image canvas.");
|
| 224 |
+
context.drawImage(
|
| 225 |
+
cameraPreview,
|
| 226 |
+
sourceX,
|
| 227 |
+
sourceY,
|
| 228 |
+
cropWidth,
|
| 229 |
+
cropHeight,
|
| 230 |
+
0,
|
| 231 |
+
0,
|
| 232 |
+
canvas.width,
|
| 233 |
+
canvas.height,
|
| 234 |
+
);
|
| 235 |
const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", 0.92));
|
| 236 |
if (!blob) throw new Error("The browser could not encode the captured photo.");
|
| 237 |
stopCamera();
|
| 238 |
+
selectFile(
|
| 239 |
+
new File([blob], `webcam-${Date.now()}.jpg`, { type: "image/jpeg" }),
|
| 240 |
+
true,
|
| 241 |
+
);
|
| 242 |
} catch (error) {
|
| 243 |
console.error(error);
|
| 244 |
showToast("Could not capture the photo. Please try again.", true);
|
|
|
|
| 261 |
cameraButton.addEventListener("click", startCamera);
|
| 262 |
cancelCameraButton.addEventListener("click", stopCamera);
|
| 263 |
captureButton.addEventListener("click", capturePhoto);
|
| 264 |
+
for (const input of cameraFormatInputs) input.addEventListener("change", updateCameraFrame);
|
| 265 |
+
cropToFrame.addEventListener("change", updateCameraFrame);
|
| 266 |
window.addEventListener("pagehide", stopCamera);
|
| 267 |
|
| 268 |
if (!navigator.mediaDevices?.getUserMedia) {
|
src/demo/server_frontend/index.html
CHANGED
|
@@ -13,7 +13,7 @@
|
|
| 13 |
? requestedTheme
|
| 14 |
: (prefersDark ? "dark" : "light");
|
| 15 |
</script>
|
| 16 |
-
<link rel="stylesheet" href="/infinisplat/assets/styles.css?v=
|
| 17 |
</head>
|
| 18 |
<body>
|
| 19 |
<main class="app-shell">
|
|
@@ -41,11 +41,31 @@
|
|
| 41 |
<div class="upload-surface" id="uploadSurface">
|
| 42 |
<input id="imageInput" type="file" accept="image/jpeg,image/png,image/webp">
|
| 43 |
<img id="imagePreview" alt="Selected input; activate to choose another image" role="button" tabindex="0" hidden>
|
| 44 |
-
<
|
|
|
|
|
|
|
| 45 |
<button class="upload-empty" id="chooseImageButton" type="button">
|
| 46 |
<strong>Choose an image</strong>
|
| 47 |
<small>JPG, PNG, or WebP</small>
|
| 48 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
<div class="camera-actions" id="cameraActions" hidden>
|
| 50 |
<button class="camera-cancel" id="cancelCameraButton" type="button">Cancel</button>
|
| 51 |
<button class="camera-capture" id="captureButton" type="button">Take photo</button>
|
|
@@ -91,6 +111,6 @@
|
|
| 91 |
</main>
|
| 92 |
|
| 93 |
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
| 94 |
-
<script type="module" src="/infinisplat/assets/app.js?v=
|
| 95 |
</body>
|
| 96 |
</html>
|
|
|
|
| 13 |
? requestedTheme
|
| 14 |
: (prefersDark ? "dark" : "light");
|
| 15 |
</script>
|
| 16 |
+
<link rel="stylesheet" href="/infinisplat/assets/styles.css?v=6">
|
| 17 |
</head>
|
| 18 |
<body>
|
| 19 |
<main class="app-shell">
|
|
|
|
| 41 |
<div class="upload-surface" id="uploadSurface">
|
| 42 |
<input id="imageInput" type="file" accept="image/jpeg,image/png,image/webp">
|
| 43 |
<img id="imagePreview" alt="Selected input; activate to choose another image" role="button" tabindex="0" hidden>
|
| 44 |
+
<div class="camera-stage" id="cameraStage" data-format="landscape" hidden>
|
| 45 |
+
<video id="cameraPreview" aria-label="Live camera preview" autoplay playsinline muted></video>
|
| 46 |
+
</div>
|
| 47 |
<button class="upload-empty" id="chooseImageButton" type="button">
|
| 48 |
<strong>Choose an image</strong>
|
| 49 |
<small>JPG, PNG, or WebP</small>
|
| 50 |
</button>
|
| 51 |
+
<div class="camera-options" id="cameraOptions" hidden>
|
| 52 |
+
<fieldset class="format-control">
|
| 53 |
+
<legend>Camera format</legend>
|
| 54 |
+
<label>
|
| 55 |
+
<input type="radio" name="cameraFormat" value="landscape" checked>
|
| 56 |
+
<span>Landscape 4:3</span>
|
| 57 |
+
</label>
|
| 58 |
+
<label>
|
| 59 |
+
<input type="radio" name="cameraFormat" value="portrait">
|
| 60 |
+
<span>Portrait 3:4</span>
|
| 61 |
+
</label>
|
| 62 |
+
</fieldset>
|
| 63 |
+
<label class="crop-control">
|
| 64 |
+
<input id="cropToFrame" type="checkbox" checked>
|
| 65 |
+
<span>Process visible frame only</span>
|
| 66 |
+
</label>
|
| 67 |
+
<small id="cameraOutputSize" aria-live="polite">Output crop: 1536 × 1152</small>
|
| 68 |
+
</div>
|
| 69 |
<div class="camera-actions" id="cameraActions" hidden>
|
| 70 |
<button class="camera-cancel" id="cancelCameraButton" type="button">Cancel</button>
|
| 71 |
<button class="camera-capture" id="captureButton" type="button">Take photo</button>
|
|
|
|
| 111 |
</main>
|
| 112 |
|
| 113 |
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
| 114 |
+
<script type="module" src="/infinisplat/assets/app.js?v=7"></script>
|
| 115 |
</body>
|
| 116 |
</html>
|
src/demo/server_frontend/styles.css
CHANGED
|
@@ -267,17 +267,41 @@ button, input { font: inherit; }
|
|
| 267 |
opacity: 0;
|
| 268 |
}
|
| 269 |
|
| 270 |
-
.upload-surface img
|
| 271 |
-
.upload-surface video {
|
| 272 |
width: 100%;
|
| 273 |
height: 510px;
|
| 274 |
object-fit: contain;
|
| 275 |
background: var(--surface-muted);
|
| 276 |
}
|
| 277 |
|
| 278 |
-
.upload-surface video { object-fit: cover; }
|
| 279 |
.upload-surface img { cursor: pointer; }
|
| 280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
.upload-empty {
|
| 282 |
display: flex;
|
| 283 |
flex-direction: column;
|
|
@@ -293,10 +317,85 @@ button, input { font: inherit; }
|
|
| 293 |
.upload-empty strong { color: var(--text); font-size: 0.9rem; }
|
| 294 |
.upload-empty small { font-size: 0.75rem; }
|
| 295 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
.camera-actions {
|
| 297 |
position: absolute;
|
| 298 |
bottom: 16px;
|
| 299 |
left: 50%;
|
|
|
|
| 300 |
display: flex;
|
| 301 |
gap: 8px;
|
| 302 |
padding: 7px;
|
|
@@ -545,7 +644,9 @@ button, input { font: inherit; }
|
|
| 545 |
.app-shell { width: calc(100% - 28px); }
|
| 546 |
.workspace { grid-template-columns: 1fr; }
|
| 547 |
.upload-surface { min-height: 420px; }
|
| 548 |
-
.upload-surface img, .
|
|
|
|
|
|
|
| 549 |
.examples-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
| 550 |
.example-item { grid-column: span 2 !important; }
|
| 551 |
}
|
|
@@ -561,7 +662,9 @@ button, input { font: inherit; }
|
|
| 561 |
.panel-content { padding: 9px; }
|
| 562 |
.source-actions, .download-bar { padding: 0 9px 9px; }
|
| 563 |
.upload-surface { min-height: 360px; }
|
| 564 |
-
.upload-surface img, .
|
|
|
|
|
|
|
| 565 |
.examples-section { margin-bottom: 24px; padding: 0; }
|
| 566 |
.examples-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); grid-auto-rows: 132px; }
|
| 567 |
.example-item { grid-column: span 2 !important; }
|
|
|
|
| 267 |
opacity: 0;
|
| 268 |
}
|
| 269 |
|
| 270 |
+
.upload-surface img {
|
|
|
|
| 271 |
width: 100%;
|
| 272 |
height: 510px;
|
| 273 |
object-fit: contain;
|
| 274 |
background: var(--surface-muted);
|
| 275 |
}
|
| 276 |
|
|
|
|
| 277 |
.upload-surface img { cursor: pointer; }
|
| 278 |
|
| 279 |
+
.camera-stage {
|
| 280 |
+
position: relative;
|
| 281 |
+
width: min(100%, 680px);
|
| 282 |
+
max-width: 100%;
|
| 283 |
+
overflow: hidden;
|
| 284 |
+
aspect-ratio: 4 / 3;
|
| 285 |
+
background: #050706;
|
| 286 |
+
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16);
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.camera-stage[data-format="portrait"] {
|
| 290 |
+
width: auto;
|
| 291 |
+
height: 510px;
|
| 292 |
+
aspect-ratio: 3 / 4;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.camera-stage video {
|
| 296 |
+
display: block;
|
| 297 |
+
width: 100%;
|
| 298 |
+
height: 100%;
|
| 299 |
+
object-fit: cover;
|
| 300 |
+
object-position: center;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.camera-stage[data-crop="full"] video { object-fit: contain; }
|
| 304 |
+
|
| 305 |
.upload-empty {
|
| 306 |
display: flex;
|
| 307 |
flex-direction: column;
|
|
|
|
| 317 |
.upload-empty strong { color: var(--text); font-size: 0.9rem; }
|
| 318 |
.upload-empty small { font-size: 0.75rem; }
|
| 319 |
|
| 320 |
+
.camera-options {
|
| 321 |
+
position: absolute;
|
| 322 |
+
top: 12px;
|
| 323 |
+
left: 50%;
|
| 324 |
+
z-index: 2;
|
| 325 |
+
display: grid;
|
| 326 |
+
justify-items: center;
|
| 327 |
+
gap: 7px;
|
| 328 |
+
width: min(94%, 390px);
|
| 329 |
+
padding: 8px;
|
| 330 |
+
border: 1px solid var(--border-strong);
|
| 331 |
+
border-radius: 8px;
|
| 332 |
+
background: color-mix(in srgb, var(--surface) 90%, transparent);
|
| 333 |
+
box-shadow: var(--panel-shadow);
|
| 334 |
+
color: var(--text);
|
| 335 |
+
transform: translateX(-50%);
|
| 336 |
+
backdrop-filter: blur(8px);
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.format-control {
|
| 340 |
+
display: grid;
|
| 341 |
+
grid-template-columns: 1fr 1fr;
|
| 342 |
+
width: 100%;
|
| 343 |
+
margin: 0;
|
| 344 |
+
padding: 0;
|
| 345 |
+
border: 0;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.format-control legend {
|
| 349 |
+
position: absolute;
|
| 350 |
+
width: 1px;
|
| 351 |
+
height: 1px;
|
| 352 |
+
overflow: hidden;
|
| 353 |
+
clip: rect(0 0 0 0);
|
| 354 |
+
white-space: nowrap;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.format-control label { cursor: pointer; }
|
| 358 |
+
.format-control input { position: absolute; opacity: 0; }
|
| 359 |
+
|
| 360 |
+
.format-control span {
|
| 361 |
+
display: grid;
|
| 362 |
+
place-items: center;
|
| 363 |
+
min-height: 34px;
|
| 364 |
+
padding: 0 10px;
|
| 365 |
+
border: 1px solid var(--border-strong);
|
| 366 |
+
background: var(--surface);
|
| 367 |
+
color: var(--muted);
|
| 368 |
+
font-size: 0.74rem;
|
| 369 |
+
font-weight: 650;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
.format-control label:first-of-type span { border-radius: 5px 0 0 5px; }
|
| 373 |
+
.format-control label:last-of-type span { border-left: 0; border-radius: 0 5px 5px 0; }
|
| 374 |
+
.format-control input:checked + span {
|
| 375 |
+
border-color: var(--primary);
|
| 376 |
+
background: var(--ready-bg);
|
| 377 |
+
color: var(--ready-text);
|
| 378 |
+
}
|
| 379 |
+
.format-control input:focus-visible + span { outline: 2px solid var(--primary); outline-offset: 2px; }
|
| 380 |
+
|
| 381 |
+
.crop-control {
|
| 382 |
+
display: flex;
|
| 383 |
+
align-items: center;
|
| 384 |
+
gap: 7px;
|
| 385 |
+
color: var(--text);
|
| 386 |
+
font-size: 0.73rem;
|
| 387 |
+
font-weight: 600;
|
| 388 |
+
cursor: pointer;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.crop-control input { accent-color: var(--primary); }
|
| 392 |
+
.camera-options small { color: var(--muted); font-size: 0.67rem; }
|
| 393 |
+
|
| 394 |
.camera-actions {
|
| 395 |
position: absolute;
|
| 396 |
bottom: 16px;
|
| 397 |
left: 50%;
|
| 398 |
+
z-index: 2;
|
| 399 |
display: flex;
|
| 400 |
gap: 8px;
|
| 401 |
padding: 7px;
|
|
|
|
| 644 |
.app-shell { width: calc(100% - 28px); }
|
| 645 |
.workspace { grid-template-columns: 1fr; }
|
| 646 |
.upload-surface { min-height: 420px; }
|
| 647 |
+
.upload-surface img, .viewer-shell { height: 420px; }
|
| 648 |
+
.camera-stage[data-format="landscape"] { width: min(100%, 560px); }
|
| 649 |
+
.camera-stage[data-format="portrait"] { height: 420px; }
|
| 650 |
.examples-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
| 651 |
.example-item { grid-column: span 2 !important; }
|
| 652 |
}
|
|
|
|
| 662 |
.panel-content { padding: 9px; }
|
| 663 |
.source-actions, .download-bar { padding: 0 9px 9px; }
|
| 664 |
.upload-surface { min-height: 360px; }
|
| 665 |
+
.upload-surface img, .viewer-shell { height: 360px; }
|
| 666 |
+
.camera-stage[data-format="landscape"] { width: min(100%, 480px); }
|
| 667 |
+
.camera-stage[data-format="portrait"] { height: 360px; }
|
| 668 |
.examples-section { margin-bottom: 24px; padding: 0; }
|
| 669 |
.examples-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); grid-auto-rows: 132px; }
|
| 670 |
.example-item { grid-column: span 2 !important; }
|
tests/test_public_configs.py
CHANGED
|
@@ -4,7 +4,11 @@ from hydra import compose, initialize_config_dir
|
|
| 4 |
from hydra.core.global_hydra import GlobalHydra
|
| 5 |
|
| 6 |
from src.demo.config import load_typed_root_config
|
| 7 |
-
from src.demo.infer_single_image import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
|
| 10 |
def _load_experiment(name: str):
|
|
@@ -45,5 +49,11 @@ def test_lidar_experiment_loads() -> None:
|
|
| 45 |
assert cfg.model.encoder.name == "infinisplat_infinidepth"
|
| 46 |
|
| 47 |
|
| 48 |
-
def
|
| 49 |
assert (INFERENCE_HEIGHT, INFERENCE_WIDTH) == (1152, 1536)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
from hydra.core.global_hydra import GlobalHydra
|
| 5 |
|
| 6 |
from src.demo.config import load_typed_root_config
|
| 7 |
+
from src.demo.infer_single_image import (
|
| 8 |
+
INFERENCE_HEIGHT,
|
| 9 |
+
INFERENCE_WIDTH,
|
| 10 |
+
resolve_inference_shape,
|
| 11 |
+
)
|
| 12 |
|
| 13 |
|
| 14 |
def _load_experiment(name: str):
|
|
|
|
| 49 |
assert cfg.model.encoder.name == "infinisplat_infinidepth"
|
| 50 |
|
| 51 |
|
| 52 |
+
def test_landscape_inference_resolution_is_hardcoded() -> None:
|
| 53 |
assert (INFERENCE_HEIGHT, INFERENCE_WIDTH) == (1152, 1536)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def test_inference_resolution_preserves_orientation() -> None:
|
| 57 |
+
assert resolve_inference_shape(image_width=1600, image_height=900) == (1152, 1536)
|
| 58 |
+
assert resolve_inference_shape(image_width=900, image_height=1600) == (1536, 1152)
|
| 59 |
+
assert resolve_inference_shape(image_width=1000, image_height=1000) == (1152, 1536)
|
tests/test_webcam_frontend.py
CHANGED
|
@@ -12,6 +12,9 @@ def test_custom_frontend_exposes_camera_controls() -> None:
|
|
| 12 |
assert 'id="cameraPreview"' in markup
|
| 13 |
assert 'id="captureButton"' in markup
|
| 14 |
assert 'id="cancelCameraButton"' in markup
|
|
|
|
|
|
|
|
|
|
| 15 |
assert "autoplay playsinline muted" in markup
|
| 16 |
|
| 17 |
|
|
@@ -20,6 +23,9 @@ def test_custom_frontend_captures_camera_as_jpeg_file() -> None:
|
|
| 20 |
|
| 21 |
assert "navigator.mediaDevices.getUserMedia" in script
|
| 22 |
assert 'canvas.toBlob(resolve, "image/jpeg", 0.92)' in script
|
|
|
|
|
|
|
|
|
|
| 23 |
assert "new File([blob]" in script
|
| 24 |
assert "stopCamera();" in script
|
| 25 |
|
|
|
|
| 12 |
assert 'id="cameraPreview"' in markup
|
| 13 |
assert 'id="captureButton"' in markup
|
| 14 |
assert 'id="cancelCameraButton"' in markup
|
| 15 |
+
assert 'name="cameraFormat" value="landscape"' in markup
|
| 16 |
+
assert 'name="cameraFormat" value="portrait"' in markup
|
| 17 |
+
assert 'id="cropToFrame" type="checkbox" checked' in markup
|
| 18 |
assert "autoplay playsinline muted" in markup
|
| 19 |
|
| 20 |
|
|
|
|
| 23 |
|
| 24 |
assert "navigator.mediaDevices.getUserMedia" in script
|
| 25 |
assert 'canvas.toBlob(resolve, "image/jpeg", 0.92)' in script
|
| 26 |
+
assert "targetRatio = dimensions.width / dimensions.height" in script
|
| 27 |
+
assert "cropWidth = sourceHeight * targetRatio" in script
|
| 28 |
+
assert "cropHeight = sourceWidth / targetRatio" in script
|
| 29 |
assert "new File([blob]" in script
|
| 30 |
assert "stopCamera();" in script
|
| 31 |
|