avans06 commited on
Commit
b199d28
·
1 Parent(s): c23cef2

feat: replace File input with interactive Gallery and add Ctrl+V paste support

Browse files
Files changed (2) hide show
  1. app.py +124 -21
  2. webui.bat +36 -11
app.py CHANGED
@@ -2283,6 +2283,76 @@ def stitch_video_frames(video_path,
2283
  return final_results, log
2284
 
2285
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2286
  # --- Gradio Interface Function ---
2287
  def run_stitching_interface(input_files,
2288
  use_transparency,
@@ -2317,15 +2387,14 @@ def run_stitching_interface(input_files,
2317
  ):
2318
  """
2319
  Wrapper function called by the Gradio interface.
2320
- Handles input (images or video), applies pre-cropping,
2321
- calls the appropriate stitching logic (passing transform_model_str),
2322
- and returns results.
2323
  Updates image reading and result saving to handle RGBA/BGRA transparency.
2324
  UPDATED to handle transparency correctly when saving to temp files.
2325
  """
2326
  if input_files is None or len(input_files) == 0:
2327
  return [], "Please upload images or a video file."
2328
-
2329
  # Convert Gradio inputs to correct types
2330
  fixed_base_image_index = int(fixed_base_image_index_input) if fixed_base_image_index_input is not None else -1
2331
  ransac_reproj_thresh = float(ransac_reproj_thresh_input) if ransac_reproj_thresh_input is not None else 3.0
@@ -2333,7 +2402,7 @@ def run_stitching_interface(input_files,
2333
  blend_smooth_ksize = int(blend_smooth_ksize_input) if blend_smooth_ksize_input is not None else -1
2334
  num_blend_levels = int(num_blend_levels_input) if num_blend_levels_input is not None else 4
2335
 
2336
- log = f"Received {len(input_files)} file(s).\n"
2337
  log = log_and_print(f"Pre-Crop Settings: Top={crop_top_percent}%, Bottom={crop_bottom_percent}%\n", log)
2338
  log = log_and_print(f"Post-Crop Black Borders: Enabled={enable_cropping}, Strict Edges={strict_no_black_edges_input}\n", log)
2339
  # Log detailed settings including new ones
@@ -2350,13 +2419,29 @@ def run_stitching_interface(input_files,
2350
  try:
2351
  # Handle potential TempfileWrappers or string paths
2352
  input_filepaths = []
2353
- for f in input_files:
2354
- if hasattr(f, 'name'): # Gradio File object
2355
- input_filepaths.append(f.name)
2356
- elif isinstance(f, str): # String path (e.g., from examples)
2357
- input_filepaths.append(f)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2358
  else:
2359
- log = log_and_print(f"Warning: Unexpected input file type: {type(f)}. Skipping.\n", log)
2360
 
2361
  if len(input_filepaths) == 1:
2362
  filepath = input_filepaths[0]
@@ -2696,8 +2781,8 @@ def run_stitching_interface(input_files,
2696
  # Use imencode -> write pattern for better handling of paths/special chars
2697
  is_success, buf = cv2.imencode('.png', img_bgra)
2698
  if is_success:
2699
- with open(full_path, 'wb') as f:
2700
- f.write(buf)
2701
  # Use Gradio File obj or just path string? Gallery seems to prefer path strings.
2702
  output_file_paths.append((full_path, filename)) # Store the full path for Gradio Gallery
2703
  # final_log = log_and_print(f"Successfully saved: {filename}\n", final_log) # Can be verbose
@@ -2732,7 +2817,7 @@ def run_stitching_interface(input_files,
2732
  return output_file_paths, final_log
2733
 
2734
  # --- Define Gradio Interface ---
2735
- with gr.Blocks() as demo:
2736
  gr.Markdown("# OpenCV Image and Video Stitcher")
2737
  gr.Markdown(
2738
  "Upload multiple images (for list/panorama stitching) OR a single video file (for sequential frame stitching). "
@@ -2743,12 +2828,23 @@ with gr.Blocks() as demo:
2743
  with gr.Row():
2744
  with gr.Column(scale=1):
2745
  stitch_button = gr.Button("Stitch", variant="primary")
2746
- input_files = gr.File(
2747
- label="Upload Images or a Video",
2748
- # Common image and video types
2749
- file_types=["image", ".mp4", ".avi", ".mov", ".mkv", ".wmv", ".webm"],
2750
- file_count="multiple",
2751
- elem_id="input_files"
 
 
 
 
 
 
 
 
 
 
 
2752
  )
2753
 
2754
  # --- Parameters Grouping ---
@@ -2916,7 +3012,11 @@ with gr.Blocks() as demo:
2916
  5000, 10000, 10000,
2917
  ]
2918
  ]
2919
- gr.Examples(examples, inputs=inputs, label="Example Configurations")
 
 
 
 
2920
 
2921
  # Connect button click to the function
2922
  stitch_button.click(
@@ -2924,6 +3024,9 @@ with gr.Blocks() as demo:
2924
  inputs=inputs,
2925
  outputs=[output_gallery, output_log]
2926
  )
 
 
 
2927
 
2928
  # --- Main Execution Block ---
2929
  if __name__ == "__main__":
 
2283
  return final_results, log
2284
 
2285
 
2286
+ # Custom CSS for Gallery
2287
+ css = """
2288
+ /* Add hover effect to Gallery to indicate it is an interactive area */
2289
+ #input_gallery:hover {
2290
+ border-color: var(--color-accent) !important;
2291
+ box-shadow: 0 0 10px rgba(0,0,0,0.1);
2292
+ }
2293
+ """
2294
+
2295
+ # JavaScript to handle Ctrl+V paste for MULTIPLE files (Images OR Videos) when hovering over the gallery
2296
+ paste_js = """
2297
+ function initPaste() {
2298
+ document.addEventListener('paste', function(e) {
2299
+ // 1. First find the Gallery component
2300
+ const gallery = document.getElementById('input_gallery');
2301
+ if (!gallery) return;
2302
+
2303
+ // 2. Check if mouse is hovering over the Gallery
2304
+ // If mouse is not over the gallery, ignore this paste event
2305
+ if (!gallery.matches(':hover')) {
2306
+ return;
2307
+ }
2308
+
2309
+ const clipboardData = e.clipboardData || e.originalEvent.clipboardData;
2310
+ if (!clipboardData) return;
2311
+
2312
+ const items = clipboardData.items;
2313
+ const files = [];
2314
+
2315
+ // 3. Check clipboard content for images OR videos
2316
+ for (let i = 0; i < items.length; i++) {
2317
+ if (items[i].kind === 'file') {
2318
+ if (items[i].type.startsWith('image/') || items[i].type.startsWith('video/')) {
2319
+ files.push(items[i].getAsFile());
2320
+ }
2321
+ }
2322
+ }
2323
+
2324
+ // 4. Check file list (Copied files from OS)
2325
+ if (files.length === 0 && clipboardData.files.length > 0) {
2326
+ for (let i = 0; i < clipboardData.files.length; i++) {
2327
+ if (clipboardData.files[i].type.startsWith('image/') || clipboardData.files[i].type.startsWith('video/')) {
2328
+ files.push(clipboardData.files[i]);
2329
+ }
2330
+ }
2331
+ }
2332
+
2333
+ if (files.length === 0) return;
2334
+
2335
+ // 5. Execute upload logic
2336
+ // Find input inside the gallery component
2337
+ const uploadInput = gallery.querySelector('input[type="file"]');
2338
+
2339
+ if (uploadInput) {
2340
+ e.preventDefault();
2341
+ e.stopPropagation();
2342
+
2343
+ const dataTransfer = new DataTransfer();
2344
+ files.forEach(file => dataTransfer.items.add(file));
2345
+
2346
+ uploadInput.files = dataTransfer.files;
2347
+
2348
+ // Trigger Gradio update
2349
+ uploadInput.dispatchEvent(new Event('change', { bubbles: true }));
2350
+ }
2351
+ });
2352
+ }
2353
+ """
2354
+
2355
+
2356
  # --- Gradio Interface Function ---
2357
  def run_stitching_interface(input_files,
2358
  use_transparency,
 
2387
  ):
2388
  """
2389
  Wrapper function called by the Gradio interface.
2390
+ Handles input (images or video) from Gallery (tuples of path/caption), applies pre-cropping,
2391
+ calls the appropriate stitching logic (passing transform_model_str), and returns results.
 
2392
  Updates image reading and result saving to handle RGBA/BGRA transparency.
2393
  UPDATED to handle transparency correctly when saving to temp files.
2394
  """
2395
  if input_files is None or len(input_files) == 0:
2396
  return [], "Please upload images or a video file."
2397
+
2398
  # Convert Gradio inputs to correct types
2399
  fixed_base_image_index = int(fixed_base_image_index_input) if fixed_base_image_index_input is not None else -1
2400
  ransac_reproj_thresh = float(ransac_reproj_thresh_input) if ransac_reproj_thresh_input is not None else 3.0
 
2402
  blend_smooth_ksize = int(blend_smooth_ksize_input) if blend_smooth_ksize_input is not None else -1
2403
  num_blend_levels = int(num_blend_levels_input) if num_blend_levels_input is not None else 4
2404
 
2405
+ log = f"Received {len(input_files)} item(s) from gallery.\n"
2406
  log = log_and_print(f"Pre-Crop Settings: Top={crop_top_percent}%, Bottom={crop_bottom_percent}%\n", log)
2407
  log = log_and_print(f"Post-Crop Black Borders: Enabled={enable_cropping}, Strict Edges={strict_no_black_edges_input}\n", log)
2408
  # Log detailed settings including new ones
 
2419
  try:
2420
  # Handle potential TempfileWrappers or string paths
2421
  input_filepaths = []
2422
+ for item in input_files:
2423
+ # Gallery input typically comes as a list of tuples: (filepath, caption)
2424
+ # We need to extract the filepath.
2425
+ extracted_path = None
2426
+
2427
+ if isinstance(item, (tuple, list)):
2428
+ # Item is (path, caption)
2429
+ potential_file = item[0]
2430
+ if isinstance(potential_file, str):
2431
+ extracted_path = potential_file
2432
+ elif hasattr(potential_file, 'name'): # Handle file-like object if present
2433
+ extracted_path = potential_file.name
2434
+ elif isinstance(potential_file, dict) and 'name' in potential_file: # Handle dictionary format
2435
+ extracted_path = potential_file['name']
2436
+ elif hasattr(item, 'name'): # Fallback for direct File objects
2437
+ extracted_path = item.name
2438
+ elif isinstance(item, str): # Fallback for direct strings
2439
+ extracted_path = item
2440
+
2441
+ if extracted_path:
2442
+ input_filepaths.append(extracted_path)
2443
  else:
2444
+ log = log_and_print(f"Warning: Could not extract path from gallery item: {item}. Skipping.\n", log)
2445
 
2446
  if len(input_filepaths) == 1:
2447
  filepath = input_filepaths[0]
 
2781
  # Use imencode -> write pattern for better handling of paths/special chars
2782
  is_success, buf = cv2.imencode('.png', img_bgra)
2783
  if is_success:
2784
+ with open(full_path, 'wb') as item:
2785
+ item.write(buf)
2786
  # Use Gradio File obj or just path string? Gallery seems to prefer path strings.
2787
  output_file_paths.append((full_path, filename)) # Store the full path for Gradio Gallery
2788
  # final_log = log_and_print(f"Successfully saved: {filename}\n", final_log) # Can be verbose
 
2817
  return output_file_paths, final_log
2818
 
2819
  # --- Define Gradio Interface ---
2820
+ with gr.Blocks(css=css) as demo:
2821
  gr.Markdown("# OpenCV Image and Video Stitcher")
2822
  gr.Markdown(
2823
  "Upload multiple images (for list/panorama stitching) OR a single video file (for sequential frame stitching). "
 
2828
  with gr.Row():
2829
  with gr.Column(scale=1):
2830
  stitch_button = gr.Button("Stitch", variant="primary")
2831
+ # File Input with Gallery Input
2832
+ input_files = gr.Gallery(
2833
+ columns=5,
2834
+ rows=3,
2835
+ show_share_button=False,
2836
+ interactive=True,
2837
+ height=300,
2838
+ label="Input Gallery (Drag multiple images/video here)",
2839
+ elem_id="input_gallery", # Crucial for the paste_js to work
2840
+ # Note: file_types is handled by the JS paste logic for clipboard
2841
+ )
2842
+ gr.Markdown(
2843
+ """
2844
+ <div style="text-align: right; font-size: 0.9em; color: gray;">
2845
+ 💡 Tip: Hover over the gallery and press <b>Ctrl+V</b> to paste images or video.
2846
+ </div>
2847
+ """
2848
  )
2849
 
2850
  # --- Parameters Grouping ---
 
3012
  5000, 10000, 10000,
3013
  ]
3014
  ]
3015
+ gr.Examples(
3016
+ examples,
3017
+ inputs=inputs,
3018
+ label="Example Configurations",
3019
+ )
3020
 
3021
  # Connect button click to the function
3022
  stitch_button.click(
 
3024
  inputs=inputs,
3025
  outputs=[output_gallery, output_log]
3026
  )
3027
+
3028
+ # Load the JavaScript for Ctrl+V support
3029
+ demo.load(None, None, None, js=paste_js)
3030
 
3031
  # --- Main Execution Block ---
3032
  if __name__ == "__main__":
webui.bat CHANGED
@@ -1,18 +1,42 @@
1
  @echo off
2
 
 
 
3
  :: The original source of the webui.bat file is stable-diffusion-webui
4
  :: Modified and enhanced by Gemini with features for venv management and requirements handling.
5
 
6
  :: --------- Configuration ---------
7
  set COMMANDLINE_ARGS=
 
 
 
 
 
8
  :: Define the name of the Launch application
9
  set APPLICATION_NAME=app.py
 
 
 
 
10
  :: Define the name of the virtual environment directory
11
  set VENV_NAME=venv
 
12
  :: Set to 1 to always attempt to update packages from requirements.txt on every launch
13
  set ALWAYS_UPDATE_REQS=0
14
  :: ---------------------------------
15
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  :: Set PYTHON executable if not already defined
18
  if not defined PYTHON (set PYTHON=python)
@@ -62,12 +86,12 @@ echo Venv created.
62
  :: Install requirements for the first time if venv was just created
63
  :: This section handles the initial installation of packages from requirements.txt
64
  :: immediately after a new virtual environment is created.
65
- echo Checking for requirements.txt for initial setup in %~dp0
66
- if exist "%~dp0requirements.txt" (
67
- echo Found requirements.txt, attempting to install for initial setup...
68
  call "%VENV_DIR%\Scripts\activate.bat"
69
- echo Installing packages from requirements.txt ^(initial setup^)...
70
- "%VENV_DIR%\Scripts\python.exe" -m pip install -r "%~dp0requirements.txt"
71
  if %ERRORLEVEL% NEQ 0 (
72
  echo Failed to install requirements during initial setup. Please check the output above.
73
  pause
@@ -76,7 +100,7 @@ if exist "%~dp0requirements.txt" (
76
  echo Initial requirements installed successfully.
77
  call "%VENV_DIR%\Scripts\deactivate.bat"
78
  ) else (
79
- echo No requirements.txt found for initial setup, skipping package installation.
80
  )
81
  goto :activate_venv_and_maybe_update
82
 
@@ -93,10 +117,10 @@ echo Activating venv: %PYTHON%
93
  if defined ALWAYS_UPDATE_REQS (
94
  if "%ALWAYS_UPDATE_REQS%"=="1" (
95
  echo ALWAYS_UPDATE_REQS is enabled.
96
- if exist "%~dp0requirements.txt" (
97
- echo Attempting to update packages from requirements.txt...
98
  REM No need to call activate.bat here again, PYTHON is already set to the venv's python
99
- %PYTHON% -m pip install -r "%~dp0requirements.txt"
100
  if %ERRORLEVEL% NEQ 0 (
101
  echo Failed to update requirements. Please check the output above.
102
  pause
@@ -104,7 +128,7 @@ if defined ALWAYS_UPDATE_REQS (
104
  )
105
  echo Requirements updated successfully.
106
  ) else (
107
- echo ALWAYS_UPDATE_REQS is enabled, but no requirements.txt found. Skipping update.
108
  )
109
  ) else (
110
  echo ALWAYS_UPDATE_REQS is not enabled or not set to 1. Skipping routine update.
@@ -121,7 +145,8 @@ goto :launch
121
  :launch
122
  :: Launch the main application
123
  echo Launching Web UI with arguments: %COMMANDLINE_ARGS% %*
124
- %PYTHON% %APPLICATION_NAME% %COMMANDLINE_ARGS% %*
 
125
  echo Launch finished.
126
  pause
127
  exit /b
 
1
  @echo off
2
 
3
+ chcp 65001
4
+ set PYTHONUTF8=1
5
  :: The original source of the webui.bat file is stable-diffusion-webui
6
  :: Modified and enhanced by Gemini with features for venv management and requirements handling.
7
 
8
  :: --------- Configuration ---------
9
  set COMMANDLINE_ARGS=
10
+
11
+ :: Define the application directory (folder name)
12
+ :: Leave empty if the app is in the root directory.
13
+ set APP_DIR=
14
+
15
  :: Define the name of the Launch application
16
  set APPLICATION_NAME=app.py
17
+
18
+ :: Define the requirements filename, default is requirements.txt
19
+ set REQUIREMENTS_FILE=requirements.txt
20
+
21
  :: Define the name of the virtual environment directory
22
  set VENV_NAME=venv
23
+
24
  :: Set to 1 to always attempt to update packages from requirements.txt on every launch
25
  set ALWAYS_UPDATE_REQS=0
26
  :: ---------------------------------
27
 
28
+ :: --------- Path Setup Logic ---------
29
+ :: Logic to handle paths based on whether APP_DIR is set
30
+ if defined APP_DIR (
31
+ set "TARGET_REQ=%~dp0%APP_DIR%\%REQUIREMENTS_FILE%"
32
+ set "TARGET_SCRIPT=%~dp0%APP_DIR%\%APPLICATION_NAME%"
33
+ echo Working in subdirectory: %APP_DIR%
34
+ ) else (
35
+ set "TARGET_REQ=%~dp0%REQUIREMENTS_FILE%"
36
+ set "TARGET_SCRIPT=%~dp0%APPLICATION_NAME%"
37
+ echo Working in root directory.
38
+ )
39
+ :: ------------------------------------
40
 
41
  :: Set PYTHON executable if not already defined
42
  if not defined PYTHON (set PYTHON=python)
 
86
  :: Install requirements for the first time if venv was just created
87
  :: This section handles the initial installation of packages from requirements.txt
88
  :: immediately after a new virtual environment is created.
89
+ echo Checking for %REQUIREMENTS_FILE% for initial setup...
90
+ if exist "%TARGET_REQ%" (
91
+ echo Found %REQUIREMENTS_FILE% at "%TARGET_REQ%", attempting to install for initial setup...
92
  call "%VENV_DIR%\Scripts\activate.bat"
93
+ echo Installing packages from %REQUIREMENTS_FILE% ^(initial setup^)...
94
+ "%VENV_DIR%\Scripts\python.exe" -m pip install -r "%TARGET_REQ%"
95
  if %ERRORLEVEL% NEQ 0 (
96
  echo Failed to install requirements during initial setup. Please check the output above.
97
  pause
 
100
  echo Initial requirements installed successfully.
101
  call "%VENV_DIR%\Scripts\deactivate.bat"
102
  ) else (
103
+ echo No %REQUIREMENTS_FILE% found at "%TARGET_REQ%", skipping package installation.
104
  )
105
  goto :activate_venv_and_maybe_update
106
 
 
117
  if defined ALWAYS_UPDATE_REQS (
118
  if "%ALWAYS_UPDATE_REQS%"=="1" (
119
  echo ALWAYS_UPDATE_REQS is enabled.
120
+ if exist "%TARGET_REQ%" (
121
+ echo Attempting to update packages from "%TARGET_REQ%"...
122
  REM No need to call activate.bat here again, PYTHON is already set to the venv's python
123
+ %PYTHON% -m pip install -r "%TARGET_REQ%"
124
  if %ERRORLEVEL% NEQ 0 (
125
  echo Failed to update requirements. Please check the output above.
126
  pause
 
128
  )
129
  echo Requirements updated successfully.
130
  ) else (
131
+ echo ALWAYS_UPDATE_REQS is enabled, but no %REQUIREMENTS_FILE% found. Skipping update.
132
  )
133
  ) else (
134
  echo ALWAYS_UPDATE_REQS is not enabled or not set to 1. Skipping routine update.
 
145
  :launch
146
  :: Launch the main application
147
  echo Launching Web UI with arguments: %COMMANDLINE_ARGS% %*
148
+ echo Script path: %TARGET_SCRIPT%
149
+ %PYTHON% "%TARGET_SCRIPT%" %COMMANDLINE_ARGS% %*
150
  echo Launch finished.
151
  pause
152
  exit /b