-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathst_posterise.py
executable file
·238 lines (181 loc) · 7.12 KB
/
st_posterise.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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
#!/usr/bin/env python3
import os
import argparse
import cv2 as cv
import numpy as np
debug = False
def main():
args = parse_args()
global debug
debug = args.debug
image = cv.imread(args.input)
image = default_posterise(image, max_colours=args.colours,
contrast=args.contrast)
# If output file is .png, ensure it has an alpha channel.
# This allows us to feed it into st_png_to_svg.py directly.
# The image won't be very useful, but this helps during workflow testing.
if args.output.endswith(".png") or args.output.endswith(".PNG"):
image = cv.cvtColor(image, cv.COLOR_BGR2BGRA)
cv.imwrite(args.output, image)
def default_posterise(image, max_colours=6, contrast=1.4):
dark_to_black(image)
# colour contrast + brightness boost?
contrast_brightness(image, contrast=contrast)
if debug:
cv.imwrite("con_bri.png", image)
image = smooth_bilateral(image)
if debug:
cv.imwrite("smooth.png", image)
# image = colour_enhance(image)
# if debug:
# cv.imwrite("col_enhan.png", image)
# Do a first pass kmeans, this produces a grainy, dithered image,
# but in limited palette, with high detail preservation.
# LAB colour space seems to do better here.
small_image = image[::4, ::4, ::1] # 1/16th area image, faster for kmeans
# and we don't care about small areas of colour
small_image = cv.cvtColor(small_image, cv.COLOR_BGR2LAB)
small_image = kmeans(small_image, max_colours=max_colours)
small_image = cv.cvtColor(small_image, cv.COLOR_LAB2BGR)
if debug:
cv.imwrite("kmeans_01.png", small_image)
palette = get_colours(small_image)
# Quantise image to the palette
image = quantise_to_palette(image, palette)
if debug:
cv.imwrite("quant_pal_01.png", image)
# cleanup the dithering with a fast MSS pass.
# This will introduce lots of blended colours...
mean_shift_segment(image)
if debug:
cv.imwrite("mss.png", image)
# Quantise image to the palette
image = quantise_to_palette(image, palette)
if debug:
cv.imwrite("quant_pal_02.png", image)
light_to_white(image)
return image
# This works well but is somewhat slow. Might be a good option to use if you
# know your colours in advance, could use instead of the current
# first kmeans step
def quantise_to_palette(image, palette):
X_query = image.reshape(-1, 3).astype(np.float32)
X_index = palette.astype(np.float32)
# find nearest in palette for each pixel
knn = cv.ml.KNearest_create()
knn.train(X_index, cv.ml.ROW_SAMPLE, np.arange(len(palette)))
ret, results, neighbours, dist = knn.findNearest(X_query, 1)
# replace image data with quantised values
neigh_int = neighbours.astype(np.uint8)
neigh_int = neigh_int.reshape(image.shape[0], image.shape[1], 1)
for i, p in enumerate(palette):
neigh_mask = cv.inRange(neigh_int, i, i)
image[neigh_mask > 0] = palette[i]
return image
def contrast_brightness(image, contrast:float=1.4, brightness:int=0):
"""
Adjusts contrast and brightness of an uint8 image.
contrast: (0.0, inf) with 1.0 leaving the contrast as is
brightness: [-255, 255] with 0 leaving the brightness as is
"""
brightness += int(round(255 * (1 - contrast) / 2))
return cv.addWeighted(image, contrast, image, 0, brightness, image)
def colour_enhance(image):
# CLAHE (Contrast Limited Adaptive Histogram Equalization)
clahe = cv.createCLAHE(clipLimit=1.1, tileGridSize=(8, 8))
lab = cv.cvtColor(image, cv.COLOR_BGR2LAB)
L, a, b = cv.split(lab)
L2 = clahe.apply(L)
lab = cv.merge((L2, a, b))
image = cv.cvtColor(lab, cv.COLOR_LAB2BGR)
return image
def light_to_white(image):
light_lo=np.array([215, 215, 215])
light_hi=np.array([255, 255, 255])
# Mask image to only select lights, force white
mask = cv.inRange(image, light_lo, light_hi)
image[mask > 0] = (255, 255, 255)
def dark_to_black(image):
dark_lo=np.array([0,0,0])
dark_hi=np.array([50,50,50])
# Mask image to only select darks, force black
mask = cv.inRange(image, dark_lo, dark_hi)
image[mask > 0] = (0, 0, 0)
def smooth_bilateral(image):
# like a blur, but preserves edges better
return cv.bilateralFilter(image, 15, 30, 30)
def kmeans(image, max_colours=6):
colours = max_colours
rounds = 1
h, w = image.shape[:2]
samples = np.zeros([h * w, 3], dtype=np.float32)
count = 0
for x in range(h):
for y in range(w):
samples[count] = image[x][y]
count += 1
compactness, labels, centers = cv.kmeans(samples,
colours,
None,
(cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 10000, 0.0001),
rounds,
cv.KMEANS_RANDOM_CENTERS)
centers = np.uint8(centers)
res = centers[labels.flatten()]
return res.reshape((image.shape))
def mean_shift_segment(image, spatial_grouping=3, colour_grouping=25):
# Higher: spatially "blur" over a wider radius. Slower.
# This one is quite subjective, so start low,
# it's faster.
spatial_distance = spatial_grouping
# Lower: larger number of more detailed groups, slower.
# Too low is "bitty" / grainy,
# too high and groups tend to merge together.
colour_distance = colour_grouping
cv.pyrMeanShiftFiltering(image, spatial_distance, colour_distance, image)
def posterise(image):
n = 3
for i in range(n):
image[(image >= i * 255 / n)
& (image < (i + 1) * 255 / n)] = i * 255 / (n - 1)
def get_colours(image):
b, g, r = cv.split(image)
b = b.astype(np.uint32)
g = g.astype(np.uint32)
r = r.astype(np.uint32)
combined_channels = b + (g << 8) + (r << 16)
uniques = np.unique(combined_channels)
# unmunge and return in a sensible format
colours = []
for c in uniques:
colours.append([c & 0xff,
(c >> 8) & 0xff,
(c >> 16) & 0xff])
return np.array(colours).astype(np.uint8)
def parse_args():
description = '''
Smart posterise a supplied image.
'''
parser = argparse.ArgumentParser(description=description)
parser.add_argument("input",
help="image file")
parser.add_argument("output",
help="posterised image file to create")
parser.add_argument("--colours", "-c",
default=6,
type=int,
help="Max colours in the posterised output")
parser.add_argument("--contrast",
default=1.4,
type=float,
help="Enhance or reduce contrast, default 1.4, an increase")
parser.add_argument("--debug",
action="store_true",
help="Save intermediate images, for debugging only")
args = parser.parse_args()
if not os.path.isfile(args.input):
print("input file didn't exist: '%s'" % args.input)
exit()
return args
if __name__ == "__main__":
main()