feat: Add Street View Animator node with animation capabilities and update README and version
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.python.org/downloads/release/python-3120/)
|
||||
[](https://github.com/ru4ls/ComfyUI_StreetView-Loader)
|
||||
|
||||
|
||||
A custom node for ComfyUI that allows you to load images directly from Google Street View to use as backgrounds, textures, or inputs in your workflows.
|
||||
A custom node for ComfyUI that allows you to load images directly from Google Street View to use as backgrounds, textures, or inputs in your workflows. Version 1.01 adds animation capabilities!
|
||||
|
||||
Instead of manually taking screenshots, this node programmatically fetches a clean, high-resolution image from any location on Earth with Street View coverage, giving you precise control over the camera angle, direction, and field of view.
|
||||
|
||||
@@ -27,6 +27,7 @@ This gives you the best of both worlds: the authenticity of a real photograph co
|
||||
* **Virtual Location Scouting:** Instantly scout real-world locations from your desktop and experiment with different styles and moods for your project.
|
||||
* **Personalized & Sentimental Art:** Create unique art based on a place with personal meaning—a childhood home, a proposal spot, or a favorite vacation view.
|
||||
* **Consistent Backgrounds for Testing:** Use a consistent, real-world background to reliably test LoRAs, IPAdapters, or character models.
|
||||
* **Animation Capabilities:** Create smooth camera movements and parameter transitions with the new Street View Animator node (v1.01).
|
||||
|
||||

|
||||
|
||||
@@ -37,6 +38,7 @@ This gives you the best of both worlds: the authenticity of a real photograph co
|
||||
- **Simple Aspect Ratio Presets:** Choose from common ratios like 16:9 or 1:1 without manual calculations.
|
||||
- **Clean Output:** No UI overlays, just the pure image.
|
||||
- **(Experimental) Panorama Mode:** Stitch multiple images together to create ultra-wide cinematic landscapes.
|
||||
- **(New in v1.01) Animation Mode:** Animate camera parameters over time to create smooth transitions and camera movements.
|
||||
|
||||
---
|
||||
|
||||
@@ -162,7 +164,54 @@ Once you have your stitched result, you have two great options to create a final
|
||||
- **Stitching Process:** If the OpenCV algorithm cannot find enough matching features, it will automatically **fall back to a simple side-by-side stitch** to ensure you always get an output. If this happens, the best solution is to increase the `overlap_percentage`.
|
||||
- **Resolution:** The output image will be very wide but only 640px tall. It is **highly recommended** to chain the output of this node into an **Upscale Image** node to increase the final resolution for your projects.
|
||||
|
||||
## 5. Troubleshooting
|
||||
## 5. Animation Node (New in v1.01)
|
||||
|
||||
Version 1.01 introduces the **Street View Animator** node, which allows you to create animated sequences by smoothly transitioning camera parameters over time.
|
||||
|
||||
This node enables you to create dynamic camera movements like slow rotations, pitch changes, or field-of-view adjustments that can be used as input for video generation workflows or simply to create smooth transitions between different viewpoints of the same location.
|
||||
|
||||

|
||||
|
||||
<video src="https://github.com/user-attachments/assets/80346b4a-6c77-4f3e-b198-3ae6d846505b" width="100%" height="auto" controls></video>
|
||||
|
||||
### How to Use & Parameter Suggestions
|
||||
|
||||
1. Add the **"Street View Animator"** node to your canvas.
|
||||
2. Provide a `location` (same as other nodes).
|
||||
3. Set your animation parameters:
|
||||
- **Start/End Values:** Define the beginning and ending values for `heading`, `pitch`, and `fov` parameters.
|
||||
- **Duration & FPS:** Control the total animation length and frame rate (frames per second).
|
||||
- **Interpolation:** Choose from different transition types (linear, ease-in, ease-out, ease-in-out) for smooth camera movements.
|
||||
- **aspect_ratio:** Select your desired output aspect ratio.
|
||||
|
||||
4. The node will generate a sequence of images as a stacked tensor output, which can be used for:
|
||||
- Video generation workflows
|
||||
- Frame-by-frame processing
|
||||
- Creating dynamic backgrounds for animations
|
||||
|
||||
### Key Animation Parameters
|
||||
|
||||
- **Start/End Heading:** Control camera rotation from start to end values (0-360 degrees)
|
||||
- **Start/End Pitch:** Adjust camera tilt from start to end values (-90 to 90 degrees)
|
||||
- **Start/End FOV:** Change field of view from start to end values (10-120 degrees)
|
||||
- **Duration:** Total animation time in seconds
|
||||
- **FPS:** Frames per second - determines smoothness and total frame count
|
||||
- **Interpolation:** Defines how values transition between start and end points
|
||||
|
||||
### Animation Tips
|
||||
|
||||
- **Smooth Rotations:** For a full 360° rotation, set start_heading=0 and end_heading=360
|
||||
- **Camera Dolly:** Keep heading constant but change fov for zoom effects
|
||||
- **Tilt Effects:** Combine pitch changes with heading changes for dynamic camera movements
|
||||
- **Frame Count:** Total frames = duration × fps (higher values = smoother but may increase API usage costs)
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **API Usage:** This node makes multiple API calls equal to the total number of frames generated. Each frame is a separate API request.
|
||||
- **Performance:** Animation rendering time increases with duration and fps. Start with low settings and increase as needed.
|
||||
- **Memory:** Large frame sequences can consume significant memory. Consider using in smaller batches if needed.
|
||||
|
||||
## 6. Troubleshooting
|
||||
|
||||
- **`ValueError: API key not found`:** Your `.env` file is missing, in the wrong location, or the variable name is not `GOOGLE_STREET_VIEW_API_KEY`.
|
||||
- **Black Image Output:** This usually means Google has no Street View imagery for that coordinate, or your API key is invalid/restricted. Check your key's restrictions on the Google Cloud Console.
|
||||
|
||||
@@ -3,17 +3,20 @@
|
||||
from .nodes.streetview_loader import StreetViewLoader
|
||||
from .nodes.streetview_url_parser import StreetViewURLParser
|
||||
from .nodes.streetview_pano_loader import StreetViewPanoLoader
|
||||
from .nodes.streetview_animator import StreetViewAnimator
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"StreetViewLoader": StreetViewLoader,
|
||||
"StreetViewURLParser": StreetViewURLParser,
|
||||
"StreetViewPanoLoader": StreetViewPanoLoader,
|
||||
"StreetViewAnimator": StreetViewAnimator,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"StreetViewLoader": "Street View Loader",
|
||||
"StreetViewURLParser": "Street View URL Parser",
|
||||
"StreetViewPanoLoader": "Street View Pano Loader",
|
||||
"StreetViewAnimator": "Street View Animator",
|
||||
}
|
||||
|
||||
print("------------------------------------------")
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 294 KiB |
@@ -0,0 +1,179 @@
|
||||
# file: ComfyUI_StreetView-Loader/nodes/streetview_animator.py
|
||||
|
||||
import torch
|
||||
import numpy as np
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from PIL import Image
|
||||
|
||||
# Import the refactored API call function from our utility file
|
||||
from ..utils.connect_api_utils import fetch_streetview_image
|
||||
|
||||
# --- Load API Key from .env file ---
|
||||
# This logic finds the .env file in the parent directory (ComfyUI_StreetView-Loader)
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parent_dir = os.path.dirname(current_dir)
|
||||
dotenv_path = os.path.join(parent_dir, '.env')
|
||||
|
||||
# Load the environment variables from the found .env file
|
||||
load_dotenv(dotenv_path=dotenv_path)
|
||||
|
||||
# Get the API key from the loaded environment variables
|
||||
API_KEY_FROM_ENV = os.getenv("GOOGLE_STREET_VIEW_API_KEY")
|
||||
|
||||
|
||||
class StreetViewAnimator:
|
||||
"""
|
||||
A ComfyUI node to animate StreetView parameters (heading, pitch, fov) over time,
|
||||
generating a sequence of images based on duration and FPS settings.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
"""
|
||||
Defines the input fields for the animation node.
|
||||
Includes start/end values for heading, pitch, fov, and animation settings.
|
||||
"""
|
||||
return {
|
||||
"required": {
|
||||
"location": ("STRING", {
|
||||
"multiline": False,
|
||||
"default": "40.720032,-73.988354" # Example: Near Katz's Deli, NYC
|
||||
}),
|
||||
# Start values for animation
|
||||
"start_heading": ("FLOAT", {"default": 0.0, "min": 0, "max": 360, "step": 0.1, "display": "slider"}),
|
||||
"end_heading": ("FLOAT", {"default": 360.0, "min": 0, "max": 360, "step": 0.1, "display": "slider"}),
|
||||
"start_pitch": ("FLOAT", {"default": 0.0, "min": -90, "max": 90, "step": 0.1, "display": "slider"}),
|
||||
"end_pitch": ("FLOAT", {"default": 0.0, "min": -90, "max": 90, "step": 0.1, "display": "slider"}),
|
||||
"start_fov": ("INT", {"default": 90, "min": 10, "max": 120, "step": 1, "display": "slider"}),
|
||||
"end_fov": ("INT", {"default": 90, "min": 10, "max": 120, "step": 1, "display": "slider"}),
|
||||
"duration": ("FLOAT", {"default": 5.0, "min": 0.1, "max": 60.0, "step": 0.1, "display": "slider"}),
|
||||
"fps": ("INT", {"default": 24, "min": 1, "max": 60, "step": 1, "display": "slider"}),
|
||||
"aspect_ratio": ([
|
||||
"1:1 Square (640x640)",
|
||||
"16:9 Widescreen (640x360)",
|
||||
"9:16 Vertical (360x640)",
|
||||
"4:3 Classic (640x480)",
|
||||
"3:2 Photography (640x427)"
|
||||
],),
|
||||
"interpolation": ([
|
||||
"linear", # Smooth linear transition
|
||||
"ease_in", # Slow start, accelerate
|
||||
"ease_out", # Fast start, decelerate
|
||||
"ease_in_out" # Slow start, fast middle, slow end
|
||||
], {"default": "linear"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "STRING")
|
||||
RETURN_NAMES = ("images", "metadata")
|
||||
FUNCTION = "animate_streetview"
|
||||
CATEGORY = "Ru4ls/StreetView/Animation"
|
||||
|
||||
def linear_interpolation(self, start_val, end_val, progress):
|
||||
"""Linear interpolation between two values."""
|
||||
return start_val + (end_val - start_val) * progress
|
||||
|
||||
def ease_in_interpolation(self, start_val, end_val, progress):
|
||||
"""Ease-in interpolation (slow start)."""
|
||||
return start_val + (end_val - start_val) * (progress ** 2)
|
||||
|
||||
def ease_out_interpolation(self, start_val, end_val, progress):
|
||||
"""Ease-out interpolation (slow end)."""
|
||||
return start_val + (end_val - start_val) * (1 - (1 - progress) ** 2)
|
||||
|
||||
def ease_in_out_interpolation(self, start_val, end_val, progress):
|
||||
"""Ease-in-out interpolation (slow start and end)."""
|
||||
if progress < 0.5:
|
||||
return start_val + (end_val - start_val) * (2 * progress ** 2)
|
||||
else:
|
||||
return start_val + (end_val - start_val) * (1 - (-2 * progress + 2) ** 2 / 2)
|
||||
|
||||
def animate_streetview(self, location, start_heading, end_heading, start_pitch, end_pitch, start_fov, end_fov, duration, fps, aspect_ratio, interpolation):
|
||||
if not API_KEY_FROM_ENV:
|
||||
raise ValueError("Google Street View API key not found in .env file. Please ensure GOOGLE_STREET_VIEW_API_KEY is set in ComfyUI_StreetView-Loader/.env")
|
||||
|
||||
# Determine width and height based on the selected aspect ratio
|
||||
if aspect_ratio == "1:1 Square (640x640)":
|
||||
width, height = 640, 640
|
||||
elif aspect_ratio == "16:9 Widescreen (640x360)":
|
||||
width, height = 640, 360
|
||||
elif aspect_ratio == "9:16 Vertical (360x640)":
|
||||
width, height = 360, 640
|
||||
elif aspect_ratio == "4:3 Classic (640x480)":
|
||||
width, height = 640, 480
|
||||
elif aspect_ratio == "3:2 Photography (640x427)":
|
||||
width, height = 640, 427
|
||||
else:
|
||||
width, height = 640, 640
|
||||
|
||||
# Calculate total frames based on duration and fps
|
||||
total_frames = int(duration * fps)
|
||||
if total_frames < 1:
|
||||
total_frames = 1
|
||||
|
||||
# Select interpolation function based on parameter
|
||||
interp_func = self.linear_interpolation
|
||||
if interpolation == "ease_in":
|
||||
interp_func = self.ease_in_interpolation
|
||||
elif interpolation == "ease_out":
|
||||
interp_func = self.ease_out_interpolation
|
||||
elif interpolation == "ease_in_out":
|
||||
interp_func = self.ease_in_out_interpolation
|
||||
|
||||
# Generate frames
|
||||
image_tensors = []
|
||||
for frame in range(total_frames):
|
||||
progress = frame / (total_frames - 1) if total_frames > 1 else 0.0
|
||||
|
||||
# Interpolate parameters based on progress
|
||||
current_heading = interp_func(start_heading, end_heading, progress)
|
||||
current_pitch = interp_func(start_pitch, end_pitch, progress)
|
||||
current_fov = int(interp_func(start_fov, end_fov, progress))
|
||||
|
||||
# Ensure fov is within valid range
|
||||
current_fov = max(10, min(120, current_fov))
|
||||
|
||||
# Handle heading wrap-around (0 to 360 degrees)
|
||||
if abs(end_heading - start_heading) > 180: # Wrap-around case
|
||||
if end_heading > start_heading:
|
||||
current_heading = start_heading + ((current_heading - start_heading + 180) % 360) - 180
|
||||
else:
|
||||
current_heading = start_heading + ((current_heading - start_heading - 180) % 360) + 180
|
||||
|
||||
# Normalize heading to 0-360 range
|
||||
current_heading = current_heading % 360
|
||||
if current_heading < 0:
|
||||
current_heading += 360
|
||||
|
||||
# Fetch the image for this frame
|
||||
image_pil, metadata = fetch_streetview_image(
|
||||
api_key=API_KEY_FROM_ENV,
|
||||
location=location,
|
||||
heading=current_heading,
|
||||
pitch=current_pitch,
|
||||
fov=current_fov,
|
||||
width=width,
|
||||
height=height
|
||||
)
|
||||
|
||||
# Convert the returned PIL image to the tensor format ComfyUI expects
|
||||
image_tensor = self.pil_to_tensor(image_pil)
|
||||
image_tensors.append(image_tensor)
|
||||
|
||||
# Stack all frames into a single tensor
|
||||
if image_tensors:
|
||||
stacked_images = torch.cat(image_tensors, dim=0)
|
||||
metadata = f"Animation: {total_frames} frames, {duration}s at {fps}fps. Parameters: heading ({start_heading:.1f}° to {end_heading:.1f}°), pitch ({start_pitch:.1f}° to {end_pitch:.1f}°), fov ({start_fov} to {end_fov}). Interpolation: {interpolation}"
|
||||
else:
|
||||
# Fallback to single black image if no frames were generated
|
||||
stacked_images = torch.zeros((1, height, width, 3), dtype=torch.float32)
|
||||
metadata = "Error: No frames were generated for animation"
|
||||
|
||||
return (stacked_images, metadata)
|
||||
|
||||
def pil_to_tensor(self, image: Image.Image):
|
||||
""" Helper function to convert a PIL Image to a PyTorch Tensor for ComfyUI. """
|
||||
image_np = np.array(image).astype(np.float32) / 255.0
|
||||
# Add the batch dimension [None,] which is equivalent to unsqueeze(0)
|
||||
return torch.from_numpy(image_np)[None,]
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-streetview-loader"
|
||||
description = "A custom node for ComfyUI that allows you to load images directly from Google Street View using Google Street Static API to use as backgrounds, textures, or inputs in your workflows."
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
license = {file = "LICENSE"}
|
||||
# classifiers = [
|
||||
# # For OS-independent nodes (works on all operating systems)
|
||||
|
||||
Reference in New Issue
Block a user