Spaces:
Running
Running
copied local files. docker initial setup
Browse files- .DS_Store +0 -0
- Dockerfile +11 -0
- compColors.py +120 -0
- dominantColors.py +89 -0
- recolorLinearColorTransfer.py +79 -0
- recolorLumaConverterAlgo.py +63 -0
- recolorOTAlgo.py +97 -0
- recolorPaletteBasedTransfer.py +115 -0
- recolorReinhardAlgo.py +89 -0
- recolorReinhardV2Algo.py +48 -0
- recolorTransferAlgo.py +70 -0
- requirements.txt +47 -0
- server.py +113 -0
- todo.md +15 -0
.DS_Store
ADDED
Binary file (6.15 kB). View file
|
|
Dockerfile
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.9
|
2 |
+
|
3 |
+
WORKDIR /code
|
4 |
+
|
5 |
+
COPY ./requirements.txt /code/requirements.txt
|
6 |
+
|
7 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
8 |
+
|
9 |
+
COPY . .
|
10 |
+
|
11 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
compColors.py
ADDED
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#This file generates Complementary Colours palletes using three different alghoritms:
|
2 |
+
#Adjacent (https://itsphbytes.wordpress.com/2016/09/25/getting-adjacent-colors-python-code/),
|
3 |
+
#Complementary (https://www.101computing.net/complementary-colours-algorithm/) and Gradient.
|
4 |
+
#The Gradient alghoritm is based on the code from https://stackoverflow.com/questions/22607043/python-generate-color-palette-from-image
|
5 |
+
|
6 |
+
import colorsys
|
7 |
+
|
8 |
+
def gradient_colors(color1, color2):
|
9 |
+
"""
|
10 |
+
It takes two colors and returns a list of 5 colors that are
|
11 |
+
complementary to the two colors. Basically is a gradient between
|
12 |
+
the two colors.
|
13 |
+
"""
|
14 |
+
if color1.startswith('#'):
|
15 |
+
color1 = color1[1:]
|
16 |
+
if color2.startswith('#'):
|
17 |
+
color2 = color2[1:]
|
18 |
+
# Convert hex colors to RGB
|
19 |
+
color1_rgb = tuple(int(color1[i:i+2], 16) for i in (0, 2, 4))
|
20 |
+
color2_rgb = tuple(int(color2[i:i+2], 16) for i in (0, 2, 4))
|
21 |
+
|
22 |
+
# Calculate the difference between the two colors
|
23 |
+
diff = [c2 - c1 for c1, c2 in zip(color1_rgb, color2_rgb)]
|
24 |
+
|
25 |
+
# Generate the palette
|
26 |
+
palette = []
|
27 |
+
for i in range(5):
|
28 |
+
r = color1_rgb[0] + diff[0] * i / 4
|
29 |
+
g = color1_rgb[1] + diff[1] * i / 4
|
30 |
+
b = color1_rgb[2] + diff[2] * i / 4
|
31 |
+
palette.append('#%02x%02x%02x' % (int(r), int(g), int(b)))
|
32 |
+
|
33 |
+
return palette
|
34 |
+
|
35 |
+
|
36 |
+
def complementary_colors(hexcolor):
|
37 |
+
# Check if the input starts with '#'. If it does, remove it.
|
38 |
+
if hexcolor.startswith('#'):
|
39 |
+
hexcolor = hexcolor[1:]
|
40 |
+
|
41 |
+
# Convert hex to RGB
|
42 |
+
r = int(hexcolor[0:2], 16)
|
43 |
+
g = int(hexcolor[2:4], 16)
|
44 |
+
b = int(hexcolor[4:6], 16)
|
45 |
+
|
46 |
+
# Calculate complementary colors
|
47 |
+
comp_r = 255 - r
|
48 |
+
comp_g = 255 - g
|
49 |
+
comp_b = 255 - b
|
50 |
+
|
51 |
+
# Convert RGB back to hex
|
52 |
+
comp_hex = "#{:02X}{:02X}{:02X}".format(comp_r, comp_g, comp_b)
|
53 |
+
|
54 |
+
return comp_hex
|
55 |
+
|
56 |
+
|
57 |
+
# Module to convert rgb to hex
|
58 |
+
def rgb_to_hex(rgb):
|
59 |
+
return '#%02X%02X%02X' % (rgb)
|
60 |
+
|
61 |
+
# Module to convert hex to rgb
|
62 |
+
def hex_to_rgb(value):
|
63 |
+
value = value.lstrip('#')
|
64 |
+
return tuple(int(value[i:i+2], 16) for i in (0, 2 ,4))
|
65 |
+
|
66 |
+
# This module gets the adjacent colors.
|
67 |
+
# They can be obtained by rotating the color by 30 and 360
|
68 |
+
# degrees.
|
69 |
+
def adjacent_colors(color):
|
70 |
+
# List to hold two colors
|
71 |
+
adjacent_colors = []
|
72 |
+
|
73 |
+
# Get the RGB
|
74 |
+
(r, g, b) = hex_to_rgb(color)
|
75 |
+
|
76 |
+
# Set the degree
|
77 |
+
d = 30/360
|
78 |
+
|
79 |
+
# Values has to be between 0 to 1
|
80 |
+
r = r / 255.0
|
81 |
+
g = g / 255.0
|
82 |
+
b = b / 255.0
|
83 |
+
|
84 |
+
# Convert to HLS using colorsys
|
85 |
+
h, l, s = colorsys.rgb_to_hls(r, g, b)
|
86 |
+
|
87 |
+
# Rotate the color
|
88 |
+
h = [(h+d) % 1 for d in (-d, d)]
|
89 |
+
|
90 |
+
# For both the colors, convert back to RGB and
|
91 |
+
# load to list
|
92 |
+
for hi in h:
|
93 |
+
r, g, b = colorsys.hls_to_rgb(hi, l, s)
|
94 |
+
r = int(r * 255.0)
|
95 |
+
g = int(g * 255.0)
|
96 |
+
b = int(b * 255.0)
|
97 |
+
hex_val = rgb_to_hex((r,g,b))
|
98 |
+
|
99 |
+
adjacent_colors.append(hex_val)
|
100 |
+
print(adjacent_colors)
|
101 |
+
return adjacent_colors
|
102 |
+
|
103 |
+
|
104 |
+
|
105 |
+
|
106 |
+
# # Test complementarty colors:
|
107 |
+
# color = "#771958"
|
108 |
+
# complementary = complementary_colors(color)
|
109 |
+
# print(f"Original color: {color}")
|
110 |
+
# print(f"Complementary color: {complementary}")
|
111 |
+
|
112 |
+
# # Test adjacent colors:
|
113 |
+
# print(f"Adjacent colors for {color}:")
|
114 |
+
# colors = adjacent_colors(color)
|
115 |
+
|
116 |
+
# # Test gradient colors:
|
117 |
+
# print(f"Gradient colors between {colors[0]} and {colors[1]}:")
|
118 |
+
# gradient = gradient_colors(colors[0], colors[1])
|
119 |
+
# for color in gradient:
|
120 |
+
# print(color)
|
dominantColors.py
ADDED
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#This file finds X dominant colors of a given image, using k-means and different channels of the image,
|
2 |
+
#like RGB, and luminance.
|
3 |
+
|
4 |
+
import colorsys
|
5 |
+
import math
|
6 |
+
import numpy as np
|
7 |
+
from PIL import Image
|
8 |
+
from sklearn.cluster import MiniBatchKMeans
|
9 |
+
import compColors
|
10 |
+
|
11 |
+
PALETTE_WIDTH = 800
|
12 |
+
PALETTE_HEIGHT = 200
|
13 |
+
MAX_SIZE = 500,500
|
14 |
+
|
15 |
+
def make_palette(file, n_clusters):
|
16 |
+
im = Image.open(file)
|
17 |
+
im.thumbnail(MAX_SIZE)
|
18 |
+
|
19 |
+
pixels = np.array([im.getpixel((x,y)) for x in range(0, im.size[0]) for y in range(0, im.size[1])])
|
20 |
+
clt = MiniBatchKMeans(n_clusters = n_clusters, n_init = 1)
|
21 |
+
clt.fit(pixels)
|
22 |
+
return [[int(round(i)) for i in color] for color in clt.cluster_centers_]
|
23 |
+
|
24 |
+
def perceived_brightness (r, g, b):
|
25 |
+
"""
|
26 |
+
Calculates perceived brightness for the given RGB values.
|
27 |
+
code from Darel Rex Finley (http://alienryderflex.com/hsp.html)
|
28 |
+
"""
|
29 |
+
return math.sqrt((.299 * r * r) + (.587 * g * g) + (.114 * b * b))
|
30 |
+
|
31 |
+
def hsp_rank (r, g, b, mult=8):
|
32 |
+
"""
|
33 |
+
Combines hue, saturation, and perceived brightness info for a
|
34 |
+
smoother sort.
|
35 |
+
"""
|
36 |
+
lum = perceived_brightness(r, g, b)
|
37 |
+
h, s, v = colorsys.rgb_to_hsv(r, g, b)
|
38 |
+
|
39 |
+
return (h * lum * mult, s * lum * mult, s * mult)
|
40 |
+
|
41 |
+
def findDominantColors(imageFile, numberOfColors, showImage = True):
|
42 |
+
src = imageFile
|
43 |
+
n_clusters = numberOfColors
|
44 |
+
|
45 |
+
cluster_centers = make_palette(src, n_clusters)
|
46 |
+
cluster_centers.sort(key=lambda rgb: hsp_rank(*rgb))
|
47 |
+
|
48 |
+
pixels = []
|
49 |
+
dominantColors = []
|
50 |
+
for i in range(n_clusters):
|
51 |
+
color = tuple(cluster_centers[i])
|
52 |
+
dominantColors.append(color)
|
53 |
+
# print("Color #{}: {}".format(i+1, color))
|
54 |
+
for j in range(PALETTE_WIDTH//n_clusters * PALETTE_HEIGHT):
|
55 |
+
pixels.append(color)
|
56 |
+
if showImage:
|
57 |
+
img = Image.new('RGB',(PALETTE_HEIGHT,PALETTE_WIDTH))
|
58 |
+
img.putdata(pixels)
|
59 |
+
img.show()
|
60 |
+
return dominantColors
|
61 |
+
|
62 |
+
def rgb_to_hex(rgb):
|
63 |
+
return '#{:02x}{:02x}{:02x}'.format(rgb[0], rgb[1], rgb[2])
|
64 |
+
|
65 |
+
def hex_to_rgb(hex):
|
66 |
+
hex = hex.lstrip('#')
|
67 |
+
return tuple(int(hex[i:i+2], 16) for i in (0, 2, 4))
|
68 |
+
|
69 |
+
def create_color_palette(colors, palette_width=800, palette_height=200):
|
70 |
+
"""
|
71 |
+
Receives a list of colors in hex format and creates a palette image
|
72 |
+
"""
|
73 |
+
pixels = []
|
74 |
+
n_colors = len(colors)
|
75 |
+
for i in range(n_colors):
|
76 |
+
color = hex_to_rgb(colors[i])
|
77 |
+
for j in range(palette_width//n_colors * palette_height):
|
78 |
+
pixels.append(color)
|
79 |
+
img = Image.new('RGB', (palette_height, palette_width))
|
80 |
+
img.putdata(pixels)
|
81 |
+
img.show()
|
82 |
+
|
83 |
+
|
84 |
+
##############pipeline for testing
|
85 |
+
# print("Dominant colors:")
|
86 |
+
# dominantColors=findDominantColors('./estampa-test.png', 10, False)
|
87 |
+
# dominantColorHex=[rgb_to_hex(color) for color in dominantColors]
|
88 |
+
# print(dominantColorHex)
|
89 |
+
# # ['#d54b18', '#555015', '#98680e', '#1f3919', '#da8007', '#a50637', '#b50406', '#d52350', '#e2090f', '#f8545c']
|
recolorLinearColorTransfer.py
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# This script performs the linear color transfer step that
|
2 |
+
# leongatys/NeuralImageSynthesis' Scale Control code performs.
|
3 |
+
# https://github.com/leongatys/NeuralImageSynthesis/blob/master/ExampleNotebooks/ScaleControl.ipynb
|
4 |
+
# Standalone script by github.com/htoyryla, and github.com/ProGamerGov
|
5 |
+
#based on this: https://github.com/ProGamerGov/Neural-Tools
|
6 |
+
|
7 |
+
import numpy as np
|
8 |
+
import argparse
|
9 |
+
import imageio
|
10 |
+
from skimage import io,transform,img_as_float
|
11 |
+
from skimage.io import imread,imsave
|
12 |
+
from PIL import Image
|
13 |
+
from numpy import eye
|
14 |
+
|
15 |
+
parser = argparse.ArgumentParser()
|
16 |
+
parser.add_argument('-t', '--target_image', type=str, help="The image you are transfering color to. Ex: target.png", required=True)
|
17 |
+
parser.add_argument('-s', '--source_image', type=str, help="The image you are transfering color from. Ex: source.png", required=True)
|
18 |
+
parser.add_argument('-o', '--output_image', default='output.png', help="The name of your output image. Ex: output.png", type=str)
|
19 |
+
parser.add_argument('-m', '--mode', default='pca', help="The color transfer mode. Options are pca, chol, or sym.", type=str)
|
20 |
+
parser.add_argument('-e', '--eps', default='1e-5', help="Your epsilon value in scientific notation or normal notation. Ex: 1e-5 or 0.00001", type=float)
|
21 |
+
parser.parse_args()
|
22 |
+
args = parser.parse_args()
|
23 |
+
|
24 |
+
|
25 |
+
Image.MAX_IMAGE_PIXELS = 1000000000 # Support gigapixel images
|
26 |
+
|
27 |
+
|
28 |
+
def main():
|
29 |
+
|
30 |
+
target_img = imageio.v2.imread(args.target_image, pilmode="RGB").astype(float)/256
|
31 |
+
source_img = imageio.v2.imread(args.source_image, pilmode="RGB").astype(float)/256
|
32 |
+
|
33 |
+
output_img = match_color(target_img, source_img, mode=args.mode, eps=args.eps)
|
34 |
+
output_img = (output_img * 255).astype(np.uint8)
|
35 |
+
imsave(args.output_image, output_img)
|
36 |
+
# imsave(args.output_image, output_img)
|
37 |
+
|
38 |
+
|
39 |
+
def match_color(target_img, source_img, mode='pca', eps=1e-5):
|
40 |
+
'''
|
41 |
+
Matches the colour distribution of the target image to that of the source image
|
42 |
+
using a linear transform.
|
43 |
+
Images are expected to be of form (w,h,c) and float in [0,1].
|
44 |
+
Modes are chol, pca or sym for different choices of basis.
|
45 |
+
'''
|
46 |
+
mu_t = target_img.mean(0).mean(0)
|
47 |
+
t = target_img - mu_t
|
48 |
+
t = t.transpose(2,0,1).reshape(3,-1)
|
49 |
+
Ct = t.dot(t.T) / t.shape[1] + eps * eye(t.shape[0])
|
50 |
+
mu_s = source_img.mean(0).mean(0)
|
51 |
+
s = source_img - mu_s
|
52 |
+
s = s.transpose(2,0,1).reshape(3,-1)
|
53 |
+
Cs = s.dot(s.T) / s.shape[1] + eps * eye(s.shape[0])
|
54 |
+
if mode == 'chol':
|
55 |
+
chol_t = np.linalg.cholesky(Ct)
|
56 |
+
chol_s = np.linalg.cholesky(Cs)
|
57 |
+
ts = chol_s.dot(np.linalg.inv(chol_t)).dot(t)
|
58 |
+
if mode == 'pca':
|
59 |
+
eva_t, eve_t = np.linalg.eigh(Ct)
|
60 |
+
Qt = eve_t.dot(np.sqrt(np.diag(eva_t))).dot(eve_t.T)
|
61 |
+
eva_s, eve_s = np.linalg.eigh(Cs)
|
62 |
+
Qs = eve_s.dot(np.sqrt(np.diag(eva_s))).dot(eve_s.T)
|
63 |
+
ts = Qs.dot(np.linalg.inv(Qt)).dot(t)
|
64 |
+
if mode == 'sym':
|
65 |
+
eva_t, eve_t = np.linalg.eigh(Ct)
|
66 |
+
Qt = eve_t.dot(np.sqrt(np.diag(eva_t))).dot(eve_t.T)
|
67 |
+
Qt_Cs_Qt = Qt.dot(Cs).dot(Qt)
|
68 |
+
eva_QtCsQt, eve_QtCsQt = np.linalg.eigh(Qt_Cs_Qt)
|
69 |
+
QtCsQt = eve_QtCsQt.dot(np.sqrt(np.diag(eva_QtCsQt))).dot(eve_QtCsQt.T)
|
70 |
+
ts = np.linalg.inv(Qt).dot(QtCsQt).dot(np.linalg.inv(Qt)).dot(t)
|
71 |
+
matched_img = ts.reshape(*target_img.transpose(2,0,1).shape).transpose(1,2,0)
|
72 |
+
matched_img += mu_s
|
73 |
+
matched_img[matched_img>1] = 1
|
74 |
+
matched_img[matched_img<0] = 0
|
75 |
+
return matched_img
|
76 |
+
|
77 |
+
|
78 |
+
if __name__ == "__main__":
|
79 |
+
main()
|
recolorLumaConverterAlgo.py
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from PIL import Image
|
2 |
+
|
3 |
+
def hex_to_rgb(value):
|
4 |
+
value = value.lstrip('#')
|
5 |
+
length = len(value)
|
6 |
+
return tuple(int(value[i:i + length // 3], 16) for i in range(0, length, length // 3))
|
7 |
+
|
8 |
+
def luminance(color):
|
9 |
+
# Use the ITU-R BT.709 formula
|
10 |
+
r, g, b = color
|
11 |
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
12 |
+
|
13 |
+
# def interpolate_color(color1, color2, factor):
|
14 |
+
# return tuple(int(c1 * (1 - factor) + c2 * factor) for c1, c2 in zip(color1, color2))
|
15 |
+
|
16 |
+
def quadratic_interpolate(factor):
|
17 |
+
#make the interpolation closer to the second color
|
18 |
+
return factor ** 2
|
19 |
+
|
20 |
+
def interpolate_color(color1, color2, factor):
|
21 |
+
adjusted_factor = quadratic_interpolate(factor)
|
22 |
+
return tuple(int(c1 * (1 - adjusted_factor) + c2 * adjusted_factor) for c1, c2 in zip(color1, color2))
|
23 |
+
|
24 |
+
|
25 |
+
def remap_image_colors(image_path, hex_palette):
|
26 |
+
# img = Image.open(image_path)
|
27 |
+
img = Image.fromarray(image_path)
|
28 |
+
|
29 |
+
rgb_palette = [hex_to_rgb(hex_code) for hex_code in hex_palette]
|
30 |
+
|
31 |
+
sorted_palette = sorted(rgb_palette, key=luminance)
|
32 |
+
|
33 |
+
img_rgb = img.convert("RGB")
|
34 |
+
pixels = img_rgb.load()
|
35 |
+
|
36 |
+
for y in range(img.height):
|
37 |
+
for x in range(img.width):
|
38 |
+
original_color = pixels[x, y]
|
39 |
+
lum = luminance(original_color)
|
40 |
+
|
41 |
+
# Find the closest palette colors by luminance (one darker, one brighter)
|
42 |
+
prev_color = sorted_palette[0]
|
43 |
+
next_color = sorted_palette[-1]
|
44 |
+
for i in range(1, len(sorted_palette)):
|
45 |
+
if luminance(sorted_palette[i]) > lum:
|
46 |
+
next_color = sorted_palette[i]
|
47 |
+
prev_color = sorted_palette[i-1]
|
48 |
+
break
|
49 |
+
|
50 |
+
# Interpolate between the two closest colors
|
51 |
+
lum_range = luminance(next_color) - luminance(prev_color)
|
52 |
+
if lum_range == 0:
|
53 |
+
mapped_color = prev_color
|
54 |
+
else:
|
55 |
+
factor = (lum - luminance(prev_color)) / lum_range
|
56 |
+
mapped_color = interpolate_color(prev_color, next_color, factor)
|
57 |
+
|
58 |
+
pixels[x, y] = mapped_color
|
59 |
+
|
60 |
+
img_rgb.save("result.jpg")
|
61 |
+
|
62 |
+
# hex_palette = ['#db5a1e', '#555115', '#9a690e', '#1f3a19', '#da8007', '#9a0633', '#b70406', '#d01b4b', '#e20b0f', '#f7515d']
|
63 |
+
# remap_image_colors('estampa.jpg', hex_palette)
|
recolorOTAlgo.py
ADDED
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
import cv2
|
3 |
+
import ot
|
4 |
+
from PIL import Image
|
5 |
+
|
6 |
+
def transfer_channel(source_channel, target_channel):
|
7 |
+
source_hist, _ = np.histogram(source_channel, bins=256, range=(0, 256))
|
8 |
+
target_hist, _ = np.histogram(target_channel, bins=256, range=(0, 256))
|
9 |
+
|
10 |
+
source_hist = source_hist.astype(np.float64) / source_hist.sum()
|
11 |
+
target_hist = target_hist.astype(np.float64) / target_hist.sum()
|
12 |
+
|
13 |
+
r = np.arange(256).reshape((-1, 1))
|
14 |
+
c = np.arange(256).reshape((1, -1))
|
15 |
+
M = (r - c) ** 2
|
16 |
+
M = M / M.max()
|
17 |
+
|
18 |
+
P = ot.emd(source_hist, target_hist, M)
|
19 |
+
|
20 |
+
transferred_channel = np.zeros_like(source_channel)
|
21 |
+
for i in range(256):
|
22 |
+
transferred_channel[source_channel == i] = P[i].argmax()
|
23 |
+
|
24 |
+
return transferred_channel
|
25 |
+
|
26 |
+
def optimal_transport_color_transfer(source, target):
|
27 |
+
source_lab = cv2.cvtColor(source, cv2.COLOR_BGR2Lab).astype(np.float64)
|
28 |
+
target_lab = cv2.cvtColor(target, cv2.COLOR_BGR2Lab).astype(np.float64)
|
29 |
+
|
30 |
+
# Transfer all channels L, a, and b
|
31 |
+
for ch in range(3):
|
32 |
+
source_lab[:, :, ch] = transfer_channel(source_lab[:, :, ch], target_lab[:, :, ch])
|
33 |
+
|
34 |
+
transferred_rgb = cv2.cvtColor(source_lab.astype(np.uint8), cv2.COLOR_Lab2BGR)
|
35 |
+
return transferred_rgb
|
36 |
+
|
37 |
+
|
38 |
+
def rgb_to_hex(rgb):
|
39 |
+
return '#{:02x}{:02x}{:02x}'.format(rgb[0], rgb[1], rgb[2])
|
40 |
+
|
41 |
+
def hex_to_rgb(hex):
|
42 |
+
hex = hex.lstrip('#')
|
43 |
+
return tuple(int(hex[i:i+2], 16) for i in (0, 2, 4))
|
44 |
+
|
45 |
+
|
46 |
+
def create_color_palette(colors, palette_width=800, palette_height=200):
|
47 |
+
"""
|
48 |
+
Receives a list of colors in hex format and creates a palette image
|
49 |
+
"""
|
50 |
+
pixels = []
|
51 |
+
n_colors = len(colors)
|
52 |
+
for i in range(n_colors):
|
53 |
+
color = hex_to_rgb(colors[i])
|
54 |
+
for j in range(palette_width//n_colors * palette_height):
|
55 |
+
pixels.append(color)
|
56 |
+
img = Image.new('RGB', (palette_height, palette_width))
|
57 |
+
img.putdata(pixels)
|
58 |
+
# img.show()
|
59 |
+
return img
|
60 |
+
|
61 |
+
|
62 |
+
# if __name__ == "__main__":
|
63 |
+
# source = cv2.imread("estampa-test.png")
|
64 |
+
# target = cv2.imread("color.png")
|
65 |
+
# transferred = optimal_transport_color_transfer(source, target)
|
66 |
+
|
67 |
+
# smooth = False#test with true to have a different result.
|
68 |
+
# if smooth:
|
69 |
+
# # Apply bilateral filtering
|
70 |
+
# diameter = 30 # diameter of each pixel neighborhood, adjust based on your image size
|
71 |
+
# sigma_color = 25 # larger value means colors farther to each other will mix together
|
72 |
+
# sigma_space = 25 # larger values means farther pixels will influence each other if their colors are close enough
|
73 |
+
# smoothed = cv2.bilateralFilter(transferred, diameter, sigma_color, sigma_space)
|
74 |
+
# cv2.imwrite("result_OTA.jpg", smoothed)
|
75 |
+
# else:
|
76 |
+
# cv2.imwrite("result_OTA.jpg", transferred)
|
77 |
+
|
78 |
+
|
79 |
+
def recolor(source, colors):
|
80 |
+
pallete_img = create_color_palette(colors)
|
81 |
+
palette_bgr = cv2.cvtColor(np.array(pallete_img), cv2.COLOR_RGB2BGR)
|
82 |
+
recolored = optimal_transport_color_transfer(source, palette_bgr)
|
83 |
+
smooth = True#test with true for different results.
|
84 |
+
if smooth:
|
85 |
+
# Apply bilateral filtering
|
86 |
+
diameter = 10 # diameter of each pixel neighborhood, adjust based on your image size
|
87 |
+
sigma_color = 25 # larger value means colors farther to each other will mix together
|
88 |
+
sigma_space = 15 # larger values means farther pixels will influence each other if their colors are close enough
|
89 |
+
smoothed = cv2.bilateralFilter(recolored, diameter, sigma_color, sigma_space)
|
90 |
+
recoloredFile = cv2.imwrite("result.jpg", smoothed, [cv2.IMWRITE_JPEG_QUALITY, 100])
|
91 |
+
return recoloredFile
|
92 |
+
else:
|
93 |
+
recoloredFile = cv2.imwrite("result.jpg", recolored)
|
94 |
+
return recoloredFile
|
95 |
+
|
96 |
+
|
97 |
+
|
recolorPaletteBasedTransfer.py
ADDED
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#https://gfx.cs.princeton.edu/pubs/Chang_2015_PPR/index.php
|
2 |
+
import numpy as np
|
3 |
+
import cv2
|
4 |
+
from sklearn.cluster import KMeans
|
5 |
+
from PIL import Image
|
6 |
+
|
7 |
+
def extract_palette(image, n_colors=5):
|
8 |
+
"""
|
9 |
+
Extracts dominant colors from the image using KMeans clustering.
|
10 |
+
|
11 |
+
Parameters:
|
12 |
+
- image: np.array, the input image
|
13 |
+
- n_colors: int, the number of colors to extract
|
14 |
+
|
15 |
+
Returns:
|
16 |
+
- palette: list, dominant colors in the image
|
17 |
+
"""
|
18 |
+
# Reshape the image to be a list of pixels
|
19 |
+
pixels = image.reshape(-1, 3)
|
20 |
+
|
21 |
+
# Apply KMeans clustering to find the most dominant colors
|
22 |
+
kmeans = KMeans(n_clusters=n_colors)
|
23 |
+
kmeans.fit(pixels)
|
24 |
+
|
25 |
+
# Get the RGB values of the clusters' centers
|
26 |
+
palette = kmeans.cluster_centers_
|
27 |
+
|
28 |
+
return palette
|
29 |
+
|
30 |
+
def map_colors(source_img, source_palette, target_palette):
|
31 |
+
"""
|
32 |
+
Maps colors from the source palette to the target palette.
|
33 |
+
|
34 |
+
Parameters:
|
35 |
+
- source_img: np.array, the source image
|
36 |
+
- source_palette: list, the source color palette
|
37 |
+
- target_palette: list, the target color palette
|
38 |
+
|
39 |
+
Returns:
|
40 |
+
- recolored_img: np.array, the recolored image
|
41 |
+
"""
|
42 |
+
recolored_img = np.copy(source_img)
|
43 |
+
|
44 |
+
for i in range(source_img.shape[0]):
|
45 |
+
for j in range(source_img.shape[1]):
|
46 |
+
# Find the nearest color in the source palette
|
47 |
+
distances = np.linalg.norm(source_palette - source_img[i, j], axis=1)
|
48 |
+
closest_idx = np.argmin(distances)
|
49 |
+
|
50 |
+
# Replace with the corresponding color from the target palette
|
51 |
+
recolored_img[i, j] = target_palette[closest_idx]
|
52 |
+
|
53 |
+
return recolored_img
|
54 |
+
|
55 |
+
def palette_based_color_transfer(source_img, target_palette, n_colors=5):
|
56 |
+
"""
|
57 |
+
Performs palette based color transfer.
|
58 |
+
|
59 |
+
Parameters:
|
60 |
+
- source_img: np.array, the source image
|
61 |
+
- target_palette: list, the target color palette
|
62 |
+
- n_colors: int, the number of colors in the source palette (default is 5)
|
63 |
+
|
64 |
+
Returns:
|
65 |
+
- recolored_img: np.array, the recolored image
|
66 |
+
"""
|
67 |
+
# Convert the source image to RGB
|
68 |
+
source_img = cv2.cvtColor(source_img, cv2.COLOR_BGR2RGB)
|
69 |
+
|
70 |
+
# Extract the source palette
|
71 |
+
source_palette = extract_palette(source_img, n_colors)
|
72 |
+
|
73 |
+
# Perform color mapping
|
74 |
+
recolored_img = map_colors(source_img, source_palette, target_palette)
|
75 |
+
|
76 |
+
return recolored_img
|
77 |
+
|
78 |
+
def create_color_palette(colors, palette_width=800, palette_height=200):
|
79 |
+
"""
|
80 |
+
Receives a list of colors in hex format and creates a palette image
|
81 |
+
"""
|
82 |
+
pixels = []
|
83 |
+
n_colors = len(colors)
|
84 |
+
for i in range(n_colors):
|
85 |
+
color = hex_to_rgb(colors[i])
|
86 |
+
for j in range(palette_width//n_colors * palette_height):
|
87 |
+
pixels.append(tuple(color)) # Convert color to tuple
|
88 |
+
img = Image.new('RGB', (palette_height, palette_width))
|
89 |
+
img.putdata(pixels)
|
90 |
+
# img.show()
|
91 |
+
return img
|
92 |
+
|
93 |
+
def hex_to_rgb(hex_color):
|
94 |
+
return [int(hex_color[i:i+2], 16) for i in (1, 3, 5)]
|
95 |
+
# Example usage:
|
96 |
+
# source_img_path = "estampa.jpg"
|
97 |
+
# # target_palette = [[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 0], [0, 255, 255]]
|
98 |
+
# hex_colors = ["#db5a1e", "#555115", "#9a690e", "#1f3a19", "#da8007", "#9a0633", "#b70406", "#d01b4b", "#e20b0f", "#f7515d"]
|
99 |
+
|
100 |
+
# target_palette = [hex_to_rgb(color) for color in hex_colors]
|
101 |
+
# recolored_img = palette_based_color_transfer(source_img_path, target_palette, 10)
|
102 |
+
# cv2.imwrite("recolored_image.jpg", cv2.cvtColor(recolored_img, cv2.COLOR_RGB2BGR))
|
103 |
+
|
104 |
+
def recolor(source, colors):
|
105 |
+
palette_img = create_color_palette(colors)
|
106 |
+
palette_bgr = cv2.cvtColor(np.array(palette_img), cv2.COLOR_RGB2BGR)
|
107 |
+
|
108 |
+
target_palette = [hex_to_rgb(color) for color in colors]
|
109 |
+
source_bgr = cv2.cvtColor(source, cv2.COLOR_RGB2BGR)
|
110 |
+
# No need to convert source to BGR, assume it's already in RGB
|
111 |
+
recolored = palette_based_color_transfer(source_bgr, target_palette, 10)
|
112 |
+
cv2.imwrite("result.jpg", cv2.cvtColor(recolored, cv2.COLOR_RGB2BGR))
|
113 |
+
return recolored
|
114 |
+
|
115 |
+
|
recolorReinhardAlgo.py
ADDED
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import cv2
|
2 |
+
import numpy as np
|
3 |
+
from PIL import Image
|
4 |
+
|
5 |
+
def reinhards_color_transfer(source, target):
|
6 |
+
# Convert the images from the RGB to the Lab color space
|
7 |
+
source_lab = cv2.cvtColor(source, cv2.COLOR_BGR2Lab).astype(np.float64)
|
8 |
+
target_lab = cv2.cvtColor(target, cv2.COLOR_BGR2Lab).astype(np.float64)
|
9 |
+
|
10 |
+
# Compute mean and standard deviation for each channel in both images
|
11 |
+
l_mean_src, l_std_src = np.mean(source_lab[:, :, 0]), np.std(source_lab[:, :, 0])
|
12 |
+
a_mean_src, a_std_src = np.mean(source_lab[:, :, 1]), np.std(source_lab[:, :, 1])
|
13 |
+
b_mean_src, b_std_src = np.mean(source_lab[:, :, 2]), np.std(source_lab[:, :, 2])
|
14 |
+
|
15 |
+
l_mean_tar, l_std_tar = np.mean(target_lab[:, :, 0]), np.std(target_lab[:, :, 0])
|
16 |
+
a_mean_tar, a_std_tar = np.mean(target_lab[:, :, 1]), np.std(target_lab[:, :, 1])
|
17 |
+
b_mean_tar, b_std_tar = np.mean(target_lab[:, :, 2]), np.std(target_lab[:, :, 2])
|
18 |
+
|
19 |
+
# Subtract the means from the source image
|
20 |
+
source_lab[:, :, 0] -= l_mean_src
|
21 |
+
source_lab[:, :, 1] -= a_mean_src
|
22 |
+
source_lab[:, :, 2] -= b_mean_src
|
23 |
+
|
24 |
+
# Scale by the standard deviations
|
25 |
+
source_lab[:, :, 0] = (l_std_tar / l_std_src) * source_lab[:, :, 0]
|
26 |
+
source_lab[:, :, 1] = (a_std_tar / a_std_src) * source_lab[:, :, 1]
|
27 |
+
source_lab[:, :, 2] = (b_std_tar / b_std_src) * source_lab[:, :, 2]
|
28 |
+
|
29 |
+
# Add the target means
|
30 |
+
source_lab[:, :, 0] += l_mean_tar
|
31 |
+
source_lab[:, :, 1] += a_mean_tar
|
32 |
+
source_lab[:, :, 2] += b_mean_tar
|
33 |
+
|
34 |
+
# Clip pixel values to ensure they fall within the valid Lab range
|
35 |
+
source_lab[:, :, 0] = np.clip(source_lab[:, :, 0], 0, 255)
|
36 |
+
source_lab[:, :, 1] = np.clip(source_lab[:, :, 1], 0, 255)
|
37 |
+
source_lab[:, :, 2] = np.clip(source_lab[:, :, 2], 0, 255)
|
38 |
+
|
39 |
+
# Convert back to RGB
|
40 |
+
transferred_rgb = cv2.cvtColor(source_lab.astype(np.uint8), cv2.COLOR_Lab2BGR)
|
41 |
+
return transferred_rgb
|
42 |
+
|
43 |
+
def rgb_to_hex(rgb):
|
44 |
+
return '#{:02x}{:02x}{:02x}'.format(rgb[0], rgb[1], rgb[2])
|
45 |
+
|
46 |
+
def hex_to_rgb(hex):
|
47 |
+
hex = hex.lstrip('#')
|
48 |
+
return tuple(int(hex[i:i+2], 16) for i in (0, 2, 4))
|
49 |
+
|
50 |
+
def create_color_palette(colors, palette_width=800, palette_height=200):
|
51 |
+
"""
|
52 |
+
Receives a list of colors in hex format and creates a palette image
|
53 |
+
"""
|
54 |
+
pixels = []
|
55 |
+
n_colors = len(colors)
|
56 |
+
for i in range(n_colors):
|
57 |
+
color = hex_to_rgb(colors[i])
|
58 |
+
for j in range(palette_width//n_colors * palette_height):
|
59 |
+
pixels.append(color)
|
60 |
+
img = Image.new('RGB', (palette_height, palette_width))
|
61 |
+
img.putdata(pixels)
|
62 |
+
# img.show()
|
63 |
+
return img
|
64 |
+
|
65 |
+
|
66 |
+
# if __name__ == "__main__":
|
67 |
+
# source = cv2.imread("estampa.jpg")
|
68 |
+
# colors = ['#6b3d68', '#6d2055', '#695977', '#6b7988', '#6f9b9b']
|
69 |
+
# # Generate palette image
|
70 |
+
# palette_img = create_color_palette(colors)
|
71 |
+
# # Convert the palette image to BGR format
|
72 |
+
# palette_bgr = cv2.cvtColor(np.array(palette_img), cv2.COLOR_RGB2BGR)
|
73 |
+
|
74 |
+
# # Save the palette image
|
75 |
+
# # cv2.imwrite("palette.jpg", palette_bgr)
|
76 |
+
|
77 |
+
# target = palette_bgr#cv2.imread("palette.jpg")
|
78 |
+
|
79 |
+
# transferred = reinhards_color_transfer(source, target)
|
80 |
+
# cv2.imwrite("transferred_reinhard.jpg", transferred)
|
81 |
+
|
82 |
+
|
83 |
+
def recolor(source, colors):
|
84 |
+
palette_img = create_color_palette(colors)
|
85 |
+
palette_bgr = cv2.cvtColor(np.array(palette_img), cv2.COLOR_RGB2BGR)
|
86 |
+
|
87 |
+
recolored = reinhards_color_transfer(source, palette_bgr)
|
88 |
+
recoloredFile = cv2.imwrite("result.jpg", recolored)
|
89 |
+
return recolored
|
recolorReinhardV2Algo.py
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import cv2
|
2 |
+
import numpy as np
|
3 |
+
|
4 |
+
def color_transfer(source, target):
|
5 |
+
# Convert the images from RGB to L*a*b* color space
|
6 |
+
source_lab = cv2.cvtColor(source, cv2.COLOR_BGR2Lab)
|
7 |
+
target_lab = cv2.cvtColor(target, cv2.COLOR_BGR2Lab)
|
8 |
+
|
9 |
+
# Split the LAB image into L, A and B channels
|
10 |
+
source_l, source_a, source_b = cv2.split(source_lab)
|
11 |
+
target_l, target_a, target_b = cv2.split(target_lab)
|
12 |
+
|
13 |
+
# Compute mean and standard deviation for each channel in both images
|
14 |
+
l_mean_src, l_std_src, = source_l.mean(), source_l.std()
|
15 |
+
a_mean_src, a_std_src = source_a.mean(), source_a.std()
|
16 |
+
b_mean_src, b_std_src = source_b.mean(), source_b.std()
|
17 |
+
|
18 |
+
l_mean_tar, l_std_tar = target_l.mean(), target_l.std()
|
19 |
+
a_mean_tar, a_std_tar = target_a.mean(), target_a.std()
|
20 |
+
b_mean_tar, b_std_tar = target_b.mean(), target_b.std()
|
21 |
+
|
22 |
+
# Perform the color transfer
|
23 |
+
l_result = (source_l - l_mean_src) * (l_std_tar / l_std_src) + l_mean_tar
|
24 |
+
a_result = (source_a - a_mean_src) * (a_std_tar / a_std_src) + a_mean_tar
|
25 |
+
b_result = (source_b - b_mean_src) * (b_std_tar / b_std_src) + b_mean_tar
|
26 |
+
|
27 |
+
# Clip and reshape the channels
|
28 |
+
l_result = np.clip(l_result, 0, 255).astype(np.uint8)
|
29 |
+
a_result = np.clip(a_result, 0, 255).astype(np.uint8)
|
30 |
+
b_result = np.clip(b_result, 0, 255).astype(np.uint8)
|
31 |
+
|
32 |
+
# Merge the channels back
|
33 |
+
result_lab = cv2.merge([l_result, a_result, b_result])
|
34 |
+
|
35 |
+
# Convert the resulting image from L*a*b* to RGB color space
|
36 |
+
result = cv2.cvtColor(result_lab, cv2.COLOR_Lab2BGR)
|
37 |
+
|
38 |
+
return result
|
39 |
+
|
40 |
+
# Load source and target images
|
41 |
+
source_img = cv2.imread("estampa.jpg")
|
42 |
+
target_img = cv2.imread("cor.jpg")
|
43 |
+
|
44 |
+
# Perform color transfer
|
45 |
+
transferred_img = color_transfer(source_img, target_img)
|
46 |
+
|
47 |
+
# Return the transferred image for visualization
|
48 |
+
cv2.imwrite("result.jpg", transferred_img)
|
recolorTransferAlgo.py
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
import cv2
|
3 |
+
from PIL import Image
|
4 |
+
|
5 |
+
def read_file(source_image, target_image):
|
6 |
+
s = cv2.cvtColor(source_image, cv2.COLOR_RGB2LAB)
|
7 |
+
t = cv2.cvtColor(np.array(target_image), cv2.COLOR_RGB2LAB)
|
8 |
+
return s, t
|
9 |
+
|
10 |
+
def get_mean_and_std(x):
|
11 |
+
x_mean, x_std = cv2.meanStdDev(x)
|
12 |
+
x_mean = np.hstack(np.around(x_mean,2))
|
13 |
+
x_std = np.hstack(np.around(x_std,2))
|
14 |
+
return x_mean, x_std
|
15 |
+
|
16 |
+
def color_transfer(source, target_image):
|
17 |
+
"""
|
18 |
+
The primary goal of this algorithm is to adjust the colors of the source image
|
19 |
+
such that its color distribution looks like the distribution of the target image.
|
20 |
+
"""
|
21 |
+
s, t = read_file(source, target_image)
|
22 |
+
s_mean, s_std = get_mean_and_std(s)
|
23 |
+
t_mean, t_std = get_mean_and_std(t)
|
24 |
+
height, width, channel = s.shape
|
25 |
+
for i in range(0,height):
|
26 |
+
for j in range(0,width):
|
27 |
+
for k in range(0,channel):
|
28 |
+
x = s[i,j,k]
|
29 |
+
x = ((x-s_mean[k])*(t_std[k]/s_std[k]))+t_mean[k]
|
30 |
+
# round or +0.5
|
31 |
+
x = round(x)
|
32 |
+
# boundary check
|
33 |
+
x = 0 if x<0 else x
|
34 |
+
x = 255 if x>255 else x
|
35 |
+
s[i,j,k] = x
|
36 |
+
s = cv2.cvtColor(s,cv2.COLOR_LAB2BGR)
|
37 |
+
cv2.imwrite('result.jpg',s)
|
38 |
+
|
39 |
+
def rgb_to_hex(rgb):
|
40 |
+
return '#{:02x}{:02x}{:02x}'.format(rgb[0], rgb[1], rgb[2])
|
41 |
+
|
42 |
+
def hex_to_rgb(hex):
|
43 |
+
hex = hex.lstrip('#')
|
44 |
+
return tuple(int(hex[i:i+2], 16) for i in (0, 2, 4))
|
45 |
+
|
46 |
+
def create_color_palette(colors, palette_width=800, palette_height=200):
|
47 |
+
"""
|
48 |
+
Receives a list of colors in hex format and creates a palette image
|
49 |
+
"""
|
50 |
+
pixels = []
|
51 |
+
n_colors = len(colors)
|
52 |
+
for i in range(n_colors):
|
53 |
+
color = hex_to_rgb(colors[i])
|
54 |
+
for j in range(palette_width//n_colors * palette_height):
|
55 |
+
pixels.append(color)
|
56 |
+
img = Image.new('RGB', (palette_height, palette_width))
|
57 |
+
img.putdata(pixels)
|
58 |
+
# img.show()
|
59 |
+
return img
|
60 |
+
|
61 |
+
# colors = ['#d54b18', '#555015', '#98680e', '#1f3919', '#da8007', '#a50637', '#b50406', '#d52350', '#e2090f', '#f8545c']
|
62 |
+
# img = create_color_palette(colors)
|
63 |
+
# color_transfer('s2', img)
|
64 |
+
|
65 |
+
def recolor(source, colors):
|
66 |
+
palette_img = create_color_palette(colors)
|
67 |
+
palette_bgr = cv2.cvtColor(np.array(palette_img), cv2.COLOR_RGB2BGR)
|
68 |
+
source_bgr = cv2.cvtColor(source, cv2.COLOR_RGB2BGR)
|
69 |
+
recolored = color_transfer(source_bgr, palette_bgr)
|
70 |
+
return recolored
|
requirements.txt
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
annotated-types==0.5.0
|
2 |
+
anyio==3.7.1
|
3 |
+
certifi==2023.7.22
|
4 |
+
click==8.1.7
|
5 |
+
dnspython==2.4.2
|
6 |
+
email-validator==2.0.0.post2
|
7 |
+
exceptiongroup==1.1.3
|
8 |
+
fastapi==0.101.1
|
9 |
+
h11==0.14.0
|
10 |
+
httpcore==0.17.3
|
11 |
+
httptools==0.6.0
|
12 |
+
httpx==0.24.1
|
13 |
+
idna==3.4
|
14 |
+
imageio==2.31.1
|
15 |
+
itsdangerous==2.1.2
|
16 |
+
Jinja2==3.1.2
|
17 |
+
joblib==1.3.2
|
18 |
+
lazy_loader==0.3
|
19 |
+
MarkupSafe==2.1.3
|
20 |
+
networkx==3.1
|
21 |
+
numpy==1.25.2
|
22 |
+
opencv-python==4.8.0.76
|
23 |
+
orjson==3.9.5
|
24 |
+
packaging==23.1
|
25 |
+
Pillow==10.0.0
|
26 |
+
POT==0.9.1
|
27 |
+
pydantic==2.2.1
|
28 |
+
pydantic-extra-types==2.0.0
|
29 |
+
pydantic-settings==2.0.3
|
30 |
+
pydantic_core==2.6.1
|
31 |
+
python-dotenv==1.0.0
|
32 |
+
python-multipart==0.0.6
|
33 |
+
PyWavelets==1.4.1
|
34 |
+
PyYAML==6.0.1
|
35 |
+
scikit-image==0.21.0
|
36 |
+
scikit-learn==1.3.0
|
37 |
+
scipy==1.11.2
|
38 |
+
sniffio==1.3.0
|
39 |
+
starlette==0.27.0
|
40 |
+
threadpoolctl==3.2.0
|
41 |
+
tifffile==2023.8.12
|
42 |
+
typing_extensions==4.7.1
|
43 |
+
ujson==5.8.0
|
44 |
+
uvicorn==0.23.2
|
45 |
+
uvloop==0.17.0
|
46 |
+
watchfiles==0.19.0
|
47 |
+
websockets==11.0.3
|
server.py
ADDED
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI, File, UploadFile, Form
|
2 |
+
from fastapi.responses import StreamingResponse
|
3 |
+
from fastapi.middleware.cors import CORSMiddleware
|
4 |
+
from typing import List
|
5 |
+
import io
|
6 |
+
from PIL import Image, ImageOps
|
7 |
+
import numpy as np
|
8 |
+
import compColors
|
9 |
+
import dominantColors
|
10 |
+
import recolorReinhardAlgo
|
11 |
+
import recolorOTAlgo
|
12 |
+
import recolorTransferAlgo
|
13 |
+
import recolorLumaConverterAlgo
|
14 |
+
import recolorPaletteBasedTransfer
|
15 |
+
|
16 |
+
app = FastAPI()
|
17 |
+
|
18 |
+
app.add_middleware(
|
19 |
+
CORSMiddleware,
|
20 |
+
allow_origins=["*"],
|
21 |
+
allow_credentials=True,
|
22 |
+
allow_methods=["*"],
|
23 |
+
allow_headers=["*"],
|
24 |
+
)
|
25 |
+
@app.post("/dominantColor/")
|
26 |
+
async def dominant_color(file: UploadFile = File(...), num_colors: int = Form(...)):
|
27 |
+
"""
|
28 |
+
Receive an image file and an integer and return the dominant color(s).
|
29 |
+
"""
|
30 |
+
file_content = await file.read()
|
31 |
+
image_bytes = io.BytesIO(file_content)
|
32 |
+
im = Image.open(image_bytes)
|
33 |
+
dominantColorsRGB = dominantColors.findDominantColors(image_bytes, num_colors, False)
|
34 |
+
dominantColorsHex = [dominantColors.rgb_to_hex(color) for color in dominantColorsRGB]
|
35 |
+
return {"dominantColors": dominantColorsHex}
|
36 |
+
|
37 |
+
@app.post("/ColorPalettes/")
|
38 |
+
async def color_palettes(colors: str = Form(...)):
|
39 |
+
"""
|
40 |
+
Receive an array of strings representing colors and return a color palette based on these colors.
|
41 |
+
"""
|
42 |
+
#maybe this isn't necessary. converting the string to an array of strings
|
43 |
+
colors = [color.strip() for color in colors.split(',')]
|
44 |
+
#generate the first pallete, which is the complementary colors of the given colors
|
45 |
+
complementaryColors = []
|
46 |
+
for color in colors:
|
47 |
+
complementaryColors.append(compColors.complementary_colors(color))
|
48 |
+
|
49 |
+
#generate the second palette using the adjacent colors algorithm:
|
50 |
+
adjacentColors = []
|
51 |
+
for color in colors:
|
52 |
+
_adjcolors = compColors.adjacent_colors(color)
|
53 |
+
for _color in _adjcolors:
|
54 |
+
if _color not in adjacentColors:
|
55 |
+
adjacentColors.append(_color)
|
56 |
+
#generate the third palette using the gradient colors algorithm:
|
57 |
+
gradientColors = []
|
58 |
+
for i in range(len(colors)-1):
|
59 |
+
gradientColors.append(compColors.gradient_colors(colors[i], colors[i+1]))
|
60 |
+
|
61 |
+
#Fixing size of palletes to 5 colors:
|
62 |
+
complementaryColors = [complementaryColors[i:i + 5] for i in range(0, len(complementaryColors), 5)]
|
63 |
+
adjacentColors = [adjacentColors[i:i + 5] for i in range(0, len(adjacentColors), 5)]
|
64 |
+
colors = [colors[i:i + 5] for i in range(0, len(colors), 5)]
|
65 |
+
|
66 |
+
return {"inputColor": colors, "complementaryColors": complementaryColors, "adjacentColors": adjacentColors, "gradientColors": gradientColors}
|
67 |
+
|
68 |
+
@app.post("/recolor/")
|
69 |
+
async def recolor(file: UploadFile = File(...), colors: str = Form(...), model: str = Form(...)):
|
70 |
+
"""
|
71 |
+
Receive an image file and an array of strings representing colors of a selected pallete and recolor an image.
|
72 |
+
"""
|
73 |
+
method = model
|
74 |
+
invertColors = False
|
75 |
+
colors = [color.strip() for color in colors.split(',')]
|
76 |
+
file_content = await file.read()
|
77 |
+
image_bytes = io.BytesIO(file_content)
|
78 |
+
image = Image.open(image_bytes)
|
79 |
+
if invertColors:
|
80 |
+
image = ImageOps.invert(image)
|
81 |
+
image_np = np.array(image)
|
82 |
+
|
83 |
+
if method == "CCA":
|
84 |
+
#Characteristic Color Analysis
|
85 |
+
recolorReinhardAlgo.recolor(image_np, colors)
|
86 |
+
elif method == "OTA":
|
87 |
+
#Optimal Transport Algorithm transfer
|
88 |
+
recolorOTAlgo.recolor(image_np, colors)
|
89 |
+
elif method =="KMEANS":
|
90 |
+
#K-means clustering transfer
|
91 |
+
recolorTransferAlgo.recolor(image_np, colors)
|
92 |
+
elif method == "LUMA":
|
93 |
+
#Luma converter transfer
|
94 |
+
recolorLumaConverterAlgo.remap_image_colors(image_np, colors)
|
95 |
+
elif method == "palette":
|
96 |
+
#palette transfer
|
97 |
+
recolorPaletteBasedTransfer.recolor(image_np, colors)
|
98 |
+
img_file = open("./result.jpg", "rb")
|
99 |
+
return StreamingResponse(img_file, media_type="image/jpeg")
|
100 |
+
|
101 |
+
|
102 |
+
|
103 |
+
|
104 |
+
if __name__ == "__main__":
|
105 |
+
import uvicorn
|
106 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
107 |
+
|
108 |
+
|
109 |
+
|
110 |
+
|
111 |
+
#how to run:
|
112 |
+
#source env/bin/activate
|
113 |
+
#uvicorn server:app --reload
|
todo.md
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
* add more two funcoes de troca de cor
|
2 |
+
*add invert colors
|
3 |
+
*check file type
|
4 |
+
*mask
|
5 |
+
|
6 |
+
**added:
|
7 |
+
recolor palette based transfer, recolor reinhard v2 and recolor linear transfer. => need to add to the backend.
|
8 |
+
|
9 |
+
`
|
10 |
+
source ./env/bin/activate
|
11 |
+
`
|
12 |
+
#how to run:
|
13 |
+
#source env/bin/activate
|
14 |
+
#uvicorn server:app --reload
|
15 |
+
`
|