Spaces:
Sleeping
Sleeping
add color wheel image choice
Browse files- app.py +51 -10
- color_conversions.py +100 -0
- image_sample.jpg +0 -0
- image_sample.png +0 -0
- synthetic_charts.py +40 -0
app.py
CHANGED
|
@@ -2,6 +2,8 @@ from interactive_pipe import interactive_pipeline, interactive
|
|
| 2 |
from interactive_pipe.data_objects.curves import SingleCurve, Curve
|
| 3 |
from interactive_pipe.data_objects.image import Image
|
| 4 |
from global_tone_mapping import apply_s_curve_tone_mapping
|
|
|
|
|
|
|
| 5 |
import numpy as np
|
| 6 |
|
| 7 |
|
|
@@ -9,10 +11,18 @@ def histogram(img: np.ndarray) -> Curve:
|
|
| 9 |
hist_curves = []
|
| 10 |
for ch in range(img.shape[-1]):
|
| 11 |
hist, bins = np.histogram(
|
| 12 |
-
img[..., ch].flatten(),
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
return hist_curve
|
| 17 |
|
| 18 |
|
|
@@ -28,16 +38,26 @@ def set_tone_mapping_params(
|
|
| 28 |
global_params["exposure"] = exposure
|
| 29 |
|
| 30 |
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
exposure = global_params.get("exposure", 0.)
|
| 33 |
shadow_boost = global_params.get("shadow_boost", 0.)
|
| 34 |
highlight_boost = global_params.get("highlight_boost", 0.)
|
| 35 |
-
|
| 36 |
-
img,
|
|
|
|
|
|
|
|
|
|
| 37 |
shadow_boost,
|
| 38 |
highlight_boost,
|
| 39 |
exposure
|
| 40 |
)
|
|
|
|
|
|
|
| 41 |
|
| 42 |
|
| 43 |
def s_curve_visualization(global_params={}) -> Curve:
|
|
@@ -57,8 +77,29 @@ def s_curve_visualization(global_params={}) -> Curve:
|
|
| 57 |
)
|
| 58 |
|
| 59 |
|
| 60 |
-
@
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
set_tone_mapping_params()
|
| 63 |
tc_image = s_curve_tone_mapping(input_image)
|
| 64 |
tone_curve = s_curve_visualization()
|
|
@@ -67,5 +108,5 @@ def image_editing_pipeline(input_image):
|
|
| 67 |
|
| 68 |
|
| 69 |
if __name__ == "__main__":
|
| 70 |
-
img = Image.load_image("image_sample.
|
| 71 |
image_editing_pipeline(img)
|
|
|
|
| 2 |
from interactive_pipe.data_objects.curves import SingleCurve, Curve
|
| 3 |
from interactive_pipe.data_objects.image import Image
|
| 4 |
from global_tone_mapping import apply_s_curve_tone_mapping
|
| 5 |
+
from color_conversions import rgb_to_hsv, hsv_to_rgb
|
| 6 |
+
from synthetic_charts import generate_color_wheel
|
| 7 |
import numpy as np
|
| 8 |
|
| 9 |
|
|
|
|
| 11 |
hist_curves = []
|
| 12 |
for ch in range(img.shape[-1]):
|
| 13 |
hist, bins = np.histogram(
|
| 14 |
+
img[..., ch].flatten(),
|
| 15 |
+
bins=128,
|
| 16 |
+
range=(0, 1)
|
| 17 |
+
)
|
| 18 |
+
hist_curves.append(
|
| 19 |
+
SingleCurve(bins[:-1], hist, style='rgb'[ch]+"-")
|
| 20 |
+
)
|
| 21 |
+
hist_curve = Curve(
|
| 22 |
+
hist_curves,
|
| 23 |
+
xlabel="Intensity",
|
| 24 |
+
ylabel="Frequency"
|
| 25 |
+
)
|
| 26 |
return hist_curve
|
| 27 |
|
| 28 |
|
|
|
|
| 38 |
global_params["exposure"] = exposure
|
| 39 |
|
| 40 |
|
| 41 |
+
@interactive(apply_tone_curve_on_luma=(True,))
|
| 42 |
+
def s_curve_tone_mapping(
|
| 43 |
+
img: np.ndarray,
|
| 44 |
+
apply_tone_curve_on_luma: bool = True,
|
| 45 |
+
global_params={}
|
| 46 |
+
) -> np.ndarray:
|
| 47 |
exposure = global_params.get("exposure", 0.)
|
| 48 |
shadow_boost = global_params.get("shadow_boost", 0.)
|
| 49 |
highlight_boost = global_params.get("highlight_boost", 0.)
|
| 50 |
+
if not apply_tone_curve_on_luma:
|
| 51 |
+
return apply_s_curve_tone_mapping(img, shadow_boost, highlight_boost, exposure)
|
| 52 |
+
hsv = rgb_to_hsv(img)
|
| 53 |
+
luma_tone_mapped = apply_s_curve_tone_mapping(
|
| 54 |
+
hsv[..., -1],
|
| 55 |
shadow_boost,
|
| 56 |
highlight_boost,
|
| 57 |
exposure
|
| 58 |
)
|
| 59 |
+
hsv[..., -1] = luma_tone_mapped
|
| 60 |
+
return hsv_to_rgb(hsv)
|
| 61 |
|
| 62 |
|
| 63 |
def s_curve_visualization(global_params={}) -> Curve:
|
|
|
|
| 77 |
)
|
| 78 |
|
| 79 |
|
| 80 |
+
@interactive(
|
| 81 |
+
input_image=("sample_image", ["sample_image", "color_wheel"]),
|
| 82 |
+
)
|
| 83 |
+
def pick_image(
|
| 84 |
+
default_image: np.ndarray,
|
| 85 |
+
input_image: str = "sample_image",
|
| 86 |
+
global_params={}
|
| 87 |
+
) -> np.ndarray:
|
| 88 |
+
if input_image == "sample_image":
|
| 89 |
+
return default_image
|
| 90 |
+
elif input_image == "color_wheel":
|
| 91 |
+
color_wheel = global_params.get("color_wheel", None)
|
| 92 |
+
if color_wheel is None:
|
| 93 |
+
color_wheel = generate_color_wheel(resolution=496)
|
| 94 |
+
global_params["color_wheel"] = color_wheel
|
| 95 |
+
return color_wheel
|
| 96 |
+
else:
|
| 97 |
+
return default_image
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@interactive_pipeline(gui="gradio")
|
| 101 |
+
def image_editing_pipeline(sample_image):
|
| 102 |
+
input_image = pick_image(sample_image)
|
| 103 |
set_tone_mapping_params()
|
| 104 |
tc_image = s_curve_tone_mapping(input_image)
|
| 105 |
tone_curve = s_curve_visualization()
|
|
|
|
| 108 |
|
| 109 |
|
| 110 |
if __name__ == "__main__":
|
| 111 |
+
img = Image.load_image("image_sample.jpg")
|
| 112 |
image_editing_pipeline(img)
|
color_conversions.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def rgb_to_hsv(rgb):
|
| 5 |
+
# Ensure the input is a float numpy array and in the range [0, 1]
|
| 6 |
+
rgb = np.asarray(rgb)
|
| 7 |
+
|
| 8 |
+
# Separate the R, G, B channels
|
| 9 |
+
r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
|
| 10 |
+
|
| 11 |
+
# Max and min of RGB values
|
| 12 |
+
max_rgb = np.max(rgb, axis=-1)
|
| 13 |
+
min_rgb = np.min(rgb, axis=-1)
|
| 14 |
+
delta = max_rgb - min_rgb
|
| 15 |
+
|
| 16 |
+
# Hue calculation
|
| 17 |
+
hue = np.zeros_like(max_rgb)
|
| 18 |
+
mask = delta != 0
|
| 19 |
+
|
| 20 |
+
# Red is max
|
| 21 |
+
idx = (max_rgb == r) & mask
|
| 22 |
+
hue[idx] = (60 * ((g - b) / delta % 6))[idx]
|
| 23 |
+
|
| 24 |
+
# Green is max
|
| 25 |
+
idx = (max_rgb == g) & mask
|
| 26 |
+
hue[idx] = (60 * ((b - r) / delta + 2))[idx]
|
| 27 |
+
|
| 28 |
+
# Blue is max
|
| 29 |
+
idx = (max_rgb == b) & mask
|
| 30 |
+
hue[idx] = (60 * ((r - g) / delta + 4))[idx]
|
| 31 |
+
|
| 32 |
+
# Saturation calculation
|
| 33 |
+
saturation = np.zeros_like(max_rgb)
|
| 34 |
+
saturation[mask] = delta[mask] / max_rgb[mask]
|
| 35 |
+
|
| 36 |
+
# Value calculation
|
| 37 |
+
value = max_rgb
|
| 38 |
+
|
| 39 |
+
# Stack the HSV components together
|
| 40 |
+
hsv = np.stack((hue, saturation, value), axis=-1)
|
| 41 |
+
|
| 42 |
+
return hsv
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def hsv_to_rgb(hsv):
|
| 46 |
+
# Ensure the input is a float numpy array and in the range [0, 1]
|
| 47 |
+
hsv = np.asarray(hsv)
|
| 48 |
+
|
| 49 |
+
# Separate the H, S, V channels
|
| 50 |
+
h, s, v = hsv[..., 0], hsv[..., 1], hsv[..., 2]
|
| 51 |
+
|
| 52 |
+
# Chromaticity component
|
| 53 |
+
c = v * s
|
| 54 |
+
x = c * (1 - np.abs((h / 60) % 2 - 1))
|
| 55 |
+
m = v - c
|
| 56 |
+
|
| 57 |
+
# Initialize RGB
|
| 58 |
+
r, g, b = np.zeros_like(h), np.zeros_like(h), np.zeros_like(h)
|
| 59 |
+
|
| 60 |
+
# Compute the RGB components based on the hue range
|
| 61 |
+
h0_60 = (h >= 0) & (h < 60)
|
| 62 |
+
r[h0_60], g[h0_60], b[h0_60] = c[h0_60], x[h0_60], 0
|
| 63 |
+
|
| 64 |
+
h60_120 = (h >= 60) & (h < 120)
|
| 65 |
+
r[h60_120], g[h60_120], b[h60_120] = x[h60_120], c[h60_120], 0
|
| 66 |
+
|
| 67 |
+
h120_180 = (h >= 120) & (h < 180)
|
| 68 |
+
r[h120_180], g[h120_180], b[h120_180] = 0, c[h120_180], x[h120_180]
|
| 69 |
+
|
| 70 |
+
h180_240 = (h >= 180) & (h < 240)
|
| 71 |
+
r[h180_240], g[h180_240], b[h180_240] = 0, x[h180_240], c[h180_240]
|
| 72 |
+
|
| 73 |
+
h240_300 = (h >= 240) & (h < 300)
|
| 74 |
+
r[h240_300], g[h240_300], b[h240_300] = x[h240_300], 0, c[h240_300]
|
| 75 |
+
|
| 76 |
+
h300_360 = (h >= 300) & (h < 360)
|
| 77 |
+
r[h300_360], g[h300_360], b[h300_360] = c[h300_360], 0, x[h300_360]
|
| 78 |
+
|
| 79 |
+
# Add m to each component and stack them together
|
| 80 |
+
rgb = np.stack((r + m, g + m, b + m), axis=-1)
|
| 81 |
+
|
| 82 |
+
return rgb
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def test_conversion():
|
| 86 |
+
# RGB array example (range [0, 1])
|
| 87 |
+
rgb = np.array([[[0.5, 0.4, 0.7], [0.1, 0.2, 0.3]],
|
| 88 |
+
[[0.8, 0.7, 0.6], [0.9, 0.1, 0.2]]])
|
| 89 |
+
# rgb = np.random.rand(1000, 100, 3)
|
| 90 |
+
# Convert RGB to HSV
|
| 91 |
+
hsv = rgb_to_hsv(rgb)
|
| 92 |
+
print(hsv.shape)
|
| 93 |
+
|
| 94 |
+
# Convert HSV back to RGB
|
| 95 |
+
rgb_back = hsv_to_rgb(hsv)
|
| 96 |
+
assert np.allclose(rgb, rgb_back), "Conversion failed!"
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
if __name__ == "__main__":
|
| 100 |
+
test_conversion()
|
image_sample.jpg
ADDED
|
image_sample.png
DELETED
|
Binary file (899 kB)
|
|
|
synthetic_charts.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import numpy as np
|
| 3 |
+
from color_conversions import hsv_to_rgb
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def generate_color_wheel(resolution=500) -> np.ndarray:
|
| 7 |
+
"""
|
| 8 |
+
Generate a color wheel image using HSV to RGB conversion.
|
| 9 |
+
|
| 10 |
+
:param resolution: The width/height of the square image
|
| 11 |
+
:return: RGB image of the color wheel
|
| 12 |
+
"""
|
| 13 |
+
# Create a 2D grid of indices (X, Y) to calculate the angle and radius
|
| 14 |
+
x = np.linspace(-1, 1, resolution)
|
| 15 |
+
y = np.linspace(-1, 1, resolution)
|
| 16 |
+
xv, yv = np.meshgrid(x, y)
|
| 17 |
+
|
| 18 |
+
# Calculate angle (hue) in degrees
|
| 19 |
+
angle = np.arctan2(yv, xv) * (180 / np.pi) + 180 # Range from 0 to 360
|
| 20 |
+
|
| 21 |
+
# Calculate radius from the center (used for saturation)
|
| 22 |
+
radius = np.sqrt(xv**2 + yv**2)
|
| 23 |
+
|
| 24 |
+
# Normalize radius to range [0, 1]
|
| 25 |
+
radius = np.clip(radius, 0, 1)
|
| 26 |
+
|
| 27 |
+
# HSV components
|
| 28 |
+
hue = angle # Hue is the angle
|
| 29 |
+
saturation = radius
|
| 30 |
+
# Saturation is proportional to the distance from the center
|
| 31 |
+
# Value is constant (1) for maximum brightness
|
| 32 |
+
value = np.ones_like(radius)
|
| 33 |
+
|
| 34 |
+
# Stack the HSV components into a single array
|
| 35 |
+
hsv_image = np.stack((hue, saturation, value), axis=-1)
|
| 36 |
+
|
| 37 |
+
# Convert the HSV image to RGB
|
| 38 |
+
rgb_image = hsv_to_rgb(hsv_image)
|
| 39 |
+
|
| 40 |
+
return rgb_image
|