Realcat commited on
Commit
ec8aa58
Β·
verified Β·
1 Parent(s): d09096d

Update imcui/ui/app_class.py

Browse files
Files changed (1) hide show
  1. imcui/ui/app_class.py +824 -817
imcui/ui/app_class.py CHANGED
@@ -1,817 +1,824 @@
1
- from pathlib import Path
2
- from typing import Any, Dict, Optional, Tuple
3
- import spaces
4
- import gradio as gr
5
- import numpy as np
6
- from easydict import EasyDict as edict
7
- from omegaconf import OmegaConf
8
-
9
- from .sfm import SfmEngine
10
- from .utils import (
11
- GRADIO_VERSION,
12
- gen_examples,
13
- generate_warp_images,
14
- get_matcher_zoo,
15
- load_config,
16
- ransac_zoo,
17
- run_matching,
18
- run_ransac,
19
- send_to_match,
20
- )
21
-
22
- DESCRIPTION = """
23
- # Image Matching WebUI
24
- This Space demonstrates [Image Matching WebUI](https://github.com/Vincentqyw/image-matching-webui) by vincent qin. Feel free to play with it, or duplicate to run image matching without a queue!
25
- <br/>
26
-
27
- πŸš€ **Now GPU-accelerated!** Thanks to HuggingFace's community grant, all algorithms run on GPU for fast, responsive inference.
28
-
29
- πŸ”Ž For more details about supported local features and matchers, please refer to https://github.com/Vincentqyw/image-matching-webui
30
-
31
- πŸ› Your feedback is valuable to me. Please do not hesitate to report any bugs [here](https://github.com/Vincentqyw/image-matching-webui/issues).
32
- """
33
-
34
- CSS = """
35
- #warning {background-color: #FFCCCB}
36
- .logs_class textarea {font-size: 12px !important}
37
- """
38
-
39
-
40
- class ImageMatchingApp:
41
- def __init__(self, server_name="0.0.0.0", server_port=7860, **kwargs):
42
- self.server_name = server_name
43
- self.server_port = server_port
44
- self.config_path = kwargs.get("config", Path(__file__).parent / "config.yaml")
45
- self.cfg = load_config(self.config_path)
46
- self.matcher_zoo = get_matcher_zoo(self.cfg["matcher_zoo"])
47
- self.app = None
48
- self.example_data_root = kwargs.get(
49
- "example_data_root", Path(__file__).parents[1] / "datasets"
50
- )
51
- # final step
52
- self.init_interface()
53
-
54
- def init_matcher_dropdown(self):
55
- algos = []
56
- for k, v in self.cfg["matcher_zoo"].items():
57
- if v.get("enable", True):
58
- algos.append(k)
59
- return algos
60
-
61
- def init_interface(self):
62
- with gr.Blocks(css=CSS) as self.app:
63
- with gr.Tab("Image Matching"):
64
- with gr.Row():
65
- with gr.Column(scale=1):
66
- gr.Image(
67
- str(Path(__file__).parent.parent / "assets/logo.webp"),
68
- elem_id="logo-img",
69
- show_label=False,
70
- show_share_button=False,
71
- show_download_button=False,
72
- )
73
- with gr.Column(scale=3):
74
- gr.Markdown(DESCRIPTION)
75
- with gr.Row(equal_height=False):
76
- with gr.Column():
77
- with gr.Row():
78
- matcher_list = gr.Dropdown(
79
- choices=self.init_matcher_dropdown(),
80
- value="disk+lightglue",
81
- label="Matching Model",
82
- interactive=True,
83
- )
84
- match_image_src = gr.Radio(
85
- (
86
- ["upload", "webcam", "clipboard"]
87
- if GRADIO_VERSION > "3"
88
- else ["upload", "webcam", "canvas"]
89
- ),
90
- label="Image Source",
91
- value="upload",
92
- )
93
- with gr.Row():
94
- input_image0 = gr.Image(
95
- label="Image 0",
96
- type="numpy",
97
- image_mode="RGB",
98
- height=300 if GRADIO_VERSION > "3" else None,
99
- interactive=True,
100
- )
101
- input_image1 = gr.Image(
102
- label="Image 1",
103
- type="numpy",
104
- image_mode="RGB",
105
- height=300 if GRADIO_VERSION > "3" else None,
106
- interactive=True,
107
- )
108
-
109
- with gr.Row():
110
- button_reset = gr.Button(value="Reset")
111
- button_run = gr.Button(value="Run Match", variant="primary")
112
- with gr.Row():
113
- button_stop = gr.Button(value="Force Stop", variant="stop")
114
-
115
- with gr.Accordion("Advanced Setting", open=False):
116
- with gr.Accordion("Image Setting", open=True):
117
- with gr.Row():
118
- image_force_resize_cb = gr.Checkbox(
119
- label="Force Resize",
120
- value=False,
121
- interactive=True,
122
- )
123
- image_setting_height = gr.Slider(
124
- minimum=48,
125
- maximum=2048,
126
- step=16,
127
- label="Image Height",
128
- value=480,
129
- visible=False,
130
- )
131
- image_setting_width = gr.Slider(
132
- minimum=64,
133
- maximum=2048,
134
- step=16,
135
- label="Image Width",
136
- value=640,
137
- visible=False,
138
- )
139
- with gr.Accordion("Matching Setting", open=True):
140
- with gr.Row():
141
- match_setting_threshold = gr.Slider(
142
- minimum=0.0,
143
- maximum=1,
144
- step=0.001,
145
- label="Match threshold",
146
- value=0.1,
147
- )
148
- match_setting_max_keypoints = gr.Slider(
149
- minimum=10,
150
- maximum=10000,
151
- step=10,
152
- label="Max features",
153
- value=1000,
154
- )
155
- # TODO: add line settings
156
- with gr.Row():
157
- detect_keypoints_threshold = gr.Slider(
158
- minimum=0,
159
- maximum=1,
160
- step=0.001,
161
- label="Keypoint threshold",
162
- value=0.015,
163
- )
164
- detect_line_threshold = ( # noqa: F841
165
- gr.Slider(
166
- minimum=0.1,
167
- maximum=1,
168
- step=0.01,
169
- label="Line threshold",
170
- value=0.2,
171
- )
172
- )
173
-
174
- with gr.Accordion("RANSAC Setting", open=True):
175
- with gr.Row(equal_height=False):
176
- ransac_method = gr.Dropdown(
177
- choices=ransac_zoo.keys(),
178
- value=self.cfg["defaults"]["ransac_method"],
179
- label="RANSAC Method",
180
- interactive=True,
181
- )
182
- ransac_reproj_threshold = gr.Slider(
183
- minimum=0.0,
184
- maximum=12,
185
- step=0.01,
186
- label="Ransac Reproj threshold",
187
- value=8.0,
188
- )
189
- ransac_confidence = gr.Slider(
190
- minimum=0.0,
191
- maximum=1,
192
- step=0.00001,
193
- label="Ransac Confidence",
194
- value=self.cfg["defaults"]["ransac_confidence"],
195
- )
196
- ransac_max_iter = gr.Slider(
197
- minimum=0.0,
198
- maximum=100000,
199
- step=100,
200
- label="Ransac Iterations",
201
- value=self.cfg["defaults"]["ransac_max_iter"],
202
- )
203
- button_ransac = gr.Button(
204
- value="Rerun RANSAC", variant="primary"
205
- )
206
- with gr.Accordion("Geometry Setting", open=False):
207
- with gr.Row(equal_height=False):
208
- choice_geometry_type = gr.Radio(
209
- ["Fundamental", "Homography"],
210
- label="Reconstruct Geometry",
211
- value=self.cfg["defaults"]["setting_geometry"],
212
- )
213
- # image resize
214
- image_force_resize_cb.select(
215
- fn=self._on_select_force_resize,
216
- inputs=image_force_resize_cb,
217
- outputs=[image_setting_width, image_setting_height],
218
- )
219
- # collect inputs
220
- state_cache = gr.State({})
221
- inputs = [
222
- input_image0,
223
- input_image1,
224
- match_setting_threshold,
225
- match_setting_max_keypoints,
226
- detect_keypoints_threshold,
227
- matcher_list,
228
- ransac_method,
229
- ransac_reproj_threshold,
230
- ransac_confidence,
231
- ransac_max_iter,
232
- choice_geometry_type,
233
- gr.State(self.matcher_zoo),
234
- image_force_resize_cb,
235
- image_setting_width,
236
- image_setting_height,
237
- ]
238
-
239
- # Add some examples
240
- with gr.Row():
241
- # Example inputs
242
- with gr.Accordion("Open for More: Examples", open=True):
243
- gr.Examples(
244
- examples=gen_examples(self.example_data_root),
245
- inputs=inputs,
246
- outputs=[],
247
- fn=run_matching,
248
- cache_examples=False,
249
- label=(
250
- "Examples (click one of the images below to Run"
251
- " Match). Thx: WxBS"
252
- ),
253
- )
254
- with gr.Accordion("Supported Algorithms", open=False):
255
- # add a table of supported algorithms
256
- self.display_supported_algorithms()
257
-
258
- with gr.Column():
259
- with gr.Accordion("Open for More: Keypoints", open=True):
260
- output_keypoints = gr.Image(label="Keypoints", type="numpy")
261
- with gr.Accordion(
262
- (
263
- "Open for More: Raw Matches"
264
- " (Green for good matches, Red for bad)"
265
- ),
266
- open=False,
267
- ):
268
- output_matches_raw = gr.Image(
269
- label="Raw Matches",
270
- type="numpy",
271
- )
272
- with gr.Accordion(
273
- (
274
- "Open for More: Ransac Matches"
275
- " (Green for good matches, Red for bad)"
276
- ),
277
- open=True,
278
- ):
279
- output_matches_ransac = gr.Image(
280
- label="Ransac Matches", type="numpy"
281
- )
282
- with gr.Accordion(
283
- "Open for More: Matches Statistics", open=False
284
- ):
285
- output_pred = gr.File(label="Outputs", elem_id="download")
286
- matches_result_info = gr.JSON(label="Matches Statistics")
287
- matcher_info = gr.JSON(label="Match info")
288
-
289
- with gr.Accordion("Open for More: Warped Image", open=True):
290
- output_wrapped = gr.Image(
291
- label="Wrapped Pair", type="numpy"
292
- )
293
- # send to input
294
- button_rerun = gr.Button(
295
- value="Send to Input Match Pair",
296
- variant="primary",
297
- )
298
- with gr.Accordion(
299
- "Open for More: Geometry info", open=False
300
- ):
301
- geometry_result = gr.JSON(
302
- label="Reconstructed Geometry"
303
- )
304
-
305
- # callbacks
306
- match_image_src.change(
307
- fn=self.ui_change_imagebox,
308
- inputs=match_image_src,
309
- outputs=input_image0,
310
- )
311
- match_image_src.change(
312
- fn=self.ui_change_imagebox,
313
- inputs=match_image_src,
314
- outputs=input_image1,
315
- )
316
- # collect outputs
317
- outputs = [
318
- output_keypoints,
319
- output_matches_raw,
320
- output_matches_ransac,
321
- matches_result_info,
322
- matcher_info,
323
- geometry_result,
324
- output_wrapped,
325
- state_cache,
326
- output_pred,
327
- ]
328
- # button callbacks
329
- click_event = button_run.click(
330
- fn=run_matching, inputs=inputs, outputs=outputs
331
- )
332
- # stop button
333
- button_stop.click(
334
- fn=None, inputs=None, outputs=None, cancels=[click_event]
335
- )
336
-
337
- # Reset images
338
- reset_outputs = [
339
- input_image0,
340
- input_image1,
341
- match_setting_threshold,
342
- match_setting_max_keypoints,
343
- detect_keypoints_threshold,
344
- matcher_list,
345
- input_image0,
346
- input_image1,
347
- match_image_src,
348
- output_keypoints,
349
- output_matches_raw,
350
- output_matches_ransac,
351
- matches_result_info,
352
- matcher_info,
353
- output_wrapped,
354
- geometry_result,
355
- ransac_method,
356
- ransac_reproj_threshold,
357
- ransac_confidence,
358
- ransac_max_iter,
359
- choice_geometry_type,
360
- output_pred,
361
- image_force_resize_cb,
362
- ]
363
- button_reset.click(
364
- fn=self.ui_reset_state,
365
- inputs=None,
366
- outputs=reset_outputs,
367
- )
368
-
369
- # run ransac button action
370
- button_ransac.click(
371
- fn=run_ransac,
372
- inputs=[
373
- state_cache,
374
- choice_geometry_type,
375
- ransac_method,
376
- ransac_reproj_threshold,
377
- ransac_confidence,
378
- ransac_max_iter,
379
- ],
380
- outputs=[
381
- output_matches_ransac,
382
- matches_result_info,
383
- output_wrapped,
384
- output_pred,
385
- ],
386
- )
387
-
388
- # send warped image to match
389
- button_rerun.click(
390
- fn=send_to_match,
391
- inputs=[state_cache],
392
- outputs=[input_image0, input_image1],
393
- )
394
-
395
- # estimate geo
396
- choice_geometry_type.change(
397
- fn=generate_warp_images,
398
- inputs=[
399
- input_image0,
400
- input_image1,
401
- geometry_result,
402
- choice_geometry_type,
403
- ],
404
- outputs=[output_wrapped, geometry_result],
405
- )
406
- with gr.Tab("Structure from Motion(under-dev)"):
407
- sfm_ui = AppSfmUI( # noqa: F841
408
- {
409
- **self.cfg,
410
- "matcher_zoo": self.matcher_zoo,
411
- "outputs": "experiments/sfm",
412
- }
413
- )
414
- sfm_ui.call_empty()
415
-
416
- def run(self):
417
- self.app.queue().launch(
418
- server_name=self.server_name,
419
- server_port=self.server_port,
420
- share=False,
421
- allowed_paths=[
422
- str(Path(__file__).parents[0]),
423
- str(Path(__file__).parents[1]),
424
- ],
425
- )
426
-
427
- def ui_change_imagebox(self, choice):
428
- """
429
- Updates the image box with the given choice.
430
-
431
- Args:
432
- choice (list): The list of image sources to be displayed in the image box.
433
-
434
- Returns:
435
- dict: A dictionary containing the updated value, sources, and type for the image box.
436
- """
437
- ret_dict = {
438
- "value": None, # The updated value of the image box
439
- "__type__": "update", # The type of update for the image box
440
- }
441
- if GRADIO_VERSION > "3":
442
- return {
443
- **ret_dict,
444
- "sources": choice, # The list of image sources to be displayed
445
- }
446
- else:
447
- return {
448
- **ret_dict,
449
- "source": choice, # The list of image sources to be displayed
450
- }
451
-
452
- def _on_select_force_resize(self, visible: bool = False):
453
- return gr.update(visible=visible), gr.update(visible=visible)
454
-
455
- def ui_reset_state(
456
- self,
457
- *args: Any,
458
- ) -> Tuple[
459
- Optional[np.ndarray],
460
- Optional[np.ndarray],
461
- float,
462
- int,
463
- float,
464
- str,
465
- Dict[str, Any],
466
- Dict[str, Any],
467
- str,
468
- Optional[np.ndarray],
469
- Optional[np.ndarray],
470
- Optional[np.ndarray],
471
- Dict[str, Any],
472
- Dict[str, Any],
473
- Optional[np.ndarray],
474
- Dict[str, Any],
475
- str,
476
- int,
477
- float,
478
- int,
479
- bool,
480
- ]:
481
- """
482
- Reset the state of the UI.
483
-
484
- Returns:
485
- tuple: A tuple containing the initial values for the UI state.
486
- """
487
- key: str = list(self.matcher_zoo.keys())[
488
- 0
489
- ] # Get the first key from matcher_zoo
490
- # flush_logs()
491
- return (
492
- None, # image0: Optional[np.ndarray]
493
- None, # image1: Optional[np.ndarray]
494
- self.cfg["defaults"]["match_threshold"], # matching_threshold: float
495
- self.cfg["defaults"]["max_keypoints"], # max_keypoints: int
496
- self.cfg["defaults"]["keypoint_threshold"], # keypoint_threshold: float
497
- key, # matcher: str
498
- self.ui_change_imagebox("upload"), # input image0: Dict[str, Any]
499
- self.ui_change_imagebox("upload"), # input image1: Dict[str, Any]
500
- "upload", # match_image_src: str
501
- None, # keypoints: Optional[np.ndarray]
502
- None, # raw matches: Optional[np.ndarray]
503
- None, # ransac matches: Optional[np.ndarray]
504
- {}, # matches result info: Dict[str, Any]
505
- {}, # matcher config: Dict[str, Any]
506
- None, # warped image: Optional[np.ndarray]
507
- {}, # geometry result: Dict[str, Any]
508
- self.cfg["defaults"]["ransac_method"], # ransac_method: str
509
- self.cfg["defaults"][
510
- "ransac_reproj_threshold"
511
- ], # ransac_reproj_threshold: float
512
- self.cfg["defaults"]["ransac_confidence"], # ransac_confidence: float
513
- self.cfg["defaults"]["ransac_max_iter"], # ransac_max_iter: int
514
- self.cfg["defaults"]["setting_geometry"], # geometry: str
515
- None, # predictions
516
- False,
517
- )
518
-
519
- def display_supported_algorithms(self, style="tab"):
520
- def get_link(link, tag="Link"):
521
- return "[{}]({})".format(tag, link) if link is not None else "None"
522
-
523
- data = []
524
- cfg = self.cfg["matcher_zoo"]
525
- if style == "md":
526
- markdown_table = "| Algo. | Conference | Code | Project | Paper |\n"
527
- markdown_table += "| ----- | ---------- | ---- | ------- | ----- |\n"
528
-
529
- for _, v in cfg.items():
530
- if not v["info"].get("display", True):
531
- continue
532
- github_link = get_link(v["info"].get("github", ""))
533
- project_link = get_link(v["info"].get("project", ""))
534
- paper_link = get_link(
535
- v["info"]["paper"],
536
- (
537
- Path(v["info"]["paper"]).name[-10:]
538
- if v["info"]["paper"] is not None
539
- else "Link"
540
- ),
541
- )
542
-
543
- markdown_table += "{}|{}|{}|{}|{}\n".format(
544
- v["info"].get("name", ""),
545
- v["info"].get("source", ""),
546
- github_link,
547
- project_link,
548
- paper_link,
549
- )
550
- return gr.Markdown(markdown_table)
551
- elif style == "tab":
552
- for k, v in cfg.items():
553
- if not v["info"].get("display", True):
554
- continue
555
- data.append(
556
- [
557
- v["info"].get("name", ""),
558
- v["info"].get("source", ""),
559
- v["info"].get("github", ""),
560
- v["info"].get("paper", ""),
561
- v["info"].get("project", ""),
562
- ]
563
- )
564
- tab = gr.Dataframe(
565
- headers=["Algo.", "Conference", "Code", "Paper", "Project"],
566
- datatype=["str", "str", "str", "str", "str"],
567
- col_count=(5, "fixed"),
568
- value=data,
569
- # wrap=True,
570
- # min_width = 1000,
571
- # height=1000,
572
- )
573
- return tab
574
-
575
-
576
- class AppBaseUI:
577
- def __init__(self, cfg: Dict[str, Any] = {}):
578
- self.cfg = OmegaConf.create(cfg)
579
- self.inputs = edict({})
580
- self.outputs = edict({})
581
- self.ui = edict({})
582
-
583
- def _init_ui(self):
584
- NotImplemented
585
-
586
- def call(self, **kwargs):
587
- NotImplemented
588
-
589
- def info(self):
590
- gr.Info("SFM is under construction.")
591
-
592
-
593
- class AppSfmUI(AppBaseUI):
594
- def __init__(self, cfg: Dict[str, Any] = None):
595
- super().__init__(cfg)
596
- assert "matcher_zoo" in self.cfg
597
- self.matcher_zoo = self.cfg["matcher_zoo"]
598
- self.sfm_engine = SfmEngine(cfg)
599
- self._init_ui()
600
-
601
- def init_retrieval_dropdown(self):
602
- algos = []
603
- for k, v in self.cfg["retrieval_zoo"].items():
604
- if v.get("enable", True):
605
- algos.append(k)
606
- return algos
607
-
608
- def _update_options(self, option):
609
- if option == "sparse":
610
- return gr.Textbox("sparse", visible=True)
611
- elif option == "dense":
612
- return gr.Textbox("dense", visible=True)
613
- else:
614
- return gr.Textbox("not set", visible=True)
615
-
616
- def _on_select_custom_params(self, value: bool = False):
617
- return gr.update(visible=value)
618
-
619
- def _init_ui(self):
620
- with gr.Row():
621
- # data settting and camera settings
622
- with gr.Column():
623
- self.inputs.input_images = gr.File(
624
- label="SfM",
625
- interactive=True,
626
- file_count="multiple",
627
- min_width=300,
628
- )
629
- # camera setting
630
- with gr.Accordion("Camera Settings", open=True):
631
- with gr.Column():
632
- with gr.Row():
633
- with gr.Column():
634
- self.inputs.camera_model = gr.Dropdown(
635
- choices=[
636
- "PINHOLE",
637
- "SIMPLE_RADIAL",
638
- "OPENCV",
639
- ],
640
- value="PINHOLE",
641
- label="Camera Model",
642
- interactive=True,
643
- )
644
- with gr.Column():
645
- gr.Checkbox(
646
- label="Shared Params",
647
- value=True,
648
- interactive=True,
649
- )
650
- camera_custom_params_cb = gr.Checkbox(
651
- label="Custom Params",
652
- value=False,
653
- interactive=True,
654
- )
655
- with gr.Row():
656
- self.inputs.camera_params = gr.Textbox(
657
- label="Camera Params",
658
- value="0,0,0,0",
659
- interactive=False,
660
- visible=False,
661
- )
662
- camera_custom_params_cb.select(
663
- fn=self._on_select_custom_params,
664
- inputs=camera_custom_params_cb,
665
- outputs=self.inputs.camera_params,
666
- )
667
-
668
- with gr.Accordion("Matching Settings", open=True):
669
- # feature extraction and matching setting
670
- with gr.Row():
671
- # matcher setting
672
- self.inputs.matcher_key = gr.Dropdown(
673
- choices=self.matcher_zoo.keys(),
674
- value="disk+lightglue",
675
- label="Matching Model",
676
- interactive=True,
677
- )
678
- with gr.Row():
679
- with gr.Accordion("Advanced Settings", open=False):
680
- with gr.Column():
681
- with gr.Row():
682
- # matching setting
683
- self.inputs.max_keypoints = gr.Slider(
684
- label="Max Keypoints",
685
- minimum=100,
686
- maximum=10000,
687
- value=1000,
688
- interactive=True,
689
- )
690
- self.inputs.keypoint_threshold = gr.Slider(
691
- label="Keypoint Threshold",
692
- minimum=0,
693
- maximum=1,
694
- value=0.01,
695
- )
696
- with gr.Row():
697
- self.inputs.match_threshold = gr.Slider(
698
- label="Match Threshold",
699
- minimum=0.01,
700
- maximum=12.0,
701
- value=0.2,
702
- )
703
- self.inputs.ransac_threshold = gr.Slider(
704
- label="Ransac Threshold",
705
- minimum=0.01,
706
- maximum=12.0,
707
- value=4.0,
708
- step=0.01,
709
- interactive=True,
710
- )
711
-
712
- with gr.Row():
713
- self.inputs.ransac_confidence = gr.Slider(
714
- label="Ransac Confidence",
715
- minimum=0.01,
716
- maximum=1.0,
717
- value=0.9999,
718
- step=0.0001,
719
- interactive=True,
720
- )
721
- self.inputs.ransac_max_iter = gr.Slider(
722
- label="Ransac Max Iter",
723
- minimum=1,
724
- maximum=100,
725
- value=100,
726
- step=1,
727
- interactive=True,
728
- )
729
- with gr.Accordion("Scene Graph Settings", open=True):
730
- # mapping setting
731
- self.inputs.scene_graph = gr.Dropdown(
732
- choices=["all", "swin", "oneref"],
733
- value="all",
734
- label="Scene Graph",
735
- interactive=True,
736
- )
737
-
738
- # global feature setting
739
- self.inputs.global_feature = gr.Dropdown(
740
- choices=self.init_retrieval_dropdown(),
741
- value="netvlad",
742
- label="Global features",
743
- interactive=True,
744
- )
745
- self.inputs.top_k = gr.Slider(
746
- label="Number of Images per Image to Match",
747
- minimum=1,
748
- maximum=100,
749
- value=10,
750
- step=1,
751
- )
752
- # button_match = gr.Button("Run Matching", variant="primary")
753
-
754
- # mapping setting
755
- with gr.Column():
756
- with gr.Accordion("Mapping Settings", open=True):
757
- with gr.Row():
758
- with gr.Accordion("Buddle Settings", open=True):
759
- with gr.Row():
760
- self.inputs.mapper_refine_focal_length = gr.Checkbox(
761
- label="Refine Focal Length",
762
- value=False,
763
- interactive=True,
764
- )
765
- self.inputs.mapper_refine_principle_points = (
766
- gr.Checkbox(
767
- label="Refine Principle Points",
768
- value=False,
769
- interactive=True,
770
- )
771
- )
772
- self.inputs.mapper_refine_extra_params = gr.Checkbox(
773
- label="Refine Extra Params",
774
- value=False,
775
- interactive=True,
776
- )
777
- with gr.Accordion("Retriangluation Settings", open=True):
778
- gr.Textbox(
779
- label="Retriangluation Details",
780
- )
781
- self.ui.button_sfm = gr.Button("Run SFM", variant="primary")
782
- self.outputs.model_3d = gr.Model3D(
783
- interactive=True,
784
- )
785
- self.outputs.output_image = gr.Image(
786
- label="SFM Visualize",
787
- type="numpy",
788
- image_mode="RGB",
789
- interactive=False,
790
- )
791
-
792
- def call_empty(self):
793
- self.ui.button_sfm.click(fn=self.info, inputs=[], outputs=[])
794
-
795
- def call(self):
796
- self.ui.button_sfm.click(
797
- fn=self.sfm_engine.call,
798
- inputs=[
799
- self.inputs.matcher_key,
800
- self.inputs.input_images, # images
801
- self.inputs.camera_model,
802
- self.inputs.camera_params,
803
- self.inputs.max_keypoints,
804
- self.inputs.keypoint_threshold,
805
- self.inputs.match_threshold,
806
- self.inputs.ransac_threshold,
807
- self.inputs.ransac_confidence,
808
- self.inputs.ransac_max_iter,
809
- self.inputs.scene_graph,
810
- self.inputs.global_feature,
811
- self.inputs.top_k,
812
- self.inputs.mapper_refine_focal_length,
813
- self.inputs.mapper_refine_principle_points,
814
- self.inputs.mapper_refine_extra_params,
815
- ],
816
- outputs=[self.outputs.model_3d, self.outputs.output_image],
817
- )
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from typing import Any, Dict, Optional, Tuple
3
+ import spaces
4
+ import gradio as gr
5
+ import numpy as np
6
+ from easydict import EasyDict as edict
7
+ from omegaconf import OmegaConf
8
+
9
+ from .sfm import SfmEngine
10
+ from .utils import (
11
+ GRADIO_VERSION,
12
+ gen_examples,
13
+ generate_warp_images,
14
+ get_matcher_zoo,
15
+ load_config,
16
+ ransac_zoo,
17
+ run_matching,
18
+ run_ransac,
19
+ send_to_match,
20
+ )
21
+
22
+ DESCRIPTION = """
23
+ <div style="text-align: left;">
24
+ <h1 style="font-size: 2rem; font-weight: bold; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; margin-bottom: 1rem;">
25
+ Image Matching WebUI
26
+ </h1>
27
+ </div>
28
+
29
+ This Space demonstrates [Image Matching WebUI](https://github.com/Vincentqyw/image-matching-webui) by vincent qin. Feel free to play with it, or duplicate to run image matching without a queue!
30
+ <br/>
31
+ πŸš€ **Now GPU-accelerated!** Thanks to HuggingFace's community grant, all algorithms run on GPU for fast, responsive inference.
32
+
33
+ πŸ”Ž For more details about supported local features and matchers, please refer to https://github.com/Vincentqyw/image-matching-webui
34
+
35
+ πŸ› Your feedback is valuable to me. Please do not hesitate to report any bugs [here](https://github.com/Vincentqyw/image-matching-webui/issues).
36
+ """
37
+
38
+ CSS = """
39
+ #warning {background-color: #FFCCCB}
40
+ .logs_class textarea {font-size: 10px !important}
41
+
42
+ .gradio-container {
43
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
44
+ }
45
+
46
+ """
47
+
48
+
49
+ class ImageMatchingApp:
50
+ def __init__(self, server_name="0.0.0.0", server_port=7860, **kwargs):
51
+ self.server_name = server_name
52
+ self.server_port = server_port
53
+ self.config_path = kwargs.get("config", Path(__file__).parent / "config.yaml")
54
+ self.cfg = load_config(self.config_path)
55
+ self.matcher_zoo = get_matcher_zoo(self.cfg["matcher_zoo"])
56
+ self.app = None
57
+ self.example_data_root = kwargs.get(
58
+ "example_data_root", Path(__file__).parents[1] / "datasets"
59
+ )
60
+ # final step
61
+ self.init_interface()
62
+
63
+ def init_matcher_dropdown(self):
64
+ algos = []
65
+ for k, v in self.cfg["matcher_zoo"].items():
66
+ if v.get("enable", True):
67
+ algos.append(k)
68
+ return algos
69
+
70
+ def init_interface(self):
71
+ with gr.Blocks(css=CSS) as self.app:
72
+ with gr.Tab("Image Matching"):
73
+ with gr.Row():
74
+ with gr.Column(scale=1):
75
+ gr.Image(
76
+ str(Path(__file__).parent.parent / "assets/logo.webp"),
77
+ elem_id="logo-img",
78
+ show_label=False,
79
+ show_share_button=False,
80
+ show_download_button=False,
81
+ )
82
+ with gr.Column(scale=3):
83
+ gr.Markdown(DESCRIPTION)
84
+ with gr.Row(equal_height=False):
85
+ with gr.Column():
86
+ with gr.Row():
87
+ matcher_list = gr.Dropdown(
88
+ choices=self.init_matcher_dropdown(),
89
+ value="disk+lightglue",
90
+ label="Matching Model",
91
+ interactive=True,
92
+ )
93
+ match_image_src = gr.Radio(
94
+ (
95
+ ["upload", "webcam", "clipboard"]
96
+ if GRADIO_VERSION > "3"
97
+ else ["upload", "webcam", "canvas"]
98
+ ),
99
+ label="Image Source",
100
+ value="upload",
101
+ )
102
+ with gr.Row():
103
+ input_image0 = gr.Image(
104
+ label="Image 0",
105
+ type="numpy",
106
+ image_mode="RGB",
107
+ height=300 if GRADIO_VERSION > "3" else None,
108
+ interactive=True,
109
+ )
110
+ input_image1 = gr.Image(
111
+ label="Image 1",
112
+ type="numpy",
113
+ image_mode="RGB",
114
+ height=300 if GRADIO_VERSION > "3" else None,
115
+ interactive=True,
116
+ )
117
+
118
+ with gr.Row():
119
+ button_reset = gr.Button(value="Reset")
120
+ button_run = gr.Button(value="Run Match", variant="primary")
121
+ with gr.Row():
122
+ button_stop = gr.Button(value="Force Stop", variant="stop")
123
+
124
+ with gr.Accordion("Advanced Setting", open=False):
125
+ with gr.Tab("Matching Setting"):
126
+ with gr.Row():
127
+ match_setting_threshold = gr.Slider(
128
+ minimum=0.0,
129
+ maximum=1,
130
+ step=0.001,
131
+ label="Match threshold",
132
+ value=0.1,
133
+ )
134
+ match_setting_max_keypoints = gr.Slider(
135
+ minimum=10,
136
+ maximum=10000,
137
+ step=10,
138
+ label="Max features",
139
+ value=1000,
140
+ )
141
+ # TODO: add line settings
142
+ with gr.Row():
143
+ detect_keypoints_threshold = gr.Slider(
144
+ minimum=0,
145
+ maximum=1,
146
+ step=0.001,
147
+ label="Keypoint threshold",
148
+ value=0.015,
149
+ )
150
+ detect_line_threshold = gr.Slider( # noqa: F841
151
+ minimum=0.1,
152
+ maximum=1,
153
+ step=0.01,
154
+ label="Line threshold",
155
+ value=0.2,
156
+ )
157
+
158
+ with gr.Tab("RANSAC Setting"):
159
+ with gr.Row(equal_height=False):
160
+ ransac_method = gr.Dropdown(
161
+ choices=ransac_zoo.keys(),
162
+ value=self.cfg["defaults"]["ransac_method"],
163
+ label="RANSAC Method",
164
+ interactive=True,
165
+ )
166
+ ransac_reproj_threshold = gr.Slider(
167
+ minimum=0.0,
168
+ maximum=12,
169
+ step=0.01,
170
+ label="Ransac Reproj threshold",
171
+ value=8.0,
172
+ )
173
+ ransac_confidence = gr.Slider(
174
+ minimum=0.0,
175
+ maximum=1,
176
+ step=0.00001,
177
+ label="Ransac Confidence",
178
+ value=self.cfg["defaults"]["ransac_confidence"],
179
+ )
180
+ ransac_max_iter = gr.Slider(
181
+ minimum=0.0,
182
+ maximum=100000,
183
+ step=100,
184
+ label="Ransac Iterations",
185
+ value=self.cfg["defaults"]["ransac_max_iter"],
186
+ )
187
+ button_ransac = gr.Button(
188
+ value="Rerun RANSAC", variant="primary"
189
+ )
190
+ with gr.Tab("Geometry Setting"):
191
+ with gr.Row(equal_height=False):
192
+ choice_geometry_type = gr.Radio(
193
+ ["Fundamental", "Homography"],
194
+ label="Reconstruct Geometry",
195
+ value=self.cfg["defaults"]["setting_geometry"],
196
+ )
197
+ with gr.Tab("Image Setting"):
198
+ with gr.Row():
199
+ image_force_resize_cb = gr.Checkbox(
200
+ label="Force Resize",
201
+ value=False,
202
+ interactive=True,
203
+ )
204
+ image_setting_height = gr.Slider(
205
+ minimum=48,
206
+ maximum=2048,
207
+ step=16,
208
+ label="Image Height",
209
+ value=480,
210
+ visible=False,
211
+ )
212
+ image_setting_width = gr.Slider(
213
+ minimum=64,
214
+ maximum=2048,
215
+ step=16,
216
+ label="Image Width",
217
+ value=640,
218
+ visible=False,
219
+ )
220
+ # image resize
221
+ image_force_resize_cb.select(
222
+ fn=self._on_select_force_resize,
223
+ inputs=image_force_resize_cb,
224
+ outputs=[image_setting_width, image_setting_height],
225
+ )
226
+ # collect inputs
227
+ state_cache = gr.State({})
228
+ inputs = [
229
+ input_image0,
230
+ input_image1,
231
+ match_setting_threshold,
232
+ match_setting_max_keypoints,
233
+ detect_keypoints_threshold,
234
+ matcher_list,
235
+ ransac_method,
236
+ ransac_reproj_threshold,
237
+ ransac_confidence,
238
+ ransac_max_iter,
239
+ choice_geometry_type,
240
+ gr.State(self.matcher_zoo),
241
+ image_force_resize_cb,
242
+ image_setting_width,
243
+ image_setting_height,
244
+ ]
245
+
246
+ # Add some examples
247
+ with gr.Row():
248
+ # Example inputs
249
+ with gr.Accordion("Open for More: Examples", open=True):
250
+ gr.Examples(
251
+ examples=gen_examples(self.example_data_root),
252
+ inputs=inputs,
253
+ outputs=[],
254
+ fn=run_matching,
255
+ cache_examples=False,
256
+ label=(
257
+ "Examples (click one of the images below to Run"
258
+ " Match). Thx: WxBS"
259
+ ),
260
+ )
261
+ with gr.Accordion("Supported Algorithms", open=False):
262
+ # add a table of supported algorithms
263
+ self.display_supported_algorithms()
264
+
265
+ with gr.Column():
266
+ with gr.Accordion("Open for More: Keypoints", open=True):
267
+ output_keypoints = gr.Image(label="Keypoints", type="numpy")
268
+ with gr.Accordion(
269
+ (
270
+ "Open for More: Raw Matches"
271
+ " (Green for good matches, Red for bad)"
272
+ ),
273
+ open=False,
274
+ ):
275
+ output_matches_raw = gr.Image(
276
+ label="Raw Matches",
277
+ type="numpy",
278
+ )
279
+ with gr.Accordion(
280
+ (
281
+ "Open for More: Ransac Matches"
282
+ " (Green for good matches, Red for bad)"
283
+ ),
284
+ open=True,
285
+ ):
286
+ output_matches_ransac = gr.Image(
287
+ label="Ransac Matches", type="numpy"
288
+ )
289
+ with gr.Accordion(
290
+ "Open for More: Matches Statistics", open=False
291
+ ):
292
+ output_pred = gr.File(label="Outputs", elem_id="download")
293
+ matches_result_info = gr.JSON(label="Matches Statistics")
294
+ matcher_info = gr.JSON(label="Match info")
295
+
296
+ with gr.Accordion("Open for More: Warped Image", open=True):
297
+ output_wrapped = gr.Image(
298
+ label="Wrapped Pair", type="numpy"
299
+ )
300
+ # send to input
301
+ button_rerun = gr.Button(
302
+ value="Send to Input Match Pair",
303
+ variant="primary",
304
+ )
305
+ with gr.Accordion(
306
+ "Open for More: Geometry info", open=False
307
+ ):
308
+ geometry_result = gr.JSON(
309
+ label="Reconstructed Geometry"
310
+ )
311
+
312
+ # callbacks
313
+ match_image_src.change(
314
+ fn=self.ui_change_imagebox,
315
+ inputs=match_image_src,
316
+ outputs=input_image0,
317
+ )
318
+ match_image_src.change(
319
+ fn=self.ui_change_imagebox,
320
+ inputs=match_image_src,
321
+ outputs=input_image1,
322
+ )
323
+ # collect outputs
324
+ outputs = [
325
+ output_keypoints,
326
+ output_matches_raw,
327
+ output_matches_ransac,
328
+ matches_result_info,
329
+ matcher_info,
330
+ geometry_result,
331
+ output_wrapped,
332
+ state_cache,
333
+ output_pred,
334
+ ]
335
+ # button callbacks
336
+ click_event = button_run.click(
337
+ fn=run_matching, inputs=inputs, outputs=outputs
338
+ )
339
+ # stop button
340
+ button_stop.click(
341
+ fn=None, inputs=None, outputs=None, cancels=[click_event]
342
+ )
343
+
344
+ # Reset images
345
+ reset_outputs = [
346
+ input_image0,
347
+ input_image1,
348
+ match_setting_threshold,
349
+ match_setting_max_keypoints,
350
+ detect_keypoints_threshold,
351
+ matcher_list,
352
+ input_image0,
353
+ input_image1,
354
+ match_image_src,
355
+ output_keypoints,
356
+ output_matches_raw,
357
+ output_matches_ransac,
358
+ matches_result_info,
359
+ matcher_info,
360
+ output_wrapped,
361
+ geometry_result,
362
+ ransac_method,
363
+ ransac_reproj_threshold,
364
+ ransac_confidence,
365
+ ransac_max_iter,
366
+ choice_geometry_type,
367
+ output_pred,
368
+ image_force_resize_cb,
369
+ ]
370
+ button_reset.click(
371
+ fn=self.ui_reset_state,
372
+ inputs=None,
373
+ outputs=reset_outputs,
374
+ )
375
+
376
+ # run ransac button action
377
+ button_ransac.click(
378
+ fn=run_ransac,
379
+ inputs=[
380
+ state_cache,
381
+ choice_geometry_type,
382
+ ransac_method,
383
+ ransac_reproj_threshold,
384
+ ransac_confidence,
385
+ ransac_max_iter,
386
+ ],
387
+ outputs=[
388
+ output_matches_ransac,
389
+ matches_result_info,
390
+ output_wrapped,
391
+ output_pred,
392
+ ],
393
+ )
394
+
395
+ # send warped image to match
396
+ button_rerun.click(
397
+ fn=send_to_match,
398
+ inputs=[state_cache],
399
+ outputs=[input_image0, input_image1],
400
+ )
401
+
402
+ # estimate geo
403
+ choice_geometry_type.change(
404
+ fn=generate_warp_images,
405
+ inputs=[
406
+ input_image0,
407
+ input_image1,
408
+ geometry_result,
409
+ choice_geometry_type,
410
+ ],
411
+ outputs=[output_wrapped, geometry_result],
412
+ )
413
+ with gr.Tab("Structure from Motion(under-dev)"):
414
+ sfm_ui = AppSfmUI( # noqa: F841
415
+ {
416
+ **self.cfg,
417
+ "matcher_zoo": self.matcher_zoo,
418
+ "outputs": "experiments/sfm",
419
+ }
420
+ )
421
+ sfm_ui.call_empty()
422
+
423
+ def run(self):
424
+ self.app.queue().launch(
425
+ server_name=self.server_name,
426
+ server_port=self.server_port,
427
+ share=False,
428
+ allowed_paths=[
429
+ str(Path(__file__).parents[0]),
430
+ str(Path(__file__).parents[1]),
431
+ ],
432
+ )
433
+
434
+ def ui_change_imagebox(self, choice):
435
+ """
436
+ Updates the image box with the given choice.
437
+
438
+ Args:
439
+ choice (list): The list of image sources to be displayed in the image box.
440
+
441
+ Returns:
442
+ dict: A dictionary containing the updated value, sources, and type for the image box.
443
+ """
444
+ ret_dict = {
445
+ "value": None, # The updated value of the image box
446
+ "__type__": "update", # The type of update for the image box
447
+ }
448
+ if GRADIO_VERSION > "3":
449
+ return {
450
+ **ret_dict,
451
+ "sources": choice, # The list of image sources to be displayed
452
+ }
453
+ else:
454
+ return {
455
+ **ret_dict,
456
+ "source": choice, # The list of image sources to be displayed
457
+ }
458
+
459
+ def _on_select_force_resize(self, visible: bool = False):
460
+ return gr.update(visible=visible), gr.update(visible=visible)
461
+
462
+ def ui_reset_state(
463
+ self,
464
+ *args: Any,
465
+ ) -> Tuple[
466
+ Optional[np.ndarray],
467
+ Optional[np.ndarray],
468
+ float,
469
+ int,
470
+ float,
471
+ str,
472
+ Dict[str, Any],
473
+ Dict[str, Any],
474
+ str,
475
+ Optional[np.ndarray],
476
+ Optional[np.ndarray],
477
+ Optional[np.ndarray],
478
+ Dict[str, Any],
479
+ Dict[str, Any],
480
+ Optional[np.ndarray],
481
+ Dict[str, Any],
482
+ str,
483
+ int,
484
+ float,
485
+ int,
486
+ bool,
487
+ ]:
488
+ """
489
+ Reset the state of the UI.
490
+
491
+ Returns:
492
+ tuple: A tuple containing the initial values for the UI state.
493
+ """
494
+ key: str = list(self.matcher_zoo.keys())[
495
+ 0
496
+ ] # Get the first key from matcher_zoo
497
+ # flush_logs()
498
+ return (
499
+ None, # image0: Optional[np.ndarray]
500
+ None, # image1: Optional[np.ndarray]
501
+ self.cfg["defaults"]["match_threshold"], # matching_threshold: float
502
+ self.cfg["defaults"]["max_keypoints"], # max_keypoints: int
503
+ self.cfg["defaults"]["keypoint_threshold"], # keypoint_threshold: float
504
+ key, # matcher: str
505
+ self.ui_change_imagebox("upload"), # input image0: Dict[str, Any]
506
+ self.ui_change_imagebox("upload"), # input image1: Dict[str, Any]
507
+ "upload", # match_image_src: str
508
+ None, # keypoints: Optional[np.ndarray]
509
+ None, # raw matches: Optional[np.ndarray]
510
+ None, # ransac matches: Optional[np.ndarray]
511
+ {}, # matches result info: Dict[str, Any]
512
+ {}, # matcher config: Dict[str, Any]
513
+ None, # warped image: Optional[np.ndarray]
514
+ {}, # geometry result: Dict[str, Any]
515
+ self.cfg["defaults"]["ransac_method"], # ransac_method: str
516
+ self.cfg["defaults"][
517
+ "ransac_reproj_threshold"
518
+ ], # ransac_reproj_threshold: float
519
+ self.cfg["defaults"]["ransac_confidence"], # ransac_confidence: float
520
+ self.cfg["defaults"]["ransac_max_iter"], # ransac_max_iter: int
521
+ self.cfg["defaults"]["setting_geometry"], # geometry: str
522
+ None, # predictions
523
+ False,
524
+ )
525
+
526
+ def display_supported_algorithms(self, style="tab"):
527
+ def get_link(link, tag="Link"):
528
+ return "[{}]({})".format(tag, link) if link is not None else "None"
529
+
530
+ data = []
531
+ cfg = self.cfg["matcher_zoo"]
532
+ if style == "md":
533
+ markdown_table = "| Algo. | Conference | Code | Project | Paper |\n"
534
+ markdown_table += "| ----- | ---------- | ---- | ------- | ----- |\n"
535
+
536
+ for _, v in cfg.items():
537
+ if not v["info"].get("display", True):
538
+ continue
539
+ github_link = get_link(v["info"].get("github", ""))
540
+ project_link = get_link(v["info"].get("project", ""))
541
+ paper_link = get_link(
542
+ v["info"]["paper"],
543
+ (
544
+ Path(v["info"]["paper"]).name[-10:]
545
+ if v["info"]["paper"] is not None
546
+ else "Link"
547
+ ),
548
+ )
549
+
550
+ markdown_table += "{}|{}|{}|{}|{}\n".format(
551
+ v["info"].get("name", ""),
552
+ v["info"].get("source", ""),
553
+ github_link,
554
+ project_link,
555
+ paper_link,
556
+ )
557
+ return gr.Markdown(markdown_table)
558
+ elif style == "tab":
559
+ for k, v in cfg.items():
560
+ if not v["info"].get("display", True):
561
+ continue
562
+ data.append(
563
+ [
564
+ v["info"].get("name", ""),
565
+ v["info"].get("source", ""),
566
+ v["info"].get("github", ""),
567
+ v["info"].get("paper", ""),
568
+ v["info"].get("project", ""),
569
+ ]
570
+ )
571
+ tab = gr.Dataframe(
572
+ headers=["Algo.", "Conference", "Code", "Paper", "Project"],
573
+ datatype=["str", "str", "str", "str", "str"],
574
+ col_count=(5, "fixed"),
575
+ value=data,
576
+ # wrap=True,
577
+ # min_width = 1000,
578
+ # height=1000,
579
+ )
580
+ return tab
581
+
582
+
583
+ class AppBaseUI:
584
+ def __init__(self, cfg: Dict[str, Any] = {}):
585
+ self.cfg = OmegaConf.create(cfg)
586
+ self.inputs = edict({})
587
+ self.outputs = edict({})
588
+ self.ui = edict({})
589
+
590
+ def _init_ui(self):
591
+ NotImplemented
592
+
593
+ def call(self, **kwargs):
594
+ NotImplemented
595
+
596
+ def info(self):
597
+ gr.Info("SFM is under construction.")
598
+
599
+
600
+ class AppSfmUI(AppBaseUI):
601
+ def __init__(self, cfg: Dict[str, Any] = None):
602
+ super().__init__(cfg)
603
+ assert "matcher_zoo" in self.cfg
604
+ self.matcher_zoo = self.cfg["matcher_zoo"]
605
+ self.sfm_engine = SfmEngine(cfg)
606
+ self._init_ui()
607
+
608
+ def init_retrieval_dropdown(self):
609
+ algos = []
610
+ for k, v in self.cfg["retrieval_zoo"].items():
611
+ if v.get("enable", True):
612
+ algos.append(k)
613
+ return algos
614
+
615
+ def _update_options(self, option):
616
+ if option == "sparse":
617
+ return gr.Textbox("sparse", visible=True)
618
+ elif option == "dense":
619
+ return gr.Textbox("dense", visible=True)
620
+ else:
621
+ return gr.Textbox("not set", visible=True)
622
+
623
+ def _on_select_custom_params(self, value: bool = False):
624
+ return gr.update(visible=value)
625
+
626
+ def _init_ui(self):
627
+ with gr.Row():
628
+ # data settting and camera settings
629
+ with gr.Column():
630
+ self.inputs.input_images = gr.File(
631
+ label="SfM",
632
+ interactive=True,
633
+ file_count="multiple",
634
+ min_width=300,
635
+ )
636
+ # camera setting
637
+ with gr.Accordion("Camera Settings", open=True):
638
+ with gr.Column():
639
+ with gr.Row():
640
+ with gr.Column():
641
+ self.inputs.camera_model = gr.Dropdown(
642
+ choices=[
643
+ "PINHOLE",
644
+ "SIMPLE_RADIAL",
645
+ "OPENCV",
646
+ ],
647
+ value="PINHOLE",
648
+ label="Camera Model",
649
+ interactive=True,
650
+ )
651
+ with gr.Column():
652
+ gr.Checkbox(
653
+ label="Shared Params",
654
+ value=True,
655
+ interactive=True,
656
+ )
657
+ camera_custom_params_cb = gr.Checkbox(
658
+ label="Custom Params",
659
+ value=False,
660
+ interactive=True,
661
+ )
662
+ with gr.Row():
663
+ self.inputs.camera_params = gr.Textbox(
664
+ label="Camera Params",
665
+ value="0,0,0,0",
666
+ interactive=False,
667
+ visible=False,
668
+ )
669
+ camera_custom_params_cb.select(
670
+ fn=self._on_select_custom_params,
671
+ inputs=camera_custom_params_cb,
672
+ outputs=self.inputs.camera_params,
673
+ )
674
+
675
+ with gr.Accordion("Matching Settings", open=True):
676
+ # feature extraction and matching setting
677
+ with gr.Row():
678
+ # matcher setting
679
+ self.inputs.matcher_key = gr.Dropdown(
680
+ choices=self.matcher_zoo.keys(),
681
+ value="disk+lightglue",
682
+ label="Matching Model",
683
+ interactive=True,
684
+ )
685
+ with gr.Row():
686
+ with gr.Accordion("Advanced Settings", open=False):
687
+ with gr.Column():
688
+ with gr.Row():
689
+ # matching setting
690
+ self.inputs.max_keypoints = gr.Slider(
691
+ label="Max Keypoints",
692
+ minimum=100,
693
+ maximum=10000,
694
+ value=1000,
695
+ interactive=True,
696
+ )
697
+ self.inputs.keypoint_threshold = gr.Slider(
698
+ label="Keypoint Threshold",
699
+ minimum=0,
700
+ maximum=1,
701
+ value=0.01,
702
+ )
703
+ with gr.Row():
704
+ self.inputs.match_threshold = gr.Slider(
705
+ label="Match Threshold",
706
+ minimum=0.01,
707
+ maximum=12.0,
708
+ value=0.2,
709
+ )
710
+ self.inputs.ransac_threshold = gr.Slider(
711
+ label="Ransac Threshold",
712
+ minimum=0.01,
713
+ maximum=12.0,
714
+ value=4.0,
715
+ step=0.01,
716
+ interactive=True,
717
+ )
718
+
719
+ with gr.Row():
720
+ self.inputs.ransac_confidence = gr.Slider(
721
+ label="Ransac Confidence",
722
+ minimum=0.01,
723
+ maximum=1.0,
724
+ value=0.9999,
725
+ step=0.0001,
726
+ interactive=True,
727
+ )
728
+ self.inputs.ransac_max_iter = gr.Slider(
729
+ label="Ransac Max Iter",
730
+ minimum=1,
731
+ maximum=100,
732
+ value=100,
733
+ step=1,
734
+ interactive=True,
735
+ )
736
+ with gr.Accordion("Scene Graph Settings", open=True):
737
+ # mapping setting
738
+ self.inputs.scene_graph = gr.Dropdown(
739
+ choices=["all", "swin", "oneref"],
740
+ value="all",
741
+ label="Scene Graph",
742
+ interactive=True,
743
+ )
744
+
745
+ # global feature setting
746
+ self.inputs.global_feature = gr.Dropdown(
747
+ choices=self.init_retrieval_dropdown(),
748
+ value="netvlad",
749
+ label="Global features",
750
+ interactive=True,
751
+ )
752
+ self.inputs.top_k = gr.Slider(
753
+ label="Number of Images per Image to Match",
754
+ minimum=1,
755
+ maximum=100,
756
+ value=10,
757
+ step=1,
758
+ )
759
+ # button_match = gr.Button("Run Matching", variant="primary")
760
+
761
+ # mapping setting
762
+ with gr.Column():
763
+ with gr.Accordion("Mapping Settings", open=True):
764
+ with gr.Row():
765
+ with gr.Accordion("Buddle Settings", open=True):
766
+ with gr.Row():
767
+ self.inputs.mapper_refine_focal_length = gr.Checkbox(
768
+ label="Refine Focal Length",
769
+ value=False,
770
+ interactive=True,
771
+ )
772
+ self.inputs.mapper_refine_principle_points = (
773
+ gr.Checkbox(
774
+ label="Refine Principle Points",
775
+ value=False,
776
+ interactive=True,
777
+ )
778
+ )
779
+ self.inputs.mapper_refine_extra_params = gr.Checkbox(
780
+ label="Refine Extra Params",
781
+ value=False,
782
+ interactive=True,
783
+ )
784
+ with gr.Accordion("Retriangluation Settings", open=True):
785
+ gr.Textbox(
786
+ label="Retriangluation Details",
787
+ )
788
+ self.ui.button_sfm = gr.Button("Run SFM", variant="primary")
789
+ self.outputs.model_3d = gr.Model3D(
790
+ interactive=True,
791
+ )
792
+ self.outputs.output_image = gr.Image(
793
+ label="SFM Visualize",
794
+ type="numpy",
795
+ image_mode="RGB",
796
+ interactive=False,
797
+ )
798
+
799
+ def call_empty(self):
800
+ self.ui.button_sfm.click(fn=self.info, inputs=[], outputs=[])
801
+
802
+ def call(self):
803
+ self.ui.button_sfm.click(
804
+ fn=self.sfm_engine.call,
805
+ inputs=[
806
+ self.inputs.matcher_key,
807
+ self.inputs.input_images, # images
808
+ self.inputs.camera_model,
809
+ self.inputs.camera_params,
810
+ self.inputs.max_keypoints,
811
+ self.inputs.keypoint_threshold,
812
+ self.inputs.match_threshold,
813
+ self.inputs.ransac_threshold,
814
+ self.inputs.ransac_confidence,
815
+ self.inputs.ransac_max_iter,
816
+ self.inputs.scene_graph,
817
+ self.inputs.global_feature,
818
+ self.inputs.top_k,
819
+ self.inputs.mapper_refine_focal_length,
820
+ self.inputs.mapper_refine_principle_points,
821
+ self.inputs.mapper_refine_extra_params,
822
+ ],
823
+ outputs=[self.outputs.model_3d, self.outputs.output_image],
824
+ )