| """ |
| CLIP Vision Encoder Wrapper for RGB Image Processing |
| |
| This module provides a wrapper around OpenAI's CLIP vision encoder |
| to process RGB camera images and produce features for the ACT policy. |
| |
| Key features: |
| - Loads pretrained CLIP weights (ViT-B/16, ViT-L/14, etc.) |
| - Extracts patch tokens (not CLS token) for spatial features |
| - Projects to policy's hidden_dim |
| - Supports frozen and trainable modes |
| """ |
|
|
| import torch |
| import torch.nn as nn |
| import torchvision.transforms as transforms |
| from typing import Optional, Tuple |
|
|
| |
| try: |
| import open_clip |
| OPEN_CLIP_AVAILABLE = True |
| except ImportError: |
| OPEN_CLIP_AVAILABLE = False |
| print("WARNING: open_clip not available. Install with: pip install open_clip_torch") |
|
|
|
|
| class CLIPEncoder(nn.Module): |
| """ |
| CLIP vision encoder wrapper for RGB image processing. |
| |
| Uses pretrained CLIP models and extracts spatial patch tokens |
| for use in transformer-based policies. |
| Also optionally includes text encoding capability. |
| """ |
| |
| def __init__(self, |
| model_name: str = 'ViT-B-16', |
| pretrained: str = 'openai', |
| hidden_dim: int = 512, |
| freeze: bool = False, |
| image_size: int = 224, |
| enable_text: bool = False): |
| """ |
| Initialize CLIP encoder. |
| |
| Args: |
| model_name: CLIP model architecture ('ViT-B-16', 'ViT-B-32', 'ViT-L-14', etc.) |
| pretrained: Pretrained weights to use ('openai', 'laion2b_s34b_b88k', etc.) |
| hidden_dim: Output feature dimension (for projection layer) |
| freeze: If True, freeze CLIP weights (no gradient updates) |
| image_size: Input image size (CLIP default is 224) |
| enable_text: If True, also initialize text encoder for language conditioning |
| """ |
| super().__init__() |
| |
| if not OPEN_CLIP_AVAILABLE: |
| raise ImportError("open_clip is required. Install with: pip install open_clip_torch") |
| |
| self.model_name = model_name |
| self.hidden_dim = hidden_dim |
| self.freeze = freeze |
| self.image_size = image_size |
| self.enable_text = enable_text |
| |
| |
| print(f"Loading CLIP model: {model_name} with {pretrained} weights") |
| self.clip_model, _, self.preprocess = open_clip.create_model_and_transforms( |
| model_name, |
| pretrained=pretrained, |
| image_size=image_size |
| ) |
| |
| |
| if enable_text: |
| self.tokenizer = open_clip.get_tokenizer(model_name) |
| print(f"CLIP text encoder enabled") |
| |
| |
| self.clip_dim = self.clip_model.visual.output_dim |
| |
| |
| if hasattr(self.clip_model.visual, 'patch_size'): |
| patch_size = self.clip_model.visual.patch_size[0] if isinstance( |
| self.clip_model.visual.patch_size, tuple) else self.clip_model.visual.patch_size |
| else: |
| |
| patch_size = 16 if 'B-16' in model_name or 'L-14' in model_name else 32 |
| |
| self.patch_size = patch_size |
| self.num_patches_per_side = image_size // patch_size |
| self.num_patches = self.num_patches_per_side ** 2 |
| |
| |
| self.projection = nn.Linear(self.clip_dim, hidden_dim) |
| |
| |
| |
| self.pos_embed = nn.Parameter(torch.randn(1, hidden_dim, self.num_patches)) |
| |
| |
| if enable_text: |
| self.text_projection = nn.Linear(self.clip_dim, hidden_dim) |
| |
| |
| if freeze: |
| self._freeze_clip() |
| |
| print(f"CLIP Encoder initialized:") |
| print(f" - Model: {model_name}") |
| print(f" - CLIP dim: {self.clip_dim}") |
| print(f" - Hidden dim: {hidden_dim}") |
| print(f" - Patch size: {patch_size}") |
| print(f" - Num patches: {self.num_patches} ({self.num_patches_per_side}x{self.num_patches_per_side})") |
| print(f" - Frozen: {freeze}") |
| print(f" - Text encoding: {enable_text}") |
| |
| def _freeze_clip(self): |
| """Freeze CLIP model parameters.""" |
| for param in self.clip_model.parameters(): |
| param.requires_grad = False |
| print("CLIP encoder frozen (all CLIP parameters set to requires_grad=False)") |
| |
| def encode_image(self, images: torch.Tensor) -> torch.Tensor: |
| """ |
| Encode images through CLIP visual encoder. |
| |
| Args: |
| images: Input images, shape (B, C, H, W) |
| |
| Returns: |
| Patch tokens, shape (B, num_patches, clip_dim) |
| """ |
| |
| x = images |
| |
| |
| |
| visual = self.clip_model.visual |
| |
| |
| x = visual.conv1(x) |
| |
| |
| x = x.reshape(x.shape[0], x.shape[1], -1) |
| x = x.permute(0, 2, 1) |
| |
| |
| x = torch.cat([ |
| visual.class_embedding.to(x.dtype) + torch.zeros( |
| x.shape[0], 1, x.shape[-1], dtype=x.dtype, device=x.device |
| ), |
| x |
| ], dim=1) |
| x = x + visual.positional_embedding.to(x.dtype) |
| |
| |
| x = visual.ln_pre(x) |
| |
| |
| x = x.permute(1, 0, 2) |
| x = visual.transformer(x) |
| x = x.permute(1, 0, 2) |
| |
| |
| patch_tokens = x[:, 1:, :] |
| |
| return patch_tokens |
| |
| def forward(self, images: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: |
| """ |
| Forward pass through CLIP encoder. |
| |
| Args: |
| images: Input images, shape (B, C, H, W) |
| Images should be normalized with CLIP's normalization |
| |
| Returns: |
| features: Projected features, shape (B, hidden_dim, num_patches) |
| pos: Position embeddings, shape (1, hidden_dim, num_patches) |
| """ |
| |
| if self.freeze: |
| with torch.no_grad(): |
| patch_tokens = self.encode_image(images) |
| else: |
| patch_tokens = self.encode_image(images) |
| |
| |
| projected = self.projection(patch_tokens) |
| |
| |
| features = projected.permute(0, 2, 1) |
| |
| |
| pos = self.pos_embed.expand(images.shape[0], -1, -1) |
| |
| return features, pos |
| |
| def preprocess_images(self, images: torch.Tensor) -> torch.Tensor: |
| """ |
| Preprocess images for CLIP encoder. |
| |
| Args: |
| images: Raw images, shape (B, C, H, W), values in [0, 255] or [0, 1] |
| |
| Returns: |
| Preprocessed images ready for CLIP |
| """ |
| |
| if images.max() > 1.0: |
| images = images / 255.0 |
| |
| |
| |
| mean = torch.tensor([0.48145466, 0.4578275, 0.40821073]).view(1, 3, 1, 1).to(images.device) |
| std = torch.tensor([0.26862954, 0.26130258, 0.27577711]).view(1, 3, 1, 1).to(images.device) |
| |
| images = (images - mean) / std |
| |
| return images |
| |
| def encode_text(self, text_prompts): |
| """ |
| Encode text prompts using CLIP text encoder. |
| |
| Args: |
| text_prompts: List of text strings or single text string |
| |
| Returns: |
| Text embeddings, shape (B, hidden_dim) where B is number of prompts |
| """ |
| if not self.enable_text: |
| raise RuntimeError("Text encoding not enabled. Set enable_text=True during initialization.") |
| |
| |
| if isinstance(text_prompts, str): |
| text_prompts = [text_prompts] |
| |
| |
| text_tokens = self.tokenizer(text_prompts).to(next(self.parameters()).device) |
| |
| |
| if self.freeze: |
| with torch.no_grad(): |
| text_features = self.clip_model.encode_text(text_tokens) |
| else: |
| text_features = self.clip_model.encode_text(text_tokens) |
| |
| |
| text_embeddings = self.text_projection(text_features) |
| |
| return text_embeddings |
| |
| def get_num_params(self) -> int: |
| """Return the number of parameters in the encoder.""" |
| total = sum(p.numel() for p in self.parameters()) |
| trainable = sum(p.numel() for p in self.parameters() if p.requires_grad) |
| print(f"CLIP Encoder: {total:,} total parameters, {trainable:,} trainable") |
| return total |
|
|
|
|
| def create_clip_encoder(model_name: str = 'ViT-B-16', |
| pretrained: str = 'openai', |
| hidden_dim: int = 512, |
| freeze: bool = False, |
| image_size: int = 224, |
| enable_text: bool = False) -> CLIPEncoder: |
| """ |
| Factory function to create a CLIP encoder. |
| |
| Args: |
| model_name: CLIP model architecture |
| pretrained: Pretrained weights |
| hidden_dim: Output feature dimension |
| freeze: Whether to freeze CLIP weights |
| image_size: Input image size |
| enable_text: Whether to enable text encoding |
| |
| Returns: |
| CLIPEncoder instance |
| """ |
| return CLIPEncoder( |
| model_name=model_name, |
| pretrained=pretrained, |
| hidden_dim=hidden_dim, |
| freeze=freeze, |
| image_size=image_size, |
| enable_text=enable_text |
| ) |
|
|
|
|