Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion blueprints/Color Curves.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"revision": 0,"last_node_id": 10,"last_link_id": 0,"nodes": [{"id": 10,"type": "d5c462c8-1372-4af8-84f2-547c83470d04","pos": [3610,-2630],"size": [270,420],"flags": {},"order": 0,"mode": 0,"inputs": [{"label": "image","localized_name": "images.image0","name": "images.image0","type": "IMAGE","link": null}],"outputs": [{"label": "IMAGE","localized_name": "IMAGE0","name": "IMAGE0","type": "IMAGE","links": []}],"properties": {"proxyWidgets": [["4","curve"],["5","curve"],["6","curve"],["7","curve"]]},"widgets_values": [],"title": "Color Curves"}],"links": [],"version": 0.4,"definitions": {"subgraphs": [{"id": "d5c462c8-1372-4af8-84f2-547c83470d04","version": 1,"state": {"lastGroupId": 0,"lastNodeId": 8,"lastLinkId": 33,"lastRerouteId": 0},"revision": 0,"config": {},"name": "Color Curves","inputNode": {"id": -10,"bounding": [2660,-4500,120,60]},"outputNode": {"id": -20,"bounding": [4270,-4500,120,60]},"inputs": [{"id": "abc345b7-f55e-4f32-a11d-3aa4c2b0936b","name": "images.image0","type": "IMAGE","linkIds": [29],"localized_name": "images.image0","label": "image","pos": [2760,-4480]}],"outputs": [{"id": "eb0ec079-46da-4408-8263-9ef85569d33d","name": "IMAGE0","type": "IMAGE","linkIds": [28],"localized_name": "IMAGE0","label": "IMAGE","pos": [4290,-4480]}],"widgets": [],"nodes": [{"id": 4,"type": "CurveEditor","pos": [3060,-4500],"size": [270,200],"flags": {},"order": 0,"mode": 0,"inputs": [{"label": "curve","localized_name": "curve","name": "curve","type": "CURVE","widget": {"name": "curve"},"link": null},{"label": "histogram","localized_name": "histogram","name": "histogram","type": "HISTOGRAM","shape": 7,"link": null}],"outputs": [{"localized_name": "CURVE","name": "CURVE","type": "CURVE","links": [30]}],"title": "RGB Master","properties": {"Node name for S&R": "CurveEditor"},"widgets_values": []},{"id": 5,"type": "CurveEditor","pos": [3060,-4250],"size": [270,200],"flags": {},"order": 1,"mode": 0,"inputs": [{"label": "curve","localized_name": "curve","name": "curve","type": "CURVE","widget": {"name": "curve"},"link": null},{"label": "histogram","localized_name": "histogram","name": "histogram","type": "HISTOGRAM","shape": 7,"link": null}],"outputs": [{"localized_name": "CURVE","name": "CURVE","type": "CURVE","links": [31]}],"title": "Red","properties": {"Node name for S&R": "CurveEditor"},"widgets_values": []},{"id": 6,"type": "CurveEditor","pos": [3060,-4000],"size": [270,200],"flags": {},"order": 2,"mode": 0,"inputs": [{"label": "curve","localized_name": "curve","name": "curve","type": "CURVE","widget": {"name": "curve"},"link": null},{"label": "histogram","localized_name": "histogram","name": "histogram","type": "HISTOGRAM","shape": 7,"link": null}],"outputs": [{"localized_name": "CURVE","name": "CURVE","type": "CURVE","links": [32]}],"title": "Green","properties": {"Node name for S&R": "CurveEditor"},"widgets_values": []},{"id": 7,"type": "CurveEditor","pos": [3060,-3750],"size": [270,200],"flags": {},"order": 3,"mode": 0,"inputs": [{"label": "curve","localized_name": "curve","name": "curve","type": "CURVE","widget": {"name": "curve"},"link": null},{"label": "histogram","localized_name": "histogram","name": "histogram","type": "HISTOGRAM","shape": 7,"link": null}],"outputs": [{"localized_name": "CURVE","name": "CURVE","type": "CURVE","links": [33]}],"title": "Blue","properties": {"Node name for S&R": "CurveEditor"},"widgets_values": []},{"id": 8,"type": "GLSLShader","pos": [3590,-4500],"size": [420,500],"flags": {},"order": 4,"mode": 0,"inputs": [{"label": "image0","localized_name": "images.image0","name": "images.image0","type": "IMAGE","link": 29},{"label": "image1","localized_name": "images.image1","name": "images.image1","shape": 7,"type": "IMAGE","link": null},{"label": "u_curve0","localized_name": "curves.u_curve0","name": "curves.u_curve0","shape": 7,"type": "CURVE","link": 30},{"label": "u_curve1","localized_name": "curves.u_curve1","name": "curves.u_curve1","shape": 7,"type": "CURVE","link": 31},{"label": "u_curve2","localized_name": "curves.u_curve2","name": "curves.u_curve2","shape": 7,"type": "CURVE","link": 32},{"label": "u_curve3","localized_name": "curves.u_curve3","name": "curves.u_curve3","shape": 7,"type": "CURVE","link": 33},{"localized_name": "fragment_shader","name": "fragment_shader","type": "STRING","widget": {"name": "fragment_shader"},"link": null},{"localized_name": "size_mode","name": "size_mode","type": "COMFY_DYNAMICCOMBO_V3","widget": {"name": "size_mode"},"link": null}],"outputs": [{"localized_name": "IMAGE0","name": "IMAGE0","type": "IMAGE","links": [28]},{"localized_name": "IMAGE1","name": "IMAGE1","type": "IMAGE","links": null},{"localized_name": "IMAGE2","name": "IMAGE2","type": "IMAGE","links": null},{"localized_name": "IMAGE3","name": "IMAGE3","type": "IMAGE","links": null}],"properties": {"Node name for S&R": "GLSLShader"},"widgets_values": ["#version 300 es\nprecision highp float;\n\nuniform sampler2D u_image0;\nuniform sampler2D u_curve0; // RGB master curve (256x1 LUT)\nuniform sampler2D u_curve1; // Red channel curve\nuniform sampler2D u_curve2; // Green channel curve\nuniform sampler2D u_curve3; // Blue channel curve\n\nin vec2 v_texCoord;\nlayout(location = 0) out vec4 fragColor0;\n\n// GIMP-compatible curve lookup with manual linear interpolation.\n// Matches gimp_curve_map_value_inline() from gimpcurve-map.c:\n// index = value * (n_samples - 1)\n// f = fract(index)\n// result = (1-f) * samples[floor] + f * samples[ceil]\n//\n// Uses texelFetch (NEAREST) to avoid GPU half-texel offset issues\n// that occur with texture() + GL_LINEAR on small 256x1 LUTs.\nfloat applyCurve(sampler2D curve, float value) {\n value = clamp(value, 0.0, 1.0);\n\n float pos = value * 255.0;\n int lo = int(floor(pos));\n int hi = min(lo + 1, 255);\n float f = pos - float(lo);\n\n float a = texelFetch(curve, ivec2(lo, 0), 0).r;\n float b = texelFetch(curve, ivec2(hi, 0), 0).r;\n\n return a + f * (b - a);\n}\n\nvoid main() {\n vec4 color = texture(u_image0, v_texCoord);\n\n // GIMP order: per-channel curves first, then RGB master curve.\n // See gimp_curve_map_pixels() default case in gimpcurve-map.c:\n // dest = colors_curve( channel_curve( src ) )\n float tmp_r = applyCurve(u_curve1, color.r);\n float tmp_g = applyCurve(u_curve2, color.g);\n float tmp_b = applyCurve(u_curve3, color.b);\n color.r = applyCurve(u_curve0, tmp_r);\n color.g = applyCurve(u_curve0, tmp_g);\n color.b = applyCurve(u_curve0, tmp_b);\n\n fragColor0 = vec4(color.rgb, color.a);\n}\n","from_input"]}],"groups": [],"links": [{"id": 29,"origin_id": -10,"origin_slot": 0,"target_id": 8,"target_slot": 0,"type": "IMAGE"},{"id": 28,"origin_id": 8,"origin_slot": 0,"target_id": -20,"target_slot": 0,"type": "IMAGE"},{"id": 30,"origin_id": 4,"origin_slot": 0,"target_id": 8,"target_slot": 2,"type": "CURVE"},{"id": 31,"origin_id": 5,"origin_slot": 0,"target_id": 8,"target_slot": 3,"type": "CURVE"},{"id": 32,"origin_id": 6,"origin_slot": 0,"target_id": 8,"target_slot": 4,"type": "CURVE"},{"id": 33,"origin_id": 7,"origin_slot": 0,"target_id": 8,"target_slot": 5,"type": "CURVE"}],"extra": {"workflowRendererVersion": "LG"},"category": "Image Tools/Color adjust"}]}}
{"revision": 0, "last_node_id": 10, "last_link_id": 0, "nodes": [{"id": 10, "type": "d5c462c8-1372-4af8-84f2-547c83470d04", "pos": [3610, -2630], "size": [270, 420], "flags": {}, "order": 0, "mode": 0, "inputs": [{"label": "image", "localized_name": "images.image0", "name": "images.image0", "type": "IMAGE", "link": null}], "outputs": [{"label": "IMAGE", "localized_name": "IMAGE0", "name": "IMAGE0", "type": "IMAGE", "links": []}], "properties": {"proxyWidgets": [["4", "curve"], ["5", "curve"], ["6", "curve"], ["7", "curve"]]}, "widgets_values": [], "title": "Color Curves"}], "links": [], "version": 0.4, "definitions": {"subgraphs": [{"id": "d5c462c8-1372-4af8-84f2-547c83470d04", "version": 1, "state": {"lastGroupId": 0, "lastNodeId": 9, "lastLinkId": 38, "lastRerouteId": 0}, "revision": 0, "config": {}, "name": "Color Curves", "inputNode": {"id": -10, "bounding": [2660, -4500, 120, 60]}, "outputNode": {"id": -20, "bounding": [4270, -4500, 120, 60]}, "inputs": [{"id": "abc345b7-f55e-4f32-a11d-3aa4c2b0936b", "name": "images.image0", "type": "IMAGE", "linkIds": [29, 34], "localized_name": "images.image0", "label": "image", "pos": [2760, -4480]}], "outputs": [{"id": "eb0ec079-46da-4408-8263-9ef85569d33d", "name": "IMAGE0", "type": "IMAGE", "linkIds": [28], "localized_name": "IMAGE0", "label": "IMAGE", "pos": [4290, -4480]}], "widgets": [], "nodes": [{"id": 4, "type": "CurveEditor", "pos": [3060, -4500], "size": [270, 200], "flags": {}, "order": 0, "mode": 0, "inputs": [{"label": "curve", "localized_name": "curve", "name": "curve", "type": "CURVE", "widget": {"name": "curve"}, "link": null}, {"label": "histogram", "localized_name": "histogram", "name": "histogram", "type": "HISTOGRAM", "shape": 7, "link": 35}], "outputs": [{"localized_name": "CURVE", "name": "CURVE", "type": "CURVE", "links": [30]}], "title": "RGB Master", "properties": {"Node name for S&R": "CurveEditor"}, "widgets_values": []}, {"id": 5, "type": "CurveEditor", "pos": [3060, -4250], "size": [270, 200], "flags": {}, "order": 1, "mode": 0, "inputs": [{"label": "curve", "localized_name": "curve", "name": "curve", "type": "CURVE", "widget": {"name": "curve"}, "link": null}, {"label": "histogram", "localized_name": "histogram", "name": "histogram", "type": "HISTOGRAM", "shape": 7, "link": 36}], "outputs": [{"localized_name": "CURVE", "name": "CURVE", "type": "CURVE", "links": [31]}], "title": "Red", "properties": {"Node name for S&R": "CurveEditor"}, "widgets_values": []}, {"id": 6, "type": "CurveEditor", "pos": [3060, -4000], "size": [270, 200], "flags": {}, "order": 2, "mode": 0, "inputs": [{"label": "curve", "localized_name": "curve", "name": "curve", "type": "CURVE", "widget": {"name": "curve"}, "link": null}, {"label": "histogram", "localized_name": "histogram", "name": "histogram", "type": "HISTOGRAM", "shape": 7, "link": 37}], "outputs": [{"localized_name": "CURVE", "name": "CURVE", "type": "CURVE", "links": [32]}], "title": "Green", "properties": {"Node name for S&R": "CurveEditor"}, "widgets_values": []}, {"id": 7, "type": "CurveEditor", "pos": [3060, -3750], "size": [270, 200], "flags": {}, "order": 3, "mode": 0, "inputs": [{"label": "curve", "localized_name": "curve", "name": "curve", "type": "CURVE", "widget": {"name": "curve"}, "link": null}, {"label": "histogram", "localized_name": "histogram", "name": "histogram", "type": "HISTOGRAM", "shape": 7, "link": 38}], "outputs": [{"localized_name": "CURVE", "name": "CURVE", "type": "CURVE", "links": [33]}], "title": "Blue", "properties": {"Node name for S&R": "CurveEditor"}, "widgets_values": []}, {"id": 8, "type": "GLSLShader", "pos": [3590, -4500], "size": [420, 500], "flags": {}, "order": 4, "mode": 0, "inputs": [{"label": "image0", "localized_name": "images.image0", "name": "images.image0", "type": "IMAGE", "link": 29}, {"label": "image1", "localized_name": "images.image1", "name": "images.image1", "shape": 7, "type": "IMAGE", "link": null}, {"label": "u_curve0", "localized_name": "curves.u_curve0", "name": "curves.u_curve0", "shape": 7, "type": "CURVE", "link": 30}, {"label": "u_curve1", "localized_name": "curves.u_curve1", "name": "curves.u_curve1", "shape": 7, "type": "CURVE", "link": 31}, {"label": "u_curve2", "localized_name": "curves.u_curve2", "name": "curves.u_curve2", "shape": 7, "type": "CURVE", "link": 32}, {"label": "u_curve3", "localized_name": "curves.u_curve3", "name": "curves.u_curve3", "shape": 7, "type": "CURVE", "link": 33}, {"localized_name": "fragment_shader", "name": "fragment_shader", "type": "STRING", "widget": {"name": "fragment_shader"}, "link": null}, {"localized_name": "size_mode", "name": "size_mode", "type": "COMFY_DYNAMICCOMBO_V3", "widget": {"name": "size_mode"}, "link": null}], "outputs": [{"localized_name": "IMAGE0", "name": "IMAGE0", "type": "IMAGE", "links": [28]}, {"localized_name": "IMAGE1", "name": "IMAGE1", "type": "IMAGE", "links": null}, {"localized_name": "IMAGE2", "name": "IMAGE2", "type": "IMAGE", "links": null}, {"localized_name": "IMAGE3", "name": "IMAGE3", "type": "IMAGE", "links": null}], "properties": {"Node name for S&R": "GLSLShader"}, "widgets_values": ["#version 300 es\nprecision highp float;\n\nuniform sampler2D u_image0;\nuniform sampler2D u_curve0; // RGB master curve (256x1 LUT)\nuniform sampler2D u_curve1; // Red channel curve\nuniform sampler2D u_curve2; // Green channel curve\nuniform sampler2D u_curve3; // Blue channel curve\n\nin vec2 v_texCoord;\nlayout(location = 0) out vec4 fragColor0;\n\n// GIMP-compatible curve lookup with manual linear interpolation.\n// Matches gimp_curve_map_value_inline() from gimpcurve-map.c:\n// index = value * (n_samples - 1)\n// f = fract(index)\n// result = (1-f) * samples[floor] + f * samples[ceil]\n//\n// Uses texelFetch (NEAREST) to avoid GPU half-texel offset issues\n// that occur with texture() + GL_LINEAR on small 256x1 LUTs.\nfloat applyCurve(sampler2D curve, float value) {\n value = clamp(value, 0.0, 1.0);\n\n float pos = value * 255.0;\n int lo = int(floor(pos));\n int hi = min(lo + 1, 255);\n float f = pos - float(lo);\n\n float a = texelFetch(curve, ivec2(lo, 0), 0).r;\n float b = texelFetch(curve, ivec2(hi, 0), 0).r;\n\n return a + f * (b - a);\n}\n\nvoid main() {\n vec4 color = texture(u_image0, v_texCoord);\n\n // GIMP order: per-channel curves first, then RGB master curve.\n // See gimp_curve_map_pixels() default case in gimpcurve-map.c:\n // dest = colors_curve( channel_curve( src ) )\n float tmp_r = applyCurve(u_curve1, color.r);\n float tmp_g = applyCurve(u_curve2, color.g);\n float tmp_b = applyCurve(u_curve3, color.b);\n color.r = applyCurve(u_curve0, tmp_r);\n color.g = applyCurve(u_curve0, tmp_g);\n color.b = applyCurve(u_curve0, tmp_b);\n\n fragColor0 = vec4(color.rgb, color.a);\n}\n", "from_input"]}, {"id": 9, "type": "ImageHistogram", "pos": [2800, -4300], "size": [210, 150], "flags": {}, "order": 5, "mode": 0, "inputs": [{"label": "image", "localized_name": "image", "name": "image", "type": "IMAGE", "link": 34}], "outputs": [{"localized_name": "HISTOGRAM", "name": "rgb", "type": "HISTOGRAM", "links": [35]}, {"localized_name": "HISTOGRAM", "name": "luminance", "type": "HISTOGRAM", "links": []}, {"localized_name": "HISTOGRAM", "name": "red", "type": "HISTOGRAM", "links": [36]}, {"localized_name": "HISTOGRAM", "name": "green", "type": "HISTOGRAM", "links": [37]}, {"localized_name": "HISTOGRAM", "name": "blue", "type": "HISTOGRAM", "links": [38]}], "properties": {"Node name for S&R": "ImageHistogram"}, "widgets_values": []}], "groups": [], "links": [{"id": 29, "origin_id": -10, "origin_slot": 0, "target_id": 8, "target_slot": 0, "type": "IMAGE"}, {"id": 28, "origin_id": 8, "origin_slot": 0, "target_id": -20, "target_slot": 0, "type": "IMAGE"}, {"id": 30, "origin_id": 4, "origin_slot": 0, "target_id": 8, "target_slot": 2, "type": "CURVE"}, {"id": 31, "origin_id": 5, "origin_slot": 0, "target_id": 8, "target_slot": 3, "type": "CURVE"}, {"id": 32, "origin_id": 6, "origin_slot": 0, "target_id": 8, "target_slot": 4, "type": "CURVE"}, {"id": 33, "origin_id": 7, "origin_slot": 0, "target_id": 8, "target_slot": 5, "type": "CURVE"}, {"id": 34, "origin_id": -10, "origin_slot": 0, "target_id": 9, "target_slot": 0, "type": "IMAGE"}, {"id": 35, "origin_id": 9, "origin_slot": 0, "target_id": 4, "target_slot": 1, "type": "HISTOGRAM"}, {"id": 36, "origin_id": 9, "origin_slot": 2, "target_id": 5, "target_slot": 1, "type": "HISTOGRAM"}, {"id": 37, "origin_id": 9, "origin_slot": 3, "target_id": 6, "target_slot": 1, "type": "HISTOGRAM"}, {"id": 38, "origin_id": 9, "origin_slot": 4, "target_id": 7, "target_slot": 1, "type": "HISTOGRAM"}], "extra": {"workflowRendererVersion": "LG"}, "category": "Image Tools/Color adjust"}]}}
52 changes: 51 additions & 1 deletion comfy_extras/nodes_curve.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import numpy as np

