ricklon commited on
Commit
63fbf4b
·
1 Parent(s): 0a8a91d

Add portrait-aware webcam cropping

Browse files
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, and 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
 
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 = INFERENCE_HEIGHT
272
- target_width = INFERENCE_WIDTH
 
 
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
- cameraPreview.hidden = true;
 
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
- cameraPreview.hidden = false;
 
 
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 width = cameraPreview.videoWidth;
163
- const height = cameraPreview.videoHeight;
164
- if (!cameraStream || !width || !height) {
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
- canvas.width = width;
173
- canvas.height = height;
174
- canvas.getContext("2d").drawImage(cameraPreview, 0, 0, width, height);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(new File([blob], `webcam-${Date.now()}.jpg`, { type: "image/jpeg" }));
 
 
 
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=5">
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
- <video id="cameraPreview" aria-label="Live camera preview" autoplay playsinline muted hidden></video>
 
 
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=6"></script>
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, .upload-surface video, .viewer-shell { height: 420px; }
 
 
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, .upload-surface video, .viewer-shell { height: 360px; }
 
 
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 INFERENCE_HEIGHT, INFERENCE_WIDTH
 
 
 
 
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 test_inference_resolution_is_hardcoded() -> None:
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