feat: Add Street View Animator node with animation capabilities and update README and version

This commit is contained in:
ru4ls
2025-11-04 21:04:48 +07:00
parent 6edeead262
commit 658bf287bd
5 changed files with 235 additions and 4 deletions
+52 -3
View File
@@ -2,9 +2,9 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Python 3.12](https://img.shields.io/badge/Python-3.12-blue.svg)](https://www.python.org/downloads/release/python-3120/)
[![Version: v1.01](https://img.shields.io/badge/Version-v1.01-green.svg)](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).
![Street View Loader Node combine with NanoBanana in ComfyUI](media/preview_2.png)
@@ -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.
![Street View Animator Node in ComfyUI](media/StreetView_Animate.png)
<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
View File
@@ -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

+179
View File
@@ -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
View File
@@ -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)