Spaces:
Running
Running
Adding code files
Browse files- ColorTransfer.py +185 -0
- GradioInterface.py +60 -0
ColorTransfer.py
ADDED
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import cv2
|
2 |
+
import numpy as np
|
3 |
+
from skimage.util import view_as_windows
|
4 |
+
from skimage.exposure import match_histograms
|
5 |
+
|
6 |
+
class ImgProcess:
|
7 |
+
def __init__(self, img1 = None, img2 = None):
|
8 |
+
self.img1 = None
|
9 |
+
self.img2 = None
|
10 |
+
self.img1LAB = None
|
11 |
+
self.img2LAB = None
|
12 |
+
|
13 |
+
self.load(img1, img2)
|
14 |
+
|
15 |
+
def load(self, img1 = None, img2 = None):
|
16 |
+
if img1 != None:
|
17 |
+
self.img1 = cv2.cvtColor(np.array(img1), cv2.COLOR_RGB2BGR)
|
18 |
+
|
19 |
+
if img2 != None:
|
20 |
+
self.img2 = cv2.cvtColor(np.array(img2), cv2.COLOR_RGB2BGR)
|
21 |
+
|
22 |
+
def isImageSet(self):
|
23 |
+
if type(self.img1) == np.ndarray and type(self.img2) == np.ndarray:
|
24 |
+
# if self.img1 != None and self.img2 != None:
|
25 |
+
return True
|
26 |
+
else:
|
27 |
+
print(f"Error! Images not set!\nImage1Set = {self.img1 != None}, Image1Type = {type(self.img1)}\nImage2Set = {self.img2 != None}, Image2Type = {type(self.img2)}")
|
28 |
+
return False
|
29 |
+
|
30 |
+
def __changeGrayaxisToRGB(self):
|
31 |
+
if len(self.img1.shape) == 3:
|
32 |
+
if self.img1.shape[2] == 1:
|
33 |
+
self.img1 = cv2.cvtColor(self.img1,cv2.COLOR_GRAY2BGR)
|
34 |
+
else:
|
35 |
+
self.img1 = cv2.cvtColor(self.img1,cv2.COLOR_GRAY2BGR)
|
36 |
+
|
37 |
+
if len(self.img2.shape) == 3:
|
38 |
+
if self.img2.shape[2] == 1:
|
39 |
+
self.img2 = cv2.cvtColor(self.img2,cv2.COLOR_GRAY2BGR)
|
40 |
+
else:
|
41 |
+
self.img2 = cv2.cvtColor(self.img2,cv2.COLOR_GRAY2BGR)
|
42 |
+
|
43 |
+
def __convertRGBToLAB(self):
|
44 |
+
self.img1LAB = cv2.cvtColor(self.img1,cv2.COLOR_BGR2LAB)
|
45 |
+
self.img2LAB = cv2.cvtColor(self.img2,cv2.COLOR_BGR2LAB)
|
46 |
+
|
47 |
+
def __calculateLABColorMeanAndStd(self):
|
48 |
+
img1mean_lst = []
|
49 |
+
img1std_lst = []
|
50 |
+
|
51 |
+
img2mean_lst = []
|
52 |
+
img2std_lst = []
|
53 |
+
|
54 |
+
for c in range(self.img1LAB.shape[2]):
|
55 |
+
img1mean_lst.append(np.mean(self.img1LAB[:, :, c]))
|
56 |
+
std1 = np.std(self.img1LAB[:, :, c])
|
57 |
+
|
58 |
+
img2mean_lst.append(np.mean(self.img2LAB[:, :, c]))
|
59 |
+
std2 = np.std(self.img2LAB[:, :, c])
|
60 |
+
|
61 |
+
img1std_lst.append(std1 if std1 != 0 else (std2 if std2 != 0 else 1))
|
62 |
+
img2std_lst.append(std2 if std2 != 0 else 1)
|
63 |
+
|
64 |
+
return (img1mean_lst, img1std_lst), (img2mean_lst, img2std_lst)
|
65 |
+
|
66 |
+
def histogramMatching(self):
|
67 |
+
matched = match_histograms(self.img1, self.img2, channel_axis=-1)
|
68 |
+
matched_rgb = cv2.cvtColor(matched.astype(np.uint8), cv2.COLOR_BGR2RGB)
|
69 |
+
return matched_rgb
|
70 |
+
|
71 |
+
def transferColor(self, funPercent = 1.0):
|
72 |
+
print("Transferring ...")
|
73 |
+
output_img = None
|
74 |
+
|
75 |
+
if self.isImageSet():
|
76 |
+
self.__changeGrayaxisToRGB()
|
77 |
+
|
78 |
+
self.__convertRGBToLAB()
|
79 |
+
|
80 |
+
(img1mean_lst, img1std_lst), (img2mean_lst, img2std_lst) = self.__calculateLABColorMeanAndStd()
|
81 |
+
|
82 |
+
img1_trnsfrmd = []
|
83 |
+
|
84 |
+
for c in range(self.img1LAB.shape[2]):
|
85 |
+
img1_star = self.img1LAB[:, :, c] - img1mean_lst[c]
|
86 |
+
img1_dash = img1_star * (img2std_lst[c] / (funPercent * img1std_lst[c]))
|
87 |
+
img1_trnsfrmd.append(img1_dash + img2mean_lst[c])
|
88 |
+
|
89 |
+
img1_trnsfrmd_arr = np.uint8(np.clip(np.rint(np.array(img1_trnsfrmd)), 0, 255))
|
90 |
+
|
91 |
+
img1_trnsfrmd_arr = np.moveaxis(img1_trnsfrmd_arr, 0, -1)
|
92 |
+
|
93 |
+
output_img = cv2.cvtColor(img1_trnsfrmd_arr,cv2.COLOR_LAB2RGB)
|
94 |
+
|
95 |
+
return output_img
|
96 |
+
|
97 |
+
def transferTexture1(self, img1, img2):
|
98 |
+
img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0]))
|
99 |
+
|
100 |
+
# Generate Gaussian pyramid for img1
|
101 |
+
g1 = img1.copy()
|
102 |
+
gp1 = [g1]
|
103 |
+
for i in range(6):
|
104 |
+
g1 = cv2.pyrDown(g1)
|
105 |
+
gp1.append(g1)
|
106 |
+
|
107 |
+
# Generate Gaussian pyramid for img2
|
108 |
+
g2 = img2.copy()
|
109 |
+
gp2 = [g2]
|
110 |
+
for i in range(6):
|
111 |
+
g2 = cv2.pyrDown(g2)
|
112 |
+
gp2.append(g2)
|
113 |
+
|
114 |
+
# Generate Laplacian Pyramid for img1
|
115 |
+
lp1 = [gp1[5]]
|
116 |
+
for i in range(5, 0, -1):
|
117 |
+
GE = cv2.pyrUp(gp1[i])
|
118 |
+
print(GE.shape, gp1[i - 1].shape)
|
119 |
+
GE = cv2.resize(GE, (gp1[i-1].shape[1], gp1[i-1].shape[0]))
|
120 |
+
print(GE.shape, gp1[i - 1].shape)
|
121 |
+
L = cv2.subtract(gp1[i - 1], GE)
|
122 |
+
lp1.append(L)
|
123 |
+
|
124 |
+
# Generate Laplacian Pyramid for img2
|
125 |
+
lp2 = [gp2[5]]
|
126 |
+
for i in range(5, 0, -1):
|
127 |
+
GE = cv2.pyrUp(gp2[i])
|
128 |
+
GE = cv2.resize(GE, (gp2[i-1].shape[1], gp2[i-1].shape[0]))
|
129 |
+
L = cv2.subtract(gp2[i - 1], GE)
|
130 |
+
lp2.append(L)
|
131 |
+
|
132 |
+
# Now add left and right halves of images in each level
|
133 |
+
LS = []
|
134 |
+
for l1, l2 in zip(lp1, lp2):
|
135 |
+
rows, cols, dpt = l1.shape
|
136 |
+
ls = np.hstack((l1[:, 0:cols // 2], l2[:, cols // 2:]))
|
137 |
+
LS.append(ls)
|
138 |
+
|
139 |
+
# Reconstruct the image
|
140 |
+
ls_ = LS[0]
|
141 |
+
for i in range(1, 6):
|
142 |
+
ls_ = cv2.pyrUp(ls_)
|
143 |
+
ls_ = cv2.resize(ls_, (LS[i].shape[1], LS[i].shape[0]))
|
144 |
+
ls_ = cv2.add(ls_, LS[i])
|
145 |
+
|
146 |
+
# Convert back to RGB for Gradio to display
|
147 |
+
texture_transferred = cv2.cvtColor(ls_, cv2.COLOR_BGR2RGB)
|
148 |
+
return texture_transferred
|
149 |
+
|
150 |
+
def transferTexture2(self, img1, img2):
|
151 |
+
img2 = cv2.resize(np.array(img2), (img1.shape[1], img1.shape[0]))
|
152 |
+
|
153 |
+
# Convert both images to numpy arrays (in BGR format for OpenCV)
|
154 |
+
# img1 = np.array(img1)
|
155 |
+
# img2 = np.array(img2)
|
156 |
+
patch_size = 24 # Patch size in pixels
|
157 |
+
overlap_size = 8
|
158 |
+
|
159 |
+
texture_patches = view_as_windows(img2, (patch_size, patch_size, 3), step=patch_size - overlap_size)
|
160 |
+
texture_patches = texture_patches.reshape(-1, patch_size, patch_size, 3)
|
161 |
+
|
162 |
+
# Create output image
|
163 |
+
output_image = np.zeros_like(img1)
|
164 |
+
|
165 |
+
# Place patches in the output image by matching overlaps
|
166 |
+
for i in range(0, img1.shape[0] - patch_size + 1, patch_size - overlap_size):
|
167 |
+
for j in range(0, img1.shape[1] - patch_size + 1, patch_size - overlap_size):
|
168 |
+
# Select a random patch to blend (for simplicity)
|
169 |
+
best_patch = texture_patches[np.random.randint(len(texture_patches))]
|
170 |
+
|
171 |
+
# Blend best patch into output with minimal seams
|
172 |
+
output_image[i:i + patch_size, j:j + patch_size] = best_patch
|
173 |
+
self.blend_patches(output_image, best_patch, i, j, overlap_size, patch_size)
|
174 |
+
|
175 |
+
return output_image
|
176 |
+
|
177 |
+
def loadAndTransfer(self, img1, img2, funPercent = 1.0):
|
178 |
+
self.load(img1, img2)
|
179 |
+
|
180 |
+
color_img1 = self.transferColor(funPercent)
|
181 |
+
color_img2 = self.histogramMatching()
|
182 |
+
return color_img1, color_img2
|
183 |
+
|
184 |
+
#return self.transferTexture1(color_img, self.img2)
|
185 |
+
#return self.transferTexture2(color_img, self.img2)
|
GradioInterface.py
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import cv2
|
3 |
+
import numpy as np
|
4 |
+
from ColorTransfer import ImgProcess
|
5 |
+
|
6 |
+
def initializeImgProcess(img1 = None, img2 = None):
|
7 |
+
return ImgProcess(img1, img2)
|
8 |
+
|
9 |
+
class GradioInterface:
|
10 |
+
def __init__(self):
|
11 |
+
self.imgProcess_obj = None
|
12 |
+
self.imgProcess_obj = self.getImgProcess_obj()
|
13 |
+
height = 300
|
14 |
+
self.fun_percent = 1.0
|
15 |
+
with gr.Blocks() as self.app:
|
16 |
+
gr.Markdown("<h2 style='text-align: center;'>Image Style Transfer App</h2>")
|
17 |
+
with gr.Row():
|
18 |
+
with gr.Column(scale = 1):
|
19 |
+
self.img1 = gr.Image(label="Upload First Image", type="pil", height = height)
|
20 |
+
self.img2 = gr.Image(label="Upload Second Image", type="pil", height = height)
|
21 |
+
|
22 |
+
with gr.Column(scale = 1):
|
23 |
+
self.output_img1 = gr.Image(label="Transformed Image1 - Color Transfer", height = height)
|
24 |
+
self.output_img2 = gr.Image(label="Transformed Image2 - Histogram Match", height = height)
|
25 |
+
|
26 |
+
with gr.Row():
|
27 |
+
with gr.Column(scale = 1):
|
28 |
+
percent_slider = gr.Slider(label="Slide for fun", minimum=0.1, maximum=1.0, step=0.1, value = 1.0)
|
29 |
+
percent_slider.change(self.updateFunPercent, inputs = percent_slider)
|
30 |
+
with gr.Column(scale = 1):
|
31 |
+
transform_button = gr.Button("Transfer Style")
|
32 |
+
transform_button.click(self._transfer_style, inputs=[self.img1, self.img2], outputs = [self.output_img1, self.output_img2])
|
33 |
+
swap_button = gr.Button("Swap Input Images")
|
34 |
+
swap_button.click(self.__swapInputImages, inputs=[self.img1, self.img2], outputs = [self.img1, self.img2])
|
35 |
+
|
36 |
+
def getImgProcess_obj(self):
|
37 |
+
if self.imgProcess_obj != None:
|
38 |
+
return self.imgProcess_obj
|
39 |
+
|
40 |
+
else:
|
41 |
+
return initializeImgProcess()
|
42 |
+
|
43 |
+
def _transfer_style(self, img1, img2):
|
44 |
+
return self.imgProcess_obj.loadAndTransfer(img1, img2, self.fun_percent)
|
45 |
+
|
46 |
+
def updateFunPercent(self, val):
|
47 |
+
self.fun_percent = val
|
48 |
+
|
49 |
+
def __swapInputImages(self, img1, img2):
|
50 |
+
return img2, img1
|
51 |
+
|
52 |
+
def launch_app(self):
|
53 |
+
self.app.launch(share=True)
|
54 |
+
|
55 |
+
if __name__ == "__main__":
|
56 |
+
print("Starting")
|
57 |
+
|
58 |
+
app_obj = GradioInterface()
|
59 |
+
|
60 |
+
app_obj.launch_app()
|