-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathshader.py
223 lines (199 loc) · 10.3 KB
/
shader.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
"""
Prototype program that automatically shades pixel art
Algorithm adapted from the paper "Automatic Sprite Shading" at https://www.sbgames.org/papers/sbgames09/computing/full/cp10_09.pdf
Code author: Mariam Fahmy F. A. Ahmed
"""
import numpy
import cv2
def main():
filename = input("Input image, e.g. drop.png: ")
print("Light position in each direction takes a value from 0 to 1")
print("Horizontal light position: closer to 0 = closer to the left, closer to 1 = closer to the right")
light_position_horizontal = float(input("Horizontal light position: "))
print("Vertical light position: closer to 0 = closer to the top, closer to 1 = closer to the bottom")
light_position_vertical = float(input("Vertical light position: "))
# open image in color
original = cv2.imread(filename)
final = cv2.imread(filename) # make a copy of the original to perform the shading on
# the following copies of the input image will be used to store visual representation of progress so far so we can see what the code is doing
ysection = cv2.imread(filename)
xsection = cv2.imread(filename)
intersection = cv2.imread(filename) # showing both segments in same image
"""black pixels are assumed to be outline pixels
white pixels are assumed to be background pixels"""
def isWithinOutline(pixel):
# if at least one of the B, G, R values is not 0, pixel is not black
not_black = pixel[0] != 0 or pixel[1] != 0 or pixel[2] or 0
# if at least one of the B, G, R values is not 255, pixel is not white
not_white = pixel[0] != 255 or pixel[1] != 255 or pixel[2] != 255
return not_black and not_white
# change color of pixel to [B, G, R]
def changeColor(pixel, B, G, R):
pixel[0] = B
pixel[1] = G
pixel[2] = R
"""
-----Perform Highlight Spot Approximation------
As described in the paper "Automatic Sprite Shading" at https://www.sbgames.org/papers/sbgames09/computing/full/cp10_09.pdf
the highlight spot is the spot with the highest exposure to the light source
"""
# light_position is the position of the light source as [row, col] pair
# row and col are in range [0.0, 1.0]
# [0.0, 0.0] means light source is at top-left corner of coordinate system
light_position = [light_position_vertical, light_position_horizontal] # light source is at position 25% of height of image and 70% width of image
height = original.shape[0] # height of image
width = original.shape[1] # width of image
"""
------Classify pixels into 3 classes (weights)------
As described in the paper "Automatic Sprite Shading" at https://www.sbgames.org/papers/sbgames09/computing/full/cp10_09.pdf
Weight 0: pixels that belong to neither x nor y segments
Weight 1: pixels that belong to only one segment
Weight 2: pixels that are highlight spots (at intersection of x and y segments)
"""
# used to store the shading weight (0, 1 or 2) of each pixel
weight = numpy.zeros(shape=(height, width, 1), dtype=numpy.float32)
# for each row of the image, holds [start column, end column] of pixels that are inside sprite
# (i.e. for each row that has pixels inside the sprite, holds row index and first pixel that is not a background
# or outline pixel and last pixel that is not a background or outline pixel)
# this is used to separate pixels inside sprite from background/outline pixels
sprite = []
# sectioning on the x-direction
for row in range(height):
col = 0
# skip_row is flag to skip a row that only contains irrelevant background pixels
skip_row = True
while col < width:
# if current pixel is not background or outline, keep going till
# a background pixel or outline pixel is reached
if isWithinOutline(original[row, col]):
# both start and end pixels are inside the art
# a start of a line segment along a row is found
# color it blue to show it
start = col
changeColor(ysection[row, col], 0, 255, 128)
# row contains pixels inside sprite, not just background pixels, so don't skip later
skip_row = False
break
col += 1
# now find end pixel
while col < width:
if not isWithinOutline(original[row, col]):
end = col - 1 # previous column is the end
sprite.append([row, start, end])
changeColor(ysection[row, col - 1], 255, 255, 0)
break
col += 1
# calculate center pixel for this row
# using centre pixel = start + light_position * (end - start)
if not skip_row:
center = int(start + light_position[1] * (end - start))
changeColor(ysection[row, center], 0, 0, 255)
changeColor(intersection[row, center], 0, 0, 255)
weight[row, center] = 1
# sectioning on the y-direction
for col in range(width):
row = 0
# skip_row is flag to skip a row that only contains irrelevant background pixels
skip_col = True
while row < height:
# if current pixel is not background or outline, keep going till
# a background pixel or outline pixel is reached
if (isWithinOutline(original[row, col])):
# both start and end pixels are inside the art
# a start of a line segment along a row is found
# color it blue to show it
start = row
changeColor(xsection[start, col], 0, 255, 128)
# row contains pixels inside sprite, not just background pixels, so don't skip later
skip_col = False
break
row += 1
# now find end pixel
while row < height:
if not isWithinOutline(original[row, col]):
end = row - 1 # previous row is the end
changeColor(xsection[end, col], 255, 255, 0)
break
row += 1
# calculate center pixel for this column
# using centre pixel = start + light_position * (end - start)
if not skip_col:
center = int(start + light_position[0] * (end - start))
changeColor(xsection[center, col], 0, 0, 255)
changeColor(intersection[center, col], 0, 0, 255)
# if [center, col] is also part of the x-direction segment, it's a highlight spot
if weight[center, col] == 1:
weight[center, col] = 2
highlight_spot = [center, col]
else: # [center, col] belongs to this segment only
weight[center, col] = 1
"""-----Get Shading Distribution-----"""
"""
Calculate the average shading distribution by blurring the weights matrix, step is adapted from "Automatic Sprite Shading"
"""
shading_distribution = numpy.zeros(shape=(height, width, 1), dtype=float)
shading_distribution = cv2.boxFilter(src=weight, dst=shading_distribution, ddepth=-1, ksize=(121, 121), normalize=True, borderType=cv2.BORDER_ISOLATED)
max_shade = 0
shading_distribution = cv2.boxFilter(src=shading_distribution, dst=shading_distribution, ddepth=-1, ksize=(99, 99), normalize=True, borderType=cv2.BORDER_ISOLATED)
for row in range(height):
for col in range(width):
if shading_distribution[row, col] > max_shade:
max_shade = shading_distribution[row, col]
# normalize shading distribution to take values in range [0, 1.0]
for row in range(height):
for col in range(width):
shading_distribution[row, col] = shading_distribution[row, col] / (max_shade + 0.00000001)
"""---------Assign shades to pixels-------"""
# convert sprite to HSI model to facilitate shading
hsi_art = cv2.cvtColor(original, cv2.COLOR_BGR2HSV)
# loop over every pixel inside sprite
for r in range(len(sprite)):
for col in range(sprite[r][1], sprite[r][2] + 1): # from start to end of pixels INSIDE sprite
row = sprite[r][0]
base_intensity = hsi_art[row, col][2]
if shading_distribution[row, col] <= 0.3:
hsi_art[row, col][2] = base_intensity * 0.8
elif shading_distribution[row, col] <= 0.4:
hsi_art[row, col][2] = base_intensity * 0.85
elif shading_distribution[row, col] <= 0.5:
hsi_art[row, col][2] = base_intensity * 0.9
elif shading_distribution[row, col] <= 0.6:
hsi_art[row, col][2] = base_intensity * 0.95
elif shading_distribution[row, col] <= 0.7:
hsi_art[row, col][2] = base_intensity * 1
elif shading_distribution[row, col] <= 0.8:
hsi_art[row, col][2] = min(255, base_intensity * 1.05)
elif shading_distribution[row, col] <= 0.9:
hsi_art[row, col][2] = 255
else:
hsi_art[row, col][1] = 0
hsi_art[row, col][2] = 255
unpixelated = cv2.cvtColor(hsi_art, cv2.COLOR_HSV2BGR) # shaded art so far in RGB but not in pixel-art style
"""
--------Pixelate the art so the final result has pixel-art style---------
Code credit: https://stackoverflow.com/questions/55508615/how-to-pixelate-image-using-opencv-in-python, HansHirse
"""
"""Start of StackOverFlow code:"""
# Get input size
height, width = unpixelated.shape[:2]
# Desired "pixelated" size
w, h = (16, 16)
# Resize input to "pixelated" size
temp = cv2.resize(unpixelated, (w, h), interpolation=cv2.INTER_LINEAR)
# Initialize output image
final = cv2.resize(temp, (width, height), interpolation=cv2.INTER_NEAREST)
"""End of StackOverFlow code"""
"""Save result as new image"""
cv2.imwrite(filename[:filename.index('.')] + "_shaded.png", final)
"""Display results"""
# display original and visual representation of progress by the code
# --->>>> to see how the code works, uncomment the commented imshow calls
cv2.imshow('Original art', original)
#cv2.imshow('y-direction segmentation', ysection)
#cv2.imshow('x-direction segmentation', xsection)
#cv2.imshow('intersection', intersection)
#cv2.imshow('Shading distribution', shading_distribution)
#cv2.imshow('Unpixelated', unpixelated)
cv2.imshow('Final', final)
cv2.waitKey()
main()