diff --git a/README.md b/README.md index 55d811c..d7c8d2d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A custom node for ComfyUI that allows you to load images directly from Google St 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. -![Street View Loader Node combine with NanoBanana in ComfyUI](media/preview.png) +![Street View Loader Node](media/preview.png) --- @@ -22,27 +22,21 @@ This gives you the best of both worlds: the authenticity of a real photograph co ### Key Advantages & Use Cases -* **Guaranteed Location Accuracy:** When you need the background to be recognizably *your* street, a specific landmark, or a favorite travel spot, this node is the only way to guarantee a 100% accurate representation. The composition, architecture, and lighting are real, not AI-generated guesswork. - -* **The Ultimate Base for `img2img` and ControlNet:** This is the most powerful use case. You can feed the real Street View image into your workflow as a base for: - * **Stylization:** Keep the exact composition of a place while transforming it into an oil painting, an anime scene, or a charcoal sketch. - * **Restoration/Re-imagining:** Take a view of an old neighborhood and use AI to add futuristic elements, or render it as it might have looked in a different historical period. - * **Precise Inpainting:** Add characters, objects, or fantastical elements into a real-world scene with a background that is perfectly stable and coherent. - -* **Virtual Location Scouting:** For concept artists, filmmakers, and game developers. You can instantly scout real-world locations from your desktop, pull them into ComfyUI, and experiment with different styles and moods for your project without ever leaving your chair. - -* **Personalized and Sentimental Art:** Create a unique piece of art based on a place with personal meaning—a childhood home, a proposal spot, or a favorite vacation view. This creates a connection that a generic prompt could never achieve. - -* **Consistent Backgrounds for Testing:** When testing LoRAs, IPAdapters, or character models, using a consistent, real-world background from this node ensures that you are only seeing the changes from your model, not the random variations of a `txt2img` background. +* **Guaranteed Location Accuracy:** When you need the background to be recognizably *your* street, a specific landmark, or a favorite travel spot, this node is the only way to guarantee a 100% accurate representation. +* **The Ultimate Base for ControlNet:** Feed the real Street View image into your workflow as a base for stylization (anime, oil painting), re-imagining (futuristic, historical), or precise inpainting. +* **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. ![Street View Loader Node combine with NanoBanana in ComfyUI](media/preview_2.png) ## Features - **Direct API Integration:** Pulls images directly from the Google Street View Static API. -- **Full Camera Control:** Adjust Location, Heading (pan), Pitch (tilt), and Field of View (zoom). +- **Easy Workflow with URL Parser:** Just find a spot on Google Maps and paste the URL. - **Secure API Key Storage:** Uses a `.env` file to keep your API key safe and out of your workflow files. +- **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. -- **Easy Workflow Integration:** Outputs a standard `IMAGE` tensor and a metadata string (the fetch URL) for debugging. +- **(Experimental) Panorama Mode:** Stitch multiple images together to create ultra-wide cinematic landscapes. --- @@ -62,7 +56,7 @@ This gives you the best of both worlds: the authenticity of a real photograph co ```bash pip install -r requirements.txt ``` - * this will install the `requests` and `python-dotenv` libraries, which are required for this node.* + *(This will install the `requests` and `python-dotenv` libraries required for the node to function.)* 4. **Restart ComfyUI.** @@ -76,87 +70,79 @@ This node requires a Google Cloud API key to function. Google provides a generou **Part A: Create Project & Enable API** 1. Go to the [Google Cloud Console](https://console.cloud.google.com/). -2. If you don't have a project, create one. If you do, click the project selector at the top and click **"New Project"**. Give it a name like `ComfyUI-API`. -3. Once the project is created, search for "API Library" in the top search bar. -4. In the library, search for and select **"Street View Static API"**. -5. Click the **"Enable"** button. +2. Create a **New Project**. Give it a name like `ComfyUI-API`. +3. In the new project, search for "API Library". +4. In the library, search for and **Enable** the **"Street View Static API"**. **Part B: Set Up Billing** -6. You will be prompted to link a billing account. This is required, but you will **not be charged** unless you exceed the $200 free monthly credit (equivalent to tens of thousands of image requests). +5. You will be prompted to link a billing account. This is required, but you will **not be charged** unless you exceed the $200 free monthly credit. **Part C: Create and Secure Your API Key** -7. In the Cloud Console search bar, navigate to **"Credentials"**. -8. Click **"+ Create Credentials"** and select **"API key"**. -9. Your new API key will be displayed. **Copy this key immediately.** -10. **(IMPORTANT!)** Click the **"Edit API key"** button. Under "API restrictions," select **"Restrict key"**. -11. In the dropdown, find and check **"Street View Static API"**, then click "OK". This ensures your key can *only* be used for this purpose, protecting your account. Click **"Save"**. +6. In the Cloud Console search bar, navigate to **"Credentials"**. +7. Click **"+ Create Credentials"** and select **"API key"**. +8. **Copy this key immediately.** +9. **(IMPORTANT!)** Click **"Edit API key"**. Under "API restrictions," select **"Restrict key"** and add **"Street View Static API"** to the list. This protects your account. Click **"Save"**. **Part D: Configure the Node** -12. In your `ComfyUI/custom_nodes/ComfyUI_StreetView-Loader/` folder, create a new file named `.env`. or rename an existing `.env.example` file to `.env`. -13. Open this `.env` file and add your copied API key in the following format: +10. In your `ComfyUI/custom_nodes/ComfyUI_StreetView-Loader/` folder, create a new file named `.env` (or rename the existing `.env.example` file). +11. Open this `.env` file and add your copied API key in the following format: ``` GOOGLE_STREET_VIEW_API_KEY="your_actual_api_key_goes_here" ``` -14. Save the file. - -Your setup is now complete! +12. Save the file. Your setup is now complete! --- -## 3. How to Use +## 3. How to Use & Upscaling -1. After restarting ComfyUI, double-click the canvas and search for **"Street View Loader"**. -2. The node will appear with several input fields. +The recommended workflow is to use the **URL Parser** node to feed information into the **Loader** node. -### Finding Your Parameters +1. **Find your view** in [Google Maps](https://maps.google.com) and enter Street View. +2. Frame the perfect shot, then **copy the entire URL** from your browser's address bar. +3. In ComfyUI, add the **`Street View URL Parser`** node and paste the URL into it. +4. Add the **`Street View Loader`** node. +5. Connect the outputs of the Parser to the inputs of the Loader (`location` to `location`, etc.). -The best way to get the exact `location`, `heading`, `pitch`, and `fov` is to use the Google Maps URL. +### Understanding the Nodes and Image Size Limit -1. Open [Google Maps](https://maps.google.com) and drop into Street View at your desired location. -2. Pan, tilt, and zoom the camera until you frame the perfect shot. -3. Look at the URL in your browser's address bar. It will look like this: - `https://www.google.com/maps/@**40.74844,-73.98566**,3a,**75**y,**273.99**h,**85.73**t/data=...` +#### `Street View URL Parser` +This node takes a full Google Maps URL as input and outputs the camera parameters (`location`, `heading`, `pitch`, `fov`). - - `@**40.74844,-73.98566**`: This is the **`location`**. - - `**75**y`: This is the **`fov`** (Field of View / Zoom). - - `**273.99**h`: This is the **`heading`**. - - `**85.73**t`: This is related to the **`pitch`**. The API uses a range of -90 (straight down) to +90 (straight up) with 0 being the horizon. A `t` value of `90` in the URL is roughly a `pitch` of `0` for the API. Adjust as needed. - -4. Copy these values into the corresponding fields on the node and generate! - -### **The Easy Way: Using the URL Parser Node** - -As of version 2.0, the recommended workflow is to use the **Street View URL Parser** node. This node extracts all the camera parameters automatically from a single Google Maps URL. - -1. Add two nodes to your workflow: - - `Street View URL Parser` (found in `Ru4ls/StreetView/Utils`) - - `Street View Loader` - -2. Find the exact view you want in Google Street View on your browser. - -3. Copy the **entire URL** from your browser's address bar. - -4. Paste the URL into the `url` field of the **Parser** node. - -5. Connect the outputs of the Parser node to the inputs of the Loader node (`location` to `location`, `heading` to `heading`, etc.). - - -This method is faster, easier, and less error-prone than manually entering each parameter. - -### Node Inputs -- `location`: The latitude and longitude (e.g., `40.74844,-73.98566`). -- `heading`: The compass direction [0 to 360]. 0 is North, 90 is East, 180 is South, 270 is West. -- `pitch`: The up/down tilt of the camera [-90 to 90]. 0 is the horizon. -- `fov`: The field of view, or zoom [10 to 120]. A lower number is more zoomed in. -- `width` / `height`: The dimensions of the output image (max 640x640 for the free API). +#### `Street View Loader` +This is the main node that fetches the image. +- **`aspect_ratio`**: Choose your desired output aspect ratio from the dropdown. This replaces manual width/height inputs. +- **API Limit & Upscaling:** The Google Street View API has a maximum output size of **640x640 pixels**. For high-resolution images (like 1080p or 4K), you **must** use an upscaling workflow. +- **Recommended HD Workflow:** + 1. Select `"16:9 Widescreen (640x360)"` in the `Street View Loader`. + 2. Connect its `IMAGE` output to an **`Upscale Image (using model)`** node. + 3. Use a `Load Upscale Model` node (e.g., `4x-UltraSharp`) to get a final, high-quality **2560x1440** image. --- -## 4. Troubleshooting +## 4. (Experimental) Panoramic Loader Node -- **Node throws a `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`. It must be in the `ComfyUI_StreetView-Loader` folder. -- **The node outputs a black image:** This almost always means Google has no Street View imagery for that exact coordinate, or the API key is invalid/restricted. Check your key's restrictions in the Google Cloud Console and try a slightly different coordinate. The `metadata` output will contain an error message. -- **The node doesn't appear in ComfyUI:** Ensure you have fully restarted the ComfyUI server after installation. +For users who need to create wide, cinematic landscapes, the project includes an experimental **Street View Pano Loader** node. + +![Street View Pano Loader Node](media/preview_3.png) + +### What It Does +This node overcomes the API's FOV limitations by fetching multiple image "tiles" and stitching them side-by-side. For example, requesting **3 images** will result in three `640x640` images being stitched into a single `1920x640` image. + +### **⚠️ Important Experimental Notes:** + +- **API Usage:** This node makes multiple API calls. A panorama with **3 images** will count as **3 requests** against your free monthly Google Cloud credit. +- **Simple Stitching:** This feature uses a basic side-by-side stitch and does not perform advanced perspective correction. It works best for distant landscapes where distortion is minimal. +- **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. + +--- + +## 5. 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. +- **Node not appearing in ComfyUI:** Ensure you have fully restarted the ComfyUI server after installation. + +--- ## License diff --git a/__init__.py b/__init__.py index 003661e..eaad462 100644 --- a/__init__.py +++ b/__init__.py @@ -2,15 +2,18 @@ from .nodes.streetview_loader import StreetViewLoader from .nodes.streetview_url_parser import StreetViewURLParser +from .nodes.streetview_pano_loader import StreetViewPanoLoader NODE_CLASS_MAPPINGS = { "StreetViewLoader": StreetViewLoader, "StreetViewURLParser": StreetViewURLParser, + "StreetViewPanoLoader": StreetViewPanoLoader, } NODE_DISPLAY_NAME_MAPPINGS = { "StreetViewLoader": "Street View Loader", "StreetViewURLParser": "Street View URL Parser", + "StreetViewPanoLoader": "Street View Pano Loader", } __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/media/preview_3.png b/media/preview_3.png new file mode 100644 index 0000000..5d18a80 Binary files /dev/null and b/media/preview_3.png differ diff --git a/nodes/streetview_loader.bu.md b/nodes/streetview_loader.bu.md new file mode 100644 index 0000000..e626e61 --- /dev/null +++ b/nodes/streetview_loader.bu.md @@ -0,0 +1,77 @@ +# file: ComfyUI_StreetView-Loader\nodes\streetview_loader.py + +import torch +import numpy as np +import os +from dotenv import load_dotenv +from PIL import Image + +from ..utils.connect_api_utils import fetch_streetview_image + +# --- Load API Key from .env file --- +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 StreetViewLoader: + """ + A ComfyUI node to load images directly from the Google Street View Static API, + using a secret API key stored in a .env file. + """ + + @classmethod + def INPUT_TYPES(s): + """ + Defines the input fields for the node. The API key is handled in the + background and is not exposed as an input in the UI. + """ + return { + "required": { + "location": ("STRING", { + "multiline": False, + "default": "40.720032,-73.988354" # Example: Near Katz's Deli, NYC + }), + "heading": ("FLOAT", {"default": 151.78, "min": 0, "max": 360, "step": 0.1, "display": "slider"}), + "pitch": ("FLOAT", {"default": -0.76, "min": -90, "max": 90, "step": 0.1, "display": "slider"}), + "fov": ("INT", {"default": 90, "min": 10, "max": 120, "step": 1, "display": "slider"}), + "width": ("INT", {"default": 512, "min": 64, "max": 640, "step": 8}), + "height": ("INT", {"default": 512, "min": 64, "max": 640, "step": 8}), + } + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("image", "metadata") + FUNCTION = "load_image" + CATEGORY = "Ru4ls/StreetView" + + def load_image(self, location, heading, pitch, fov, width, height): + + 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") + + image_pil, metadata = fetch_streetview_image( + api_key=API_KEY_FROM_ENV, + location=location, + heading=heading, + pitch=pitch, + fov=fov, + width=width, + height=height + ) + + # Convert the returned PIL image to the tensor format ComfyUI expects. + image_tensor = self.pil_to_tensor(image_pil) + + return (image_tensor, 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 + return torch.from_numpy(image_np)[None,] \ No newline at end of file diff --git a/nodes/streetview_loader.py b/nodes/streetview_loader.py index e626e61..d321748 100644 --- a/nodes/streetview_loader.py +++ b/nodes/streetview_loader.py @@ -6,9 +6,12 @@ import os from dotenv import load_dotenv from PIL import Image +# Import the refactored API call function from our utility file +# No changes are needed in connect_api_utils.py 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') @@ -23,14 +26,14 @@ API_KEY_FROM_ENV = os.getenv("GOOGLE_STREET_VIEW_API_KEY") class StreetViewLoader: """ A ComfyUI node to load images directly from the Google Street View Static API, - using a secret API key stored in a .env file. + using a secret API key stored in a .env file and aspect ratio presets. """ @classmethod def INPUT_TYPES(s): """ - Defines the input fields for the node. The API key is handled in the - background and is not exposed as an input in the UI. + Defines the input fields for the node. Width and height are now controlled + by a user-friendly aspect ratio dropdown menu. """ return { "required": { @@ -41,8 +44,14 @@ class StreetViewLoader: "heading": ("FLOAT", {"default": 151.78, "min": 0, "max": 360, "step": 0.1, "display": "slider"}), "pitch": ("FLOAT", {"default": -0.76, "min": -90, "max": 90, "step": 0.1, "display": "slider"}), "fov": ("INT", {"default": 90, "min": 10, "max": 120, "step": 1, "display": "slider"}), - "width": ("INT", {"default": 512, "min": 64, "max": 640, "step": 8}), - "height": ("INT", {"default": 512, "min": 64, "max": 640, "step": 8}), + # NEW: Aspect ratio dropdown replaces width/height inputs + "aspect_ratio": ([ + "1:1 Square (640x640)", + "16:9 Widescreen (640x360)", + "9:16 Vertical (360x640)", + "4:3 Classic (640x480)", + "3:2 Photography (640x427)" + ],), } } @@ -51,11 +60,27 @@ class StreetViewLoader: FUNCTION = "load_image" CATEGORY = "Ru4ls/StreetView" - def load_image(self, location, heading, pitch, fov, width, height): - + def load_image(self, location, heading, pitch, fov, aspect_ratio): + # The API key is now taken directly from the globally loaded variable. 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") + # NEW: Logic to 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: + # Fallback to a default just in case + width, height = 640, 640 + + # Call the refactored utility function with the calculated width and height. image_pil, metadata = fetch_streetview_image( api_key=API_KEY_FROM_ENV, location=location, @@ -74,4 +99,5 @@ class StreetViewLoader: 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,] \ No newline at end of file diff --git a/nodes/streetview_pano_loader.py b/nodes/streetview_pano_loader.py new file mode 100644 index 0000000..efd6024 --- /dev/null +++ b/nodes/streetview_pano_loader.py @@ -0,0 +1,102 @@ +# file: ComfyUI_StreetView-Loader/nodes/streetview_pano_loader.py + +import torch +import numpy as np +import os +from dotenv import load_dotenv +from PIL import Image + +# We can reuse the exact same utility function! This is the power of good refactoring. +from ..utils.connect_api_utils import fetch_streetview_image + +# --- Load API Key from .env file --- +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_dotenv(dotenv_path=dotenv_path) +API_KEY_FROM_ENV = os.getenv("GOOGLE_STREET_VIEW_API_KEY") + + +class StreetViewPanoLoader: + """ + A ComfyUI node to load a panoramic image by fetching multiple Google Street + View images with different headings and stitching them side-by-side. + """ + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "location": ("STRING", {"multiline": False, "default": "46.6237597,8.0305018"}), # Switzerland example + "center_heading": ("FLOAT", {"default": 133.44, "min": 0, "max": 360, "step": 0.1, "display": "slider"}), + "pitch": ("FLOAT", {"default": -5.0, "min": -90, "max": 90, "step": 0.1, "display": "slider"}), + "fov_per_image": ("INT", {"default": 90, "min": 30, "max": 120, "step": 1, "display": "slider"}), + "num_images": ("INT", {"default": 3, "min": 2, "max": 5, "step": 1, "display": "slider"}), + } + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("image", "metadata") + FUNCTION = "load_panorama" + CATEGORY = "Ru4ls/StreetView" + + def load_panorama(self, location, center_heading, pitch, fov_per_image, num_images): + if not API_KEY_FROM_ENV: + raise ValueError("Google Street View API key not found in .env file.") + + images = [] + # We will fetch 640x640 images as they give the most vertical data for stitching. + width, height = 640, 640 + + # --- Calculate Headings for Each Image --- + # This determines how far apart each camera shot is. + # For a simple side-by-side stitch, the step angle is equal to the field of view. + step_angle = fov_per_image + + # Calculate the heading for the very first (leftmost) image + start_heading = center_heading - (step_angle * (num_images - 1) / 2.0) + + print(f"StreetView Pano: Fetching {num_images} images with {fov_per_image}° FOV each.") + + for i in range(num_images): + # Calculate the heading for the current shot in the sequence + current_heading = (start_heading + i * step_angle) % 360 + + print(f" - Fetching image {i+1}/{num_images} at heading {current_heading:.2f}°...") + + # Fetch a single image using our existing utility function + image_pil, _ = fetch_streetview_image( + api_key=API_KEY_FROM_ENV, + location=location, + heading=current_heading, + pitch=pitch, + fov=fov_per_image, + width=width, + height=height + ) + if image_pil: + images.append(image_pil) + + if not images: + print("StreetView Pano: Failed to fetch any images.") + blank_image = torch.zeros((1, height, width, 3), dtype=torch.float32) + return (blank_image, "Failed to fetch any images.") + + # --- Stitch the Images Together --- + total_width = width * len(images) + stitched_image = Image.new('RGB', (total_width, height)) + + for i, img in enumerate(images): + stitched_image.paste(img, (i * width, 0)) + + print(f"StreetView Pano: Stitching complete. Final size: {total_width}x{height}") + + # Convert the final stitched PIL image to the tensor format ComfyUI expects + final_tensor = self.pil_to_tensor(stitched_image) + metadata = f"Stitched {len(images)} images. Center Heading: {center_heading}, Total Width: {total_width}px" + + return (final_tensor, metadata) + + def pil_to_tensor(self, image: Image.Image): + image_np = np.array(image).astype(np.float32) / 255.0 + return torch.from_numpy(image_np)[None,] \ No newline at end of file