Skip to content

Commit ecb07a8

Browse files
authored
Merge pull request #3 from MrLixm/feat-whitebalance
Feat: add whitebalance tool
2 parents aa2784d + a8a0fcd commit ecb07a8

File tree

9 files changed

+376
-1
lines changed

9 files changed

+376
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ _this table might not be always up-to-date_
1616
| [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) |
1717
| [ocio-saturation](src/ocio-saturation) | saturation with variable weights also based on OCIO implementation | nodes | ![grading](https://img.shields.io/badge/grading-43896b) |
1818
| [hsv](src/hsv) | color correction with HSV model | nodes | ![grading](https://img.shields.io/badge/grading-43896b) |
19+
| [whitebalance](src/whitebalance) | creative white balance with temperature/tint | nodes, blink | ![grading](https://img.shields.io/badge/grading-43896b) |
1920

2021
# Utilisation
2122

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "foundry-nuke"
3-
version = "0.4.0"
3+
version = "0.5.0"
44
description = "Collection of script & resources for Foundry's Nuke software."
55
authors = ["Liam Collod <monsieurlixm@gmail.com>"]
66
readme = "README.md"

src/whitebalance/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# White Balance
2+
3+
Change the "white balance" of your source imagery.
4+
5+
This is not a technical transform and was more designed as creative transformation tool.
6+
Usually whitebalancing is applied on undemosaiced raw camera data while
7+
this node will operate on already debayered data.
8+
9+
![demo screenshot of the node in nuke](cover.png)
10+
11+
# Instructions
12+
13+
## Install
14+
15+
- Copy/paste the content of [WhiteBalance.nk](WhiteBalance.nk) in any nuke
16+
scene.
17+
- That's it
18+
19+
## Requirements
20+
21+
The tool use the following features :
22+
23+
- blink script but works on non-commercial versions >= 14.0
24+
25+
## Reference
26+
27+
Default value for temperature and tint try to match the value of the illuminant E
28+
which should produce a "no-operation" result.
29+
30+
### temperature
31+
32+
In Kelvin, lower produce a warmer look, higher a colder one.
33+
34+
### tint
35+
36+
Deviation from the planckian locus, referred usually as "D*uv*". Scaled x3000
37+
for conveniency. Negatives values produce a pink shift while positive value a green tint.
38+
39+
### intensity
40+
41+
Strength of the effect applied as a simple linear interpolation. 0.0 means no
42+
effect.
43+
44+
### show RGB coefficients
45+
46+
Fill the image with the R-G-B coefficient used to whitebalance the image. Useful
47+
for debugging or alternative workflow:
48+
- provide a 1x1 pixels input in the node
49+
- check `show RGB coefficients`
50+
- reformat output to size of your image
51+
- merge previous step with the actual image using a `multiply` blend mode
52+
53+
54+
# Developer
55+
56+
See the [./src/](./src) folder for development instructions.

src/whitebalance/WhiteBalance.nk

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Group {
2+
name WhiteBalance
3+
tile_color 0xca7c4200
4+
addUserKnob {20 User}
5+
addUserKnob {26 txt_title l "" T "<h1>WhiteBalance</h1>"}
6+
addUserKnob {7 temperature l "temperature" t "CCT expressed in Kelvin" R 1000 15000}
7+
temperature 5495
8+
addUserKnob {7 tint l "tint" t "Deviation from planckian locus. Negatives are pinkish, positives are greener." R -150 150}
9+
tint -13
10+
addUserKnob {7 intensity l "intensity" t "Intensity of the balancing using linear interpolation." R 0.0 5.0}
11+
intensity 1.0
12+
addUserKnob {6 show_coefficients l "show RGB coefficients" t "Fill the input with RGB coefficients instead of white-balancing." +STARTLINE}
13+
addUserKnob {20 About}
14+
addUserKnob {26 toolName l name T WhiteBalance}
15+
addUserKnob {26 toolVersion l version T 0.1.0}
16+
addUserKnob {26 toolAuthor l author T "<a style=\"color: rgb(200,200,200);\" href=\"https://mrlixm.github.io/\">Liam Collod</a>"}
17+
addUserKnob {26 toolDescription l description T "Creative white-balancing with temperature and tint control."}
18+
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>"}
19+
}
20+
Input {
21+
inputs 0
22+
name Input1
23+
xpos 0
24+
}
25+
BlinkScript {
26+
inputs 1
27+
recompileCount 2
28+
ProgramGroup 1
29+
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"
30+
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\};"
31+
rebuild ""
32+
WhiteBalance_u_temperature {{parent.temperature}}
33+
WhiteBalance_u_tint {{parent.tint}}
34+
WhiteBalance_u_intensity {{parent.intensity}}
35+
WhiteBalance_u_show_coeffs {{parent.show_coefficients}}
36+
format "2048 2048 0 0 2048 2048 1 square_2K"
37+
rebuild_finalise ""
38+
name WhiteBalanceBlink
39+
xpos 0
40+
ypos 150
41+
}
42+
Output {
43+
name Output1
44+
xpos 0
45+
ypos 300
46+
}
47+
end_group

src/whitebalance/cover.png

570 KB
Loading

src/whitebalance/src/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# src
2+
3+
code here need to be "compiled" to be usable. This is achieved by executing
4+
the `build.py` file.
5+
6+
# build instructions
7+
8+
## build-requires
9+
10+
- python-3
11+
- any nuke version (including non-commercial)
12+
13+
## build-usage
14+
15+
- take the blink script at root and import them into a nuke scene
16+
- make sure to compile the blink script
17+
- add a new user `python button` knob
18+
- use the following code inside :
19+
```python
20+
node = nuke.thisNode()
21+
print(repr(node["kernelSource"].getValue()))
22+
print()
23+
print(repr(node["KernelDescription"].getValue()))
24+
```
25+
- execute the button and check the result in the Script Editor
26+
- copy the first line (kernelSource) and paste into a new file named `WhiteBalance.blink.src`
27+
- think to remove the first and trailling quote `'`
28+
- do the same for the second line (KernelDescription) :
29+
- new `WhiteBalance.blink.desc` file
30+
- think to remove the first and trailling quote `'`
31+
- run `build.py`
32+
- check result which is `../WhiteBalance.nk` defined by `BuildPaths.build_gizmo` variable (in build.py)
33+
34+
You need to perform the manipulation again **everytime** the blink script
35+
is modified.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Group {
2+
name WhiteBalance
3+
tile_color 0xca7c4200
4+
addUserKnob {20 User}
5+
addUserKnob {26 txt_title l "" T "<h1>WhiteBalance</h1>"}
6+
addUserKnob {7 temperature l "temperature" t "CCT expressed in Kelvin" R 1000 15000}
7+
temperature 5495
8+
addUserKnob {7 tint l "tint" t "Deviation from planckian locus. Negatives are pinkish, positives are greener." R -150 150}
9+
tint -13
10+
addUserKnob {7 intensity l "intensity" t "Intensity of the balancing using linear interpolation." R 0.0 5.0}
11+
intensity 1.0
12+
addUserKnob {6 show_coefficients l "show RGB coefficients" t "Fill the input with RGB coefficients instead of white-balancing." +STARTLINE}
13+
addUserKnob {20 About}
14+
addUserKnob {26 toolName l name T WhiteBalance}
15+
addUserKnob {26 toolVersion l version T 0.1.0}
16+
addUserKnob {26 toolAuthor l author T "<a style=\"color: rgb(200,200,200);\" href=\"https://mrlixm.github.io/\">Liam Collod</a>"}
17+
addUserKnob {26 toolDescription l description T "Creative white-balancing with temperature and tint control."}
18+
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>"}
19+
}
20+
Input {
21+
inputs 0
22+
name Input1
23+
xpos 0
24+
}
25+
BlinkScript {
26+
inputs 1
27+
recompileCount 2
28+
ProgramGroup 1
29+
KernelDescription "%BLINK_DESC%"
30+
kernelSource "%BLINK_SRC%"
31+
rebuild ""
32+
WhiteBalance_u_temperature {{parent.temperature}}
33+
WhiteBalance_u_tint {{parent.tint}}
34+
WhiteBalance_u_intensity {{parent.intensity}}
35+
WhiteBalance_u_show_coeffs {{parent.show_coefficients}}
36+
format "2048 2048 0 0 2048 2048 1 square_2K"
37+
rebuild_finalise ""
38+
name WhiteBalanceBlink
39+
xpos 0
40+
ypos 150
41+
}
42+
Output {
43+
name Output1
44+
xpos 0
45+
ypos 300
46+
}
47+
end_group
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// version 2
2+
//
3+
// References :
4+
// - [2] Ohno, Yoshi (2014). Practical Use and Calculation of CCT and Duv. LEUKOS, 10(1), 47-55. doi:10.1080/15502724.2014.839020
5+
// - [3] https://en.wikipedia.org/wiki/Planckian_locus#Approximation
6+
// - [4] SMPTE Recommended Practice - Derivation of Basic Television Color Equations https://ieeexplore.ieee.org/document/7291155
7+
8+
#define ohno_deltaT float(0.01)
9+
10+
11+
float powsafe(float color, float power){
12+
// pow() but safe for NaNs/negatives
13+
return pow(fabs(color), power) * sign(color);
14+
}
15+
16+
17+
float2 convert_CCT_to_uv_Krystek1985(float CCT){
18+
// Convert the given CCT to CIE 1960 u,v colorspace values using Krystek's method.
19+
//
20+
// Krystek's method is an approximation and not intended for accuracy.
21+
//
22+
// :param CCT: in kelvin, ~[1000-15000] range
23+
// --[3]
24+
float CCT_2 = pow(CCT,2.0f);
25+
float u = 0.860117757f + 1.54118254f * pow(10.0f,-4.0f) * CCT + 1.28641212f * pow(10.0f,-7.0f) * CCT_2;
26+
u = u / (1.0f + 8.42420235f * pow(10.0f,-4.0f) * CCT + 7.08145163f * pow(10.0f,-7.0f) * CCT_2);
27+
float v = 0.317398726f + 4.22806245f * pow(10.0f,-5.0f) * CCT + 4.20481691f * pow(10.0f,-8.0f) * CCT_2;
28+
v = v / (1.0f - 2.89741816f * pow(10.0f,-5.0f) * CCT + 1.61456053f * pow(10.0f,-7.0f) * CCT_2);
29+
return float2(u, v);
30+
}
31+
32+
33+
float2 convert_CCT_Duv_to_xy(float CCT, float Duv){
34+
// :param CCT: correlated color temperature in kelvin, ~[1000-15000] range
35+
// :param Duv: also called "tint" [-0.05-+0.05] range
36+
// -- [2]
37+
float2 uv0 = convert_CCT_to_uv_Krystek1985(CCT);
38+
float2 uv1 = convert_CCT_to_uv_Krystek1985(CCT + ohno_deltaT);
39+
40+
float du = uv0.x - uv1.x;
41+
float dv = uv0.y - uv1.y;
42+
43+
float hypothenus = sqrt(powsafe(du,2.0f) + powsafe(dv,2.0f));
44+
float sinTheta = dv / hypothenus;
45+
float cosTheta = du / hypothenus;
46+
47+
float u = uv0.x - Duv * sinTheta;
48+
float v = uv0.y + Duv * cosTheta;
49+
50+
float u_p = u;
51+
float v_p = 1.5f * v;
52+
53+
float x = 9.0f * u_p / (6.0f * u_p - 16.0f * v_p + 12.0f);
54+
float y = 2.0f * v_p / (3.0f * u_p - 8.0f * v_p + 6.0f);
55+
return float2(x, y);
56+
}
57+
58+
59+
kernel WhiteBalance : ImageComputationKernel<ePixelWise>
60+
{
61+
Image<eRead, eAccessPoint, eEdgeClamped> src;
62+
Image<eWrite> dst;
63+
64+
param:
65+
bool u_show_coeffs;
66+
float u_temperature;
67+
float u_tint;
68+
float u_intensity;
69+
70+
void define(){
71+
// default values try to match illuminant E
72+
defineParam(u_temperature, "u_temperature", 5600.0f);
73+
defineParam(u_tint, "u_tint", -15.5f);
74+
defineParam(u_intensity, "u_intensity", 1.0f);
75+
}
76+
77+
float lerp(float a1, float a2, float amount){
78+
// linear interpolation between 2 values
79+
return (1.0f - amount) * a1 + amount * a2;
80+
}
81+
82+
void process() {
83+
84+
// 3000 is an arbitrary scale for the tint parameter to have a more UI friendly range.
85+
// (actually same as Adobe)
86+
float2 new_white_xy = convert_CCT_Duv_to_xy(u_temperature, u_tint/3000.0f);
87+
88+
// --[4] normalise primary matrix algorithm but only with whitepoint
89+
float Wz = 1.0f - new_white_xy.x - new_white_xy.y;
90+
float3 W = float3(new_white_xy.x / new_white_xy.y, 1.0f, Wz / new_white_xy.y);
91+
92+
float4 rgba = src();
93+
float3 new_rgb(rgba.x, rgba.y, rgba.z);
94+
95+
if (u_show_coeffs){
96+
new_rgb.x = W.x;
97+
new_rgb.y = W.y;
98+
new_rgb.z = W.z;
99+
} else {
100+
new_rgb.x = lerp(rgba.x, new_rgb.x * W.x, u_intensity);
101+
new_rgb.y = lerp(rgba.y, new_rgb.y * W.y, u_intensity);
102+
new_rgb.z = lerp(rgba.z, new_rgb.z * W.z, u_intensity);
103+
}
104+
105+
dst() = float4(
106+
new_rgb.x,
107+
new_rgb.y,
108+
new_rgb.z,
109+
rgba.w
110+
);
111+
}
112+
};

src/whitebalance/src/build.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# python 3
2+
import logging
3+
import sys
4+
from pathlib import Path
5+
6+
LOGGER = logging.getLogger(__name__)
7+
THIS_DIR = Path(__file__).parent
8+
9+
10+
class BuildPaths:
11+
src_blink_script = THIS_DIR / "WhiteBalance.blink"
12+
assert src_blink_script.exists()
13+
14+
src_gizmo = THIS_DIR / "WhiteBalance-template.nk"
15+
assert src_gizmo.exists()
16+
17+
build_dir = THIS_DIR.parent
18+
build_gizmo = build_dir / "WhiteBalance.nk"
19+
20+
21+
def sanitize_nuke_script(script: str, convert_new_lines=True) -> str:
22+
if convert_new_lines:
23+
newscript = script.replace("\\", r"\\")
24+
newscript = newscript.split("\n")
25+
newscript = r"\n".join(newscript)
26+
else:
27+
newscript = script.split(r"\n")
28+
newscript = [line.replace("\\", r"\\") for line in newscript]
29+
newscript = r"\n".join(newscript)
30+
31+
newscript = newscript.replace('"', r"\"")
32+
newscript = newscript.replace("{", r"\{")
33+
newscript = newscript.replace("}", r"\}")
34+
newscript = newscript.replace("[", r"\[")
35+
return newscript
36+
37+
38+
def build():
39+
LOGGER.info(f"build started")
40+
base_gizmo = BuildPaths.src_gizmo.read_text("utf-8")
41+
42+
blink_source = BuildPaths.src_blink_script.with_suffix(".blink.src")
43+
assert blink_source.exists()
44+
blink_source = blink_source.read_text()
45+
blink_source = sanitize_nuke_script(blink_source, False)
46+
47+
blink_desc = BuildPaths.src_blink_script.with_suffix(".blink.desc")
48+
assert blink_desc.exists()
49+
blink_desc = blink_desc.read_text()
50+
blink_desc = sanitize_nuke_script(blink_desc, False)
51+
52+
new_gizmo = []
53+
54+
for line_index, line in enumerate(base_gizmo.split("\n")):
55+
if "%BLINK_SRC%" in line:
56+
line = line.replace("%BLINK_SRC%", blink_source)
57+
LOGGER.debug(f"replaced BLINK_SRC")
58+
elif "%BLINK_DESC%" in line:
59+
line = line.replace("%BLINK_DESC%", blink_desc)
60+
LOGGER.debug(f"replaced BLINK_DESC")
61+
62+
new_gizmo.append(line)
63+
64+
new_gizmo = "\n".join(new_gizmo)
65+
LOGGER.info(f"writting {BuildPaths.build_gizmo}")
66+
BuildPaths.build_gizmo.write_text(new_gizmo, "utf-8")
67+
LOGGER.info("build finished")
68+
69+
70+
if __name__ == "__main__":
71+
logging.basicConfig(
72+
level=logging.DEBUG,
73+
format="{levelname: <7} | {asctime} [{name}] {message}",
74+
style="{",
75+
stream=sys.stdout,
76+
)
77+
build()

0 commit comments

Comments
 (0)