feat: add vectorscope V3 node with YCbCr/HSV/CIE xy color models
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import numpy as np
|
||||
import logging
|
||||
|
||||
try:
|
||||
from ..utils.debug_utils import setup_logging
|
||||
setup_logging()
|
||||
except ImportError:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from ..utils.debug_utils import debug_log
|
||||
except ImportError:
|
||||
def debug_log(logger, level, simple_msg, verbose_msg=None, **kwargs):
|
||||
getattr(logger, level.lower())(simple_msg)
|
||||
|
||||
from comfy_api.latest import io
|
||||
|
||||
SRGB_TO_XYZ = np.array([
|
||||
[0.4124564, 0.3575761, 0.1804375],
|
||||
[0.2126729, 0.7151522, 0.0721750],
|
||||
[0.0193339, 0.1191920, 0.9503041],
|
||||
])
|
||||
|
||||
|
||||
def _convert_ycbcr(pixels):
|
||||
"""Convert RGB pixels to YCbCr Rec.709 scatter coordinates."""
|
||||
r, g, b = pixels[:, 0], pixels[:, 1], pixels[:, 2]
|
||||
y = 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
cb = (b - y) / 1.8556
|
||||
cr = (r - y) / 1.5748
|
||||
return cb, -cr
|
||||
|
||||
|
||||
def _convert_hsv(pixels):
|
||||
"""Convert RGB pixels to HSV polar scatter coordinates."""
|
||||
r, g, b = pixels[:, 0], pixels[:, 1], pixels[:, 2]
|
||||
hue = np.arctan2(np.sqrt(3.0) * (g - b), 2.0 * r - g - b)
|
||||
max_c = np.maximum(np.maximum(r, g), b)
|
||||
min_c = np.minimum(np.minimum(r, g), b)
|
||||
saturation = np.where(max_c > 0, 1.0 - min_c / max_c, 0.0)
|
||||
x = saturation * np.cos(hue)
|
||||
y = saturation * np.sin(hue)
|
||||
return x, y
|
||||
|
||||
|
||||
def _convert_cie_xy(pixels):
|
||||
"""Convert RGB pixels to CIE xy chromaticity scatter coordinates."""
|
||||
xyz = pixels @ SRGB_TO_XYZ.T
|
||||
total = xyz[:, 0] + xyz[:, 1] + xyz[:, 2]
|
||||
valid = total > 1e-10
|
||||
x_chrom = np.where(valid, xyz[:, 0] / total, 0.3127)
|
||||
y_chrom = np.where(valid, xyz[:, 1] / total, 0.3290)
|
||||
x_display = (x_chrom - 0.15) / 0.65
|
||||
y_display = (y_chrom - 0.05) / 0.65
|
||||
return x_display, y_display
|
||||
|
||||
|
||||
class VectorscopeNode(io.ComfyNode):
|
||||
"""Visualizes color distribution on a circular chromaticity display."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="VectorscopeNode",
|
||||
display_name="CoCo Vectorscope",
|
||||
description="Displays a vectorscope visualization of image color distribution with selectable color models.",
|
||||
category="COCO Tools/Analysis",
|
||||
is_output_node=True,
|
||||
inputs=[
|
||||
io.Image.Input("image", tooltip="Image tensor to analyze"),
|
||||
io.Combo.Input("color_model", options=["YCbCr", "HSV", "CIE xy"], default="YCbCr", tooltip="Color model for vectorscope display"),
|
||||
io.Int.Input("sample_count", default=50000, min=5000, max=200000, tooltip="Maximum number of scatter points"),
|
||||
],
|
||||
outputs=[],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, image, color_model="YCbCr", sample_count=50000) -> io.NodeOutput:
|
||||
try:
|
||||
frame = image[0].cpu().numpy()
|
||||
height, width, channels = frame.shape
|
||||
|
||||
pixels = frame.reshape(-1, channels)[:, :3]
|
||||
|
||||
total_pixels = pixels.shape[0]
|
||||
if total_pixels > sample_count:
|
||||
indices = np.linspace(0, total_pixels - 1, sample_count, dtype=int)
|
||||
pixels = pixels[indices]
|
||||
|
||||
converters = {
|
||||
"YCbCr": _convert_ycbcr,
|
||||
"HSV": _convert_hsv,
|
||||
"CIE xy": _convert_cie_xy,
|
||||
}
|
||||
x_coords, y_coords = converters[color_model](pixels)
|
||||
|
||||
x_coords = np.round(x_coords, 4)
|
||||
y_coords = np.round(y_coords, 4)
|
||||
|
||||
result = {
|
||||
"color_model": color_model,
|
||||
"x_coords": x_coords.tolist(),
|
||||
"y_coords": y_coords.tolist(),
|
||||
"point_count": len(x_coords),
|
||||
"image_info": {
|
||||
"width": width,
|
||||
"height": height,
|
||||
"channels": channels,
|
||||
},
|
||||
}
|
||||
|
||||
debug_log(logger, "info",
|
||||
f"Vectorscope computed: {width}x{height}, {color_model}, {len(x_coords)} points")
|
||||
|
||||
return io.NodeOutput(ui={"vectorscope_data": [result]})
|
||||
|
||||
except Exception as e:
|
||||
debug_log(logger, "error", "Vectorscope computation failed", f"Error computing vectorscope: {str(e)}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def fingerprint_inputs(cls, **kwargs):
|
||||
return float("NaN")
|
||||
Reference in New Issue
Block a user