from comfy_api.latest import ComfyExtension, io
from comfy_api.input import CurveInput
from typing_extensions import override
Expand Down Expand Up @@ -32,10 +34,58 @@ def execute(cls, curve, histogram=None) -> io.NodeOutput:
return io.NodeOutput(result, ui=ui) if ui else io.NodeOutput(result)


class ImageHistogram(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageHistogram",
display_name="Image Histogram",
category="utils",
inputs=[
io.Image.Input("image"),
],
outputs=[
io.Histogram.Output("rgb"),
io.Histogram.Output("luminance"),
io.Histogram.Output("red"),
io.Histogram.Output("green"),
io.Histogram.Output("blue"),
],
)

@classmethod
def execute(cls, image) -> io.NodeOutput:
img = image[0].cpu().numpy()
img_uint8 = np.clip(img * 255, 0, 255).astype(np.uint8)

def bincount(data):
return np.bincount(data.ravel(), minlength=256)[:256]

hist_r = bincount(img_uint8[:, :, 0])
hist_g = bincount(img_uint8[:, :, 1])
hist_b = bincount(img_uint8[:, :, 2])

# Average of R, G, B histograms (same as Photoshop's RGB composite)
rgb = ((hist_r + hist_g + hist_b) // 3).tolist()

# ITU-R BT.709-6, Item 3.2 (p.6) — Derivation of luminance signal
# https://www.itu.int/rec/R-REC-BT.709-6-201506-I/en
lum = 0.2126 * img[:, :, 0] + 0.7152 * img[:, :, 1] + 0.0722 * img[:, :, 2]
luminance = bincount(np.clip(lum * 255, 0, 255).astype(np.uint8)).tolist()

return io.NodeOutput(
rgb,
luminance,
hist_r.tolist(),
hist_g.tolist(),
hist_b.tolist(),
)


class CurveExtension(ComfyExtension):
@override
async def get_node_list(self):
return [CurveEditor]
return [CurveEditor, ImageHistogram]


async def comfy_entrypoint():
Expand Down
Loading