Skip to content

Commit

Permalink
Merge pull request #3 from MrLixm/feat-whitebalance
Browse files Browse the repository at this point in the history
Feat: add whitebalance tool
  • Loading branch information
MrLixm authored Dec 28, 2023
2 parents aa2784d + a8a0fcd commit ecb07a8
Show file tree
Hide file tree
Showing 9 changed files with 376 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ _this table might not be always up-to-date_
| [ocio-contrast-log](src/ocio-contrast-log) | contrast on log encoded imagery based on the OCIO implementation | nodes | ![grading](https://img.shields.io/badge/grading-43896b) |
| [ocio-saturation](src/ocio-saturation) | saturation with variable weights also based on OCIO implementation | nodes | ![grading](https://img.shields.io/badge/grading-43896b) |
| [hsv](src/hsv) | color correction with HSV model | nodes | ![grading](https://img.shields.io/badge/grading-43896b) |
| [whitebalance](src/whitebalance) | creative white balance with temperature/tint | nodes, blink | ![grading](https://img.shields.io/badge/grading-43896b) |

# Utilisation

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "foundry-nuke"
version = "0.4.0"
version = "0.5.0"
description = "Collection of script & resources for Foundry's Nuke software."
authors = ["Liam Collod <monsieurlixm@gmail.com>"]
readme = "README.md"
Expand Down
56 changes: 56 additions & 0 deletions src/whitebalance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# White Balance

Change the "white balance" of your source imagery.

This is not a technical transform and was more designed as creative transformation tool.
Usually whitebalancing is applied on undemosaiced raw camera data while
this node will operate on already debayered data.

![demo screenshot of the node in nuke](cover.png)

# Instructions

## Install

- Copy/paste the content of [WhiteBalance.nk](WhiteBalance.nk) in any nuke
scene.
- That's it

## Requirements

The tool use the following features :

- blink script but works on non-commercial versions >= 14.0

## Reference

Default value for temperature and tint try to match the value of the illuminant E
which should produce a "no-operation" result.

### temperature

In Kelvin, lower produce a warmer look, higher a colder one.

### tint

Deviation from the planckian locus, referred usually as "D*uv*". Scaled x3000
for conveniency. Negatives values produce a pink shift while positive value a green tint.

### intensity

Strength of the effect applied as a simple linear interpolation. 0.0 means no
effect.

### show RGB coefficients

Fill the image with the R-G-B coefficient used to whitebalance the image. Useful
for debugging or alternative workflow:
- provide a 1x1 pixels input in the node
- check `show RGB coefficients`
- reformat output to size of your image
- merge previous step with the actual image using a `multiply` blend mode


# Developer

See the [./src/](./src) folder for development instructions.
47 changes: 47 additions & 0 deletions src/whitebalance/WhiteBalance.nk
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Group {
name WhiteBalance
tile_color 0xca7c4200
addUserKnob {20 User}
addUserKnob {26 txt_title l "" T "<h1>WhiteBalance</h1>"}
addUserKnob {7 temperature l "temperature" t "CCT expressed in Kelvin" R 1000 15000}
temperature 5495
addUserKnob {7 tint l "tint" t "Deviation from planckian locus. Negatives are pinkish, positives are greener." R -150 150}
tint -13
addUserKnob {7 intensity l "intensity" t "Intensity of the balancing using linear interpolation." R 0.0 5.0}
intensity 1.0
addUserKnob {6 show_coefficients l "show RGB coefficients" t "Fill the input with RGB coefficients instead of white-balancing." +STARTLINE}
addUserKnob {20 About}
addUserKnob {26 toolName l name T WhiteBalance}
addUserKnob {26 toolVersion l version T 0.1.0}
addUserKnob {26 toolAuthor l author T "<a style=\"color: rgb(200,200,200);\" href=\"https://mrlixm.github.io/\">Liam Collod</a>"}
addUserKnob {26 toolDescription l description T "Creative white-balancing with temperature and tint control."}
addUserKnob {26 toolUrl l url T "<a style=\"color: rgb(200,200,200);\" href=\"https://github.com/MrLixm/Foundry_Nuke\">https://github.com/MrLixm/Foundry_Nuke</a>"}
}
Input {
inputs 0
name Input1
xpos 0
}
BlinkScript {
inputs 1
recompileCount 2
ProgramGroup 1
KernelDescription "3 \"WhiteBalance\" iterate pixelWise 18eb8371345f90c6c9786195225c12c35bf1c5b11eeec618d305318349545608 2 \"src\" Read Point \"dst\" Write Point 4 \"u_show_coeffs\" Bool 1 AA== \"u_temperature\" Float 1 AACvRQ== \"u_tint\" Float 1 AAB4wQ== \"u_intensity\" Float 1 AACAPw== 4 \"u_show_coeffs\" 1 1 Default \"u_temperature\" 1 1 Default \"u_tint\" 1 1 Default \"u_intensity\" 1 1 Default 0"
kernelSource "// version 2\n//\n// References :\n// - \[2] Ohno, Yoshi (2014). Practical Use and Calculation of CCT and Duv. LEUKOS, 10(1), 47-55. doi:10.1080/15502724.2014.839020\n// - \[3] https://en.wikipedia.org/wiki/Planckian_locus#Approximation\n// - \[4] SMPTE Recommended Practice - Derivation of Basic Television Color Equations https://ieeexplore.ieee.org/document/7291155\n\n#define ohno_deltaT float(0.01)\n\n\nfloat powsafe(float color, float power)\{\n // pow() but safe for NaNs/negatives\n return pow(fabs(color), power) * sign(color);\n\}\n\n\nfloat2 convert_CCT_to_uv_Krystek1985(float CCT)\{\n // Convert the given CCT to CIE 1960 u,v colorspace values using Krystek\\'s method.\n //\n // Krystek\\'s method is an approximation and not intended for accuracy.\n //\n // :param CCT: in kelvin, ~\[1000-15000] range\n // --\[3]\n float CCT_2 = pow(CCT,2.0f);\n float u = 0.860117757f + 1.54118254f * pow(10.0f,-4.0f) * CCT + 1.28641212f * pow(10.0f,-7.0f) * CCT_2;\n u = u / (1.0f + 8.42420235f * pow(10.0f,-4.0f) * CCT + 7.08145163f * pow(10.0f,-7.0f) * CCT_2);\n float v = 0.317398726f + 4.22806245f * pow(10.0f,-5.0f) * CCT + 4.20481691f * pow(10.0f,-8.0f) * CCT_2;\n v = v / (1.0f - 2.89741816f * pow(10.0f,-5.0f) * CCT + 1.61456053f * pow(10.0f,-7.0f) * CCT_2);\n return float2(u, v);\n\}\n\n\nfloat2 convert_CCT_Duv_to_xy(float CCT, float Duv)\{\n // :param CCT: correlated color temperature in kelvin, ~\[1000-15000] range\n // :param Duv: also called \"tint\" \[-0.05-+0.05] range\n // -- \[2]\n float2 uv0 = convert_CCT_to_uv_Krystek1985(CCT);\n float2 uv1 = convert_CCT_to_uv_Krystek1985(CCT + ohno_deltaT);\n\n float du = uv0.x - uv1.x;\n float dv = uv0.y - uv1.y;\n\n float hypothenus = sqrt(powsafe(du,2.0f) + powsafe(dv,2.0f));\n float sinTheta = dv / hypothenus;\n float cosTheta = du / hypothenus;\n\n float u = uv0.x - Duv * sinTheta;\n float v = uv0.y + Duv * cosTheta;\n\n float u_p = u;\n float v_p = 1.5f * v;\n\n float x = 9.0f * u_p / (6.0f * u_p - 16.0f * v_p + 12.0f);\n float y = 2.0f * v_p / (3.0f * u_p - 8.0f * v_p + 6.0f);\n return float2(x, y);\n\}\n\n\nkernel WhiteBalance : ImageComputationKernel<ePixelWise>\n\{\n Image<eRead, eAccessPoint, eEdgeClamped> src;\n Image<eWrite> dst;\n\n param:\n bool u_show_coeffs;\n float u_temperature;\n float u_tint;\n float u_intensity;\n\n void define()\{\n // default values try to match illuminant E\n defineParam(u_temperature, \"u_temperature\", 5600.0f);\n defineParam(u_tint, \"u_tint\", -15.5f);\n defineParam(u_intensity, \"u_intensity\", 1.0f);\n \}\n\n float lerp(float a1, float a2, float amount)\{\n // linear interpolation between 2 values\n return (1.0f - amount) * a1 + amount * a2;\n \}\n\n void process() \{\n\n // 3000 is an arbitrary scale for the tint parameter to have a more UI friendly range.\n // (actually same as Adobe)\n float2 new_white_xy = convert_CCT_Duv_to_xy(u_temperature, u_tint/3000.0f);\n\n // --\[4] normalise primary matrix algorithm but only with whitepoint\n float Wz = 1.0f - new_white_xy.x - new_white_xy.y;\n float3 W = float3(new_white_xy.x / new_white_xy.y, 1.0f, Wz / new_white_xy.y);\n\n float4 rgba = src();\n float3 new_rgb(rgba.x, rgba.y, rgba.z);\n\n if (u_show_coeffs)\{\n new_rgb.x = W.x;\n new_rgb.y = W.y;\n new_rgb.z = W.z;\n \} else \{\n new_rgb.x = lerp(rgba.x, new_rgb.x * W.x, u_intensity);\n new_rgb.y = lerp(rgba.y, new_rgb.y * W.y, u_intensity);\n new_rgb.z = lerp(rgba.z, new_rgb.z * W.z, u_intensity);\n \}\n\n dst() = float4(\n new_rgb.x,\n new_rgb.y,\n new_rgb.z,\n rgba.w\n );\n \}\n\};"
rebuild ""
WhiteBalance_u_temperature {{parent.temperature}}
WhiteBalance_u_tint {{parent.tint}}
WhiteBalance_u_intensity {{parent.intensity}}
WhiteBalance_u_show_coeffs {{parent.show_coefficients}}
format "2048 2048 0 0 2048 2048 1 square_2K"
rebuild_finalise ""
name WhiteBalanceBlink
xpos 0
ypos 150
}
Output {
name Output1
xpos 0
ypos 300
}
end_group
Binary file added src/whitebalance/cover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions src/whitebalance/src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# src

code here need to be "compiled" to be usable. This is achieved by executing
the `build.py` file.

# build instructions

## build-requires

- python-3
- any nuke version (including non-commercial)

## build-usage

- take the blink script at root and import them into a nuke scene
- make sure to compile the blink script
- add a new user `python button` knob
- use the following code inside :
```python
node = nuke.thisNode()
print(repr(node["kernelSource"].getValue()))
print()
print(repr(node["KernelDescription"].getValue()))
```
- execute the button and check the result in the Script Editor
- copy the first line (kernelSource) and paste into a new file named `WhiteBalance.blink.src`
- think to remove the first and trailling quote `'`
- do the same for the second line (KernelDescription) :
- new `WhiteBalance.blink.desc` file
- think to remove the first and trailling quote `'`
- run `build.py`
- check result which is `../WhiteBalance.nk` defined by `BuildPaths.build_gizmo` variable (in build.py)
You need to perform the manipulation again **everytime** the blink script
is modified.
47 changes: 47 additions & 0 deletions src/whitebalance/src/WhiteBalance-template.nk
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Group {
name WhiteBalance
tile_color 0xca7c4200
addUserKnob {20 User}
addUserKnob {26 txt_title l "" T "<h1>WhiteBalance</h1>"}
addUserKnob {7 temperature l "temperature" t "CCT expressed in Kelvin" R 1000 15000}
temperature 5495
addUserKnob {7 tint l "tint" t "Deviation from planckian locus. Negatives are pinkish, positives are greener." R -150 150}
tint -13
addUserKnob {7 intensity l "intensity" t "Intensity of the balancing using linear interpolation." R 0.0 5.0}
intensity 1.0
addUserKnob {6 show_coefficients l "show RGB coefficients" t "Fill the input with RGB coefficients instead of white-balancing." +STARTLINE}
addUserKnob {20 About}
addUserKnob {26 toolName l name T WhiteBalance}
addUserKnob {26 toolVersion l version T 0.1.0}
addUserKnob {26 toolAuthor l author T "<a style=\"color: rgb(200,200,200);\" href=\"https://mrlixm.github.io/\">Liam Collod</a>"}
addUserKnob {26 toolDescription l description T "Creative white-balancing with temperature and tint control."}
addUserKnob {26 toolUrl l url T "<a style=\"color: rgb(200,200,200);\" href=\"https://github.com/MrLixm/Foundry_Nuke\">https://github.com/MrLixm/Foundry_Nuke</a>"}
}
Input {
inputs 0
name Input1
xpos 0
}
BlinkScript {
inputs 1
recompileCount 2
ProgramGroup 1
KernelDescription "%BLINK_DESC%"
kernelSource "%BLINK_SRC%"
rebuild ""
WhiteBalance_u_temperature {{parent.temperature}}
WhiteBalance_u_tint {{parent.tint}}
WhiteBalance_u_intensity {{parent.intensity}}
WhiteBalance_u_show_coeffs {{parent.show_coefficients}}
format "2048 2048 0 0 2048 2048 1 square_2K"
rebuild_finalise ""
name WhiteBalanceBlink
xpos 0
ypos 150
}
Output {
name Output1
xpos 0
ypos 300
}
end_group
112 changes: 112 additions & 0 deletions src/whitebalance/src/WhiteBalance.blink
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// version 2
//
// References :
// - [2] Ohno, Yoshi (2014). Practical Use and Calculation of CCT and Duv. LEUKOS, 10(1), 47-55. doi:10.1080/15502724.2014.839020
// - [3] https://en.wikipedia.org/wiki/Planckian_locus#Approximation
// - [4] SMPTE Recommended Practice - Derivation of Basic Television Color Equations https://ieeexplore.ieee.org/document/7291155

#define ohno_deltaT float(0.01)


float powsafe(float color, float power){
// pow() but safe for NaNs/negatives
return pow(fabs(color), power) * sign(color);
}


float2 convert_CCT_to_uv_Krystek1985(float CCT){
// Convert the given CCT to CIE 1960 u,v colorspace values using Krystek's method.
//
// Krystek's method is an approximation and not intended for accuracy.
//
// :param CCT: in kelvin, ~[1000-15000] range
// --[3]
float CCT_2 = pow(CCT,2.0f);
float u = 0.860117757f + 1.54118254f * pow(10.0f,-4.0f) * CCT + 1.28641212f * pow(10.0f,-7.0f) * CCT_2;
u = u / (1.0f + 8.42420235f * pow(10.0f,-4.0f) * CCT + 7.08145163f * pow(10.0f,-7.0f) * CCT_2);
float v = 0.317398726f + 4.22806245f * pow(10.0f,-5.0f) * CCT + 4.20481691f * pow(10.0f,-8.0f) * CCT_2;
v = v / (1.0f - 2.89741816f * pow(10.0f,-5.0f) * CCT + 1.61456053f * pow(10.0f,-7.0f) * CCT_2);
return float2(u, v);
}


float2 convert_CCT_Duv_to_xy(float CCT, float Duv){
// :param CCT: correlated color temperature in kelvin, ~[1000-15000] range
// :param Duv: also called "tint" [-0.05-+0.05] range
// -- [2]
float2 uv0 = convert_CCT_to_uv_Krystek1985(CCT);
float2 uv1 = convert_CCT_to_uv_Krystek1985(CCT + ohno_deltaT);

float du = uv0.x - uv1.x;
float dv = uv0.y - uv1.y;

float hypothenus = sqrt(powsafe(du,2.0f) + powsafe(dv,2.0f));
float sinTheta = dv / hypothenus;
float cosTheta = du / hypothenus;

float u = uv0.x - Duv * sinTheta;
float v = uv0.y + Duv * cosTheta;

float u_p = u;
float v_p = 1.5f * v;

float x = 9.0f * u_p / (6.0f * u_p - 16.0f * v_p + 12.0f);
float y = 2.0f * v_p / (3.0f * u_p - 8.0f * v_p + 6.0f);
return float2(x, y);
}


kernel WhiteBalance : ImageComputationKernel<ePixelWise>
{
Image<eRead, eAccessPoint, eEdgeClamped> src;
Image<eWrite> dst;

param:
bool u_show_coeffs;
float u_temperature;
float u_tint;
float u_intensity;

void define(){
// default values try to match illuminant E
defineParam(u_temperature, "u_temperature", 5600.0f);
defineParam(u_tint, "u_tint", -15.5f);
defineParam(u_intensity, "u_intensity", 1.0f);
}

float lerp(float a1, float a2, float amount){
// linear interpolation between 2 values
return (1.0f - amount) * a1 + amount * a2;
}

void process() {

// 3000 is an arbitrary scale for the tint parameter to have a more UI friendly range.
// (actually same as Adobe)
float2 new_white_xy = convert_CCT_Duv_to_xy(u_temperature, u_tint/3000.0f);

// --[4] normalise primary matrix algorithm but only with whitepoint
float Wz = 1.0f - new_white_xy.x - new_white_xy.y;
float3 W = float3(new_white_xy.x / new_white_xy.y, 1.0f, Wz / new_white_xy.y);

float4 rgba = src();
float3 new_rgb(rgba.x, rgba.y, rgba.z);

if (u_show_coeffs){
new_rgb.x = W.x;
new_rgb.y = W.y;
new_rgb.z = W.z;
} else {
new_rgb.x = lerp(rgba.x, new_rgb.x * W.x, u_intensity);
new_rgb.y = lerp(rgba.y, new_rgb.y * W.y, u_intensity);
new_rgb.z = lerp(rgba.z, new_rgb.z * W.z, u_intensity);
}

dst() = float4(
new_rgb.x,
new_rgb.y,
new_rgb.z,
rgba.w
);
}
};
77 changes: 77 additions & 0 deletions src/whitebalance/src/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# python 3
import logging
import sys
from pathlib import Path

LOGGER = logging.getLogger(__name__)
THIS_DIR = Path(__file__).parent


class BuildPaths:
src_blink_script = THIS_DIR / "WhiteBalance.blink"
assert src_blink_script.exists()

src_gizmo = THIS_DIR / "WhiteBalance-template.nk"
assert src_gizmo.exists()

build_dir = THIS_DIR.parent
build_gizmo = build_dir / "WhiteBalance.nk"


def sanitize_nuke_script(script: str, convert_new_lines=True) -> str:
if convert_new_lines:
newscript = script.replace("\\", r"\\")
newscript = newscript.split("\n")
newscript = r"\n".join(newscript)
else:
newscript = script.split(r"\n")
newscript = [line.replace("\\", r"\\") for line in newscript]
newscript = r"\n".join(newscript)

newscript = newscript.replace('"', r"\"")
newscript = newscript.replace("{", r"\{")
newscript = newscript.replace("}", r"\}")
newscript = newscript.replace("[", r"\[")
return newscript


def build():
LOGGER.info(f"build started")
base_gizmo = BuildPaths.src_gizmo.read_text("utf-8")

blink_source = BuildPaths.src_blink_script.with_suffix(".blink.src")
assert blink_source.exists()
blink_source = blink_source.read_text()
blink_source = sanitize_nuke_script(blink_source, False)

blink_desc = BuildPaths.src_blink_script.with_suffix(".blink.desc")
assert blink_desc.exists()
blink_desc = blink_desc.read_text()
blink_desc = sanitize_nuke_script(blink_desc, False)

new_gizmo = []

for line_index, line in enumerate(base_gizmo.split("\n")):
if "%BLINK_SRC%" in line:
line = line.replace("%BLINK_SRC%", blink_source)
LOGGER.debug(f"replaced BLINK_SRC")
elif "%BLINK_DESC%" in line:
line = line.replace("%BLINK_DESC%", blink_desc)
LOGGER.debug(f"replaced BLINK_DESC")

new_gizmo.append(line)

new_gizmo = "\n".join(new_gizmo)
LOGGER.info(f"writting {BuildPaths.build_gizmo}")
BuildPaths.build_gizmo.write_text(new_gizmo, "utf-8")
LOGGER.info("build finished")


if __name__ == "__main__":
logging.basicConfig(
level=logging.DEBUG,
format="{levelname: <7} | {asctime} [{name}] {message}",
style="{",
stream=sys.stdout,
)
build()

0 comments on commit ecb07a8

Please sign in to comment.