228 lines
7.9 KiB
Python
228 lines
7.9 KiB
Python
# ComfyUI custom node: Camera Angle Control
|
|
# Place this file in: ComfyUI/custom_nodes/comfyui-camera-angle-control/camera_angle_control_node.py
|
|
|
|
from typing import Tuple
|
|
|
|
class CameraAngleControl:
|
|
"""
|
|
A node that generates camera angle descriptions for image generation prompts.
|
|
|
|
FEATURES:
|
|
- Precise Mode: Appends exact degree values for fine-tuning.
|
|
- Strong Keywords: Uses photography terms (Profile, High Angle) to prevent AI from confusing rotation with image flipping.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"precise_mode": ("BOOLEAN", {"default": True, "tooltip": "Include exact numbers (e.g. '77°') alongside keywords."}),
|
|
},
|
|
"optional": {
|
|
# Camera Rotation (Horizontal Pan)
|
|
"enable_rotation": ("BOOLEAN", {"default": False}),
|
|
"rotation_value": ("FLOAT", {
|
|
"default": 0.0,
|
|
"min": -180.0,
|
|
"max": 180.0,
|
|
"step": 5.0,
|
|
"display": "number", # 0=Front, 90=Right, 180=Back, -90=Left
|
|
}),
|
|
|
|
# Camera Vertical Movement (Elevation)
|
|
"enable_vertical": ("BOOLEAN", {"default": False}),
|
|
"vertical_value": ("FLOAT", {
|
|
"default": 0.0,
|
|
"min": -100.0,
|
|
"max": 100.0,
|
|
"step": 5.0,
|
|
"display": "number", # >0 = High Angle, <0 = Low Angle
|
|
}),
|
|
|
|
# Camera Depth Movement (Zoom/Distance)
|
|
"enable_depth": ("BOOLEAN", {"default": False}),
|
|
"depth_value": ("FLOAT", {
|
|
"default": 0.0,
|
|
"min": -100.0,
|
|
"max": 100.0,
|
|
"step": 5.0,
|
|
"display": "number", # >0 = Close, <0 = Far
|
|
}),
|
|
|
|
# Focal Length
|
|
"enable_focal_length": ("BOOLEAN", {"default": False}),
|
|
"focal_length_value": ("FLOAT", {
|
|
"default": 50.0,
|
|
"min": 0.0,
|
|
"max": 600.0,
|
|
"step": 5.0,
|
|
"display": "number",
|
|
}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("camera_description",)
|
|
FUNCTION = "generate_camera_description"
|
|
CATEGORY = "flow-assistor"
|
|
|
|
def _rotation_to_text(self, rotation: float, precise: bool) -> str:
|
|
"""
|
|
Maps degrees to strong viewpoint keywords + optional precise degrees.
|
|
"""
|
|
text = ""
|
|
# Handle the center
|
|
if -10 < rotation < 10:
|
|
text = "front view, looking at camera, symmetrical face"
|
|
else:
|
|
# Determine Left vs Right
|
|
side = "right" if rotation > 0 else "left"
|
|
abs_rot = abs(rotation)
|
|
|
|
# Map ranges to keywords
|
|
if 10 <= abs_rot < 40:
|
|
text = f"slight {side} angle, looking slightly away"
|
|
elif 40 <= abs_rot < 70:
|
|
text = f"three-quarter view from {side}, 3/4 angle"
|
|
elif 70 <= abs_rot < 110:
|
|
text = f"{side} side view, profile shot, from side"
|
|
elif 110 <= abs_rot < 160:
|
|
text = f"view from behind {side}, looking away from camera, dorsal view"
|
|
else:
|
|
text = "back view, view from behind, from back"
|
|
|
|
if precise and abs(rotation) > 0:
|
|
text += f" (angle {rotation:.1f}°)"
|
|
|
|
return text
|
|
|
|
def _vertical_to_text(self, vertical: float, precise: bool) -> str:
|
|
"""
|
|
Maps -100 to 100 to vertical camera angles.
|
|
"""
|
|
text = ""
|
|
if -10 < vertical < 10:
|
|
text = "eye level shot"
|
|
|
|
elif vertical >= 10:
|
|
if vertical < 40:
|
|
text = "slightly high angle, looking down"
|
|
elif vertical < 80:
|
|
text = "high angle, from above"
|
|
else:
|
|
text = "bird's eye view, overhead shot, top down view"
|
|
|
|
else: # vertical <= -10
|
|
if vertical > -40:
|
|
text = "slightly low angle, looking up"
|
|
elif vertical > -80:
|
|
text = "low angle, from below"
|
|
else:
|
|
text = "worm's eye view, ground level shot, extreme low angle"
|
|
|
|
if precise and abs(vertical) > 0:
|
|
text += f" (vertical angle {vertical:.1f})"
|
|
|
|
return text
|
|
|
|
def _depth_to_text(self, depth: float, precise: bool) -> str:
|
|
"""
|
|
Maps -100 to 100 to shot framing/distance.
|
|
"""
|
|
text = ""
|
|
if -10 < depth < 10:
|
|
text = "medium shot, mid shot"
|
|
|
|
elif depth >= 10:
|
|
if depth < 40:
|
|
text = "close-up shot"
|
|
elif depth < 75:
|
|
text = "extreme close-up, face focus"
|
|
else:
|
|
text = "macro photography, extreme detail"
|
|
|
|
else: # depth <= -10
|
|
if depth > -40:
|
|
text = "medium full shot, knees up"
|
|
elif depth > -75:
|
|
text = "full body shot, wide shot"
|
|
else:
|
|
text = "extreme wide shot, distant view, establishing shot"
|
|
|
|
if precise and abs(depth) > 0:
|
|
text += f" (zoom level {depth:.1f})"
|
|
|
|
return text
|
|
|
|
def _focal_length_to_text(self, focal: float, precise: bool) -> str:
|
|
"""
|
|
Maps mm to lens characteristics.
|
|
"""
|
|
text = ""
|
|
if focal < 20:
|
|
text = "fisheye lens, distorted perspective"
|
|
elif focal < 35:
|
|
text = "wide angle lens, expanded background"
|
|
elif focal < 70:
|
|
text = "standard lens, natural perspective"
|
|
elif focal < 110:
|
|
text = "portrait photography, shallow depth of field"
|
|
elif focal < 300:
|
|
text = "telephoto lens, compressed perspective, bokeh"
|
|
else:
|
|
text = "super telephoto lens, extreme compression, blurred background"
|
|
|
|
# Precise always adds the mm for focal length as it is standard terminology
|
|
if precise:
|
|
text += f" ({focal:.1f}mm)"
|
|
else:
|
|
# Even in non-precise mode, adding mm is usually helpful for lens logic,
|
|
# but we respect the toggle if user wants pure keywords.
|
|
pass
|
|
|
|
return text
|
|
|
|
def generate_camera_description(
|
|
self,
|
|
precise_mode: bool = True,
|
|
enable_rotation: bool = False,
|
|
rotation_value: float = 0.0,
|
|
enable_vertical: bool = False,
|
|
vertical_value: float = 0.0,
|
|
enable_depth: bool = False,
|
|
depth_value: float = 0.0,
|
|
enable_focal_length: bool = False,
|
|
focal_length_value: float = 50.0,
|
|
) -> Tuple[str]:
|
|
|
|
parts = []
|
|
|
|
# 1. Rotation
|
|
if enable_rotation:
|
|
parts.append(self._rotation_to_text(rotation_value, precise_mode))
|
|
|
|
# 2. Vertical
|
|
if enable_vertical:
|
|
parts.append(self._vertical_to_text(vertical_value, precise_mode))
|
|
|
|
# 3. Depth
|
|
if enable_depth:
|
|
parts.append(self._depth_to_text(depth_value, precise_mode))
|
|
|
|
# 4. Focal Length
|
|
if enable_focal_length:
|
|
parts.append(self._focal_length_to_text(focal_length_value, precise_mode))
|
|
|
|
final_text = ", ".join(parts)
|
|
|
|
return (final_text,)
|
|
|
|
|
|
# Required mappings so ComfyUI can discover the node
|
|
NODE_CLASS_MAPPINGS = {
|
|
"CameraAngleControl": CameraAngleControl,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"CameraAngleControl": "Camera Angle Control",
|
|
} |