Created
May 9, 2020 08:31
-
-
Save mohnjoosemiller/808afcdae0f46f9f48aa69482bcc8ffe to your computer and use it in GitHub Desktop.
modifications to https://github.com/ghostwriternr/lowpolify to get working output for directory of files for https://twitter.com/LowPolyLOONA
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'''Lowpolify any image using Delaunay triangulation''' | |
import os | |
import sys | |
import random | |
import warnings | |
from multiprocessing import Process | |
import cv2 | |
import numpy as np | |
from scipy.spatial import Delaunay #pylint: disable-msg=E0611 | |
import sharedmem | |
import dlib | |
import itertools | |
import glob | |
# Path to predictor used in face detection model | |
predictor_path = os.path.join(os.path.dirname(__file__), "shape_predictor_68_face_landmarks.dat") | |
# Threshold for intra-triangle variance | |
varhtresh = 25 | |
''' | |
def chunk(l, n): | |
#Splits a list into n chunks' | |
for i in range(0, len(l), n): | |
yield l[i:i + n] | |
''' | |
def builder(part, tridex, lowpoly_image, highpoly_image): | |
'''Generates a portion of the final image''' | |
for tri in part: | |
lowpoly_image[tridex == tri, :] = np.mean( | |
highpoly_image[tridex == tri, :], axis=0) | |
def PolyArea(x,y): | |
'''Returns area of triangle''' | |
return 0.5*np.abs(np.dot(x,np.roll(y,1))-np.dot(y,np.roll(x,1))) | |
def reduce_tail(l, index, threshold=1): | |
elm = l[index] | |
mask = np.linalg.norm(elm-l, axis=1) > threshold | |
mask[:index+1] = True #ensure to return the head of the array unchanged | |
return l[mask] | |
def my_reduce(z, threshold=1): | |
z = np.array(z) | |
index = 0 | |
while True: | |
z = reduce_tail(z, index, threshold) | |
index += 1 | |
if index == z.shape[0]: | |
break | |
return z.tolist() | |
def divideHighVariance(tris, highPolyImage): | |
'''Divide triangles further if variance crosses a threshold''' | |
# print(tris.points.size) | |
subs = np.transpose(np.where(np.ones(highPolyImage.shape[:2]))) | |
subs = subs[:, :2] | |
tridex = tris.find_simplex(subs) | |
tridex = tridex.reshape(highPolyImage.shape[:2]) | |
pTris = np.unique(tridex) | |
for tri in pTris: | |
var3d = np.std( | |
highPolyImage[tridex == tri, :], axis=0) | |
var = sum(var3d) / 3 | |
if var > varhtresh: | |
newPts = [] | |
subarr = np.asarray(np.where(tridex == tri)) | |
for i in range(subarr.shape[1]): | |
newPts.append([subarr[0][i], subarr[1][i]]) | |
# x = [p[0] for p in newPts] | |
# y = [p[1] for p in newPts] | |
# centroid = (sum(x) / len(newPts), sum(y) / len(newPts)) | |
# print(centroid) | |
randNewPts = [newPts[i] for i in np.random.randint( | |
0, len(newPts) - 1, size=int(round(0.4 * len(newPts))))] | |
tris.add_points(randNewPts) | |
# tris.add_points([[int(round(sum(x) / len(newPts))), | |
# int(round(sum(y), len(newPts)))]]) | |
# print(tris.points.size) | |
def get_lowpoly(tris, highpoly_image): | |
'''Returns low poly image''' | |
# 'highpoly_image.shape[:2]' returns the dimensions of the image. | |
# 'np.ones(highpoly_image.shape[:2])' gives same size image, filled with 1. | |
# So subs contains an array of all coordinates of new array. | |
subs = np.transpose(np.where(np.ones(highpoly_image.shape[:2]))) | |
subs = subs[:, :2] | |
# Find the simplices in 'tris' containing the given points | |
tridex = tris.find_simplex(subs) | |
# Array of image dimensions with mapping to the repective simplices. | |
tridex = tridex.reshape(highpoly_image.shape[:2]) | |
# Retrieve the unique simplices from tridex | |
p_tris = np.unique(tridex) | |
# Split the list into multiple parts, to enable parallel processing | |
#chunks = list(chunk(p_tris, len(p_tris) // 4)) | |
# Initialize output image (3-channel) | |
# lowpoly_image contains mean of all such points from highpoly_image, where | |
# tridex = tri | |
lowpoly_image = sharedmem.empty(highpoly_image.shape) | |
lowpoly_image.fill(0) | |
# Start 4 different processes to simultaneouly process different simplices | |
#processes = [Process(target=builder, args=( | |
# p_tris, tridex, lowpoly_image, highpoly_image)) for i in range(4)] | |
# Start each process | |
#for p in processes: | |
# p.start() | |
# Wait for all process to finish | |
#for p in processes: | |
# p.join() | |
# unint8 represents Unsigned integer (0 to 255) | |
builder(p_tris, tridex, lowpoly_image, highpoly_image) | |
lowpoly_image = lowpoly_image.astype(np.uint8) | |
# return low-poly image | |
return lowpoly_image | |
def get_triangulation(im, gray_image, a=50, b=55, c=0.15, show=False, randomize=False): | |
'''Returns triangulations''' | |
# Using canny edge detection. | |
# | |
# Reference: http://docs.opencv.org/3.1.0/da/d22/tutorial_py_canny.html | |
# First argument: Input image | |
# Second argument: minVal (argument 'a') | |
# Third argument: maxVal (argument 'b') | |
# | |
# 'minVal' and 'maxVal' are used in the Hysterisis Thresholding step. | |
# Any edges with intensity gradient more than maxVal are sure to be edges | |
# and those below minVal are sure to be non-edges, so discarded. Those who | |
# lie between these two thresholds are classified edges or non-edges based | |
# on their connectivity. | |
detector = dlib.get_frontal_face_detector() | |
predictor = dlib.shape_predictor(predictor_path) | |
edges = cv2.Canny(gray_image, a, b) | |
if show: | |
cv2.imshow('Canny', edges) | |
cv2.waitKey(0) | |
cv2.destroyAllWindows() | |
win = dlib.image_window() | |
# Set number of points for low-poly edge vertices | |
num_points = int(np.where(edges)[0].size * c) | |
# Return the indices of the elements that are non-zero. | |
# 'nonzero' returns a tuple of arrays, one for each dimension of a, | |
# containing the indices of the non-zero elements in that dimension. | |
# So, r consists of row indices of non-zero elements, and c column indices. | |
r, c = np.nonzero(edges) | |
# r.shape, here, returns the count of all points that belong to an edge. | |
# So 'np.zeros(r.shape)' an array of this size, with all zeros. | |
# 'rnd' is thus an array of this size, with all values as 'False'. | |
rnd = np.zeros(r.shape) == 1 | |
# Mark indices from beginning to 'num_points - 1' as True. | |
rnd[:num_points] = True | |
# Shuffle | |
np.random.shuffle(rnd) | |
# Randomly select 'num_points' of points from the set of all edge vertices. | |
r = r[rnd] | |
c = c[rnd] | |
# Number of rows and columns in image | |
sz = im.shape | |
r_max = sz[0] | |
c_max = sz[1] | |
# Co-ordinates of all randomly chosen points | |
pts = np.vstack([r, c]).T | |
if randomize: | |
rand_offset = 50 | |
rand_dirs = [(0, rand_offset), (-rand_offset, 0), (0, -rand_offset), (rand_offset, 0)] | |
rnd_count = 0 | |
for point in pts: | |
if random.random() < 0.3: | |
rnd_count += 1 | |
rand_dir = random.randint(0, 3) | |
point[0] += rand_dirs[rand_dir][0] | |
point[1] += rand_dirs[rand_dir][1] | |
# Append (0,0) to the vertical stack | |
pts = np.vstack([pts, [0, 0]]) | |
# Append (0,c_max) to the vertical stack | |
pts = np.vstack([pts, [0, c_max]]) | |
# Append (r_max,0) to the vertical stack | |
pts = np.vstack([pts, [r_max, 0]]) | |
# Append (r_max,c_max) to the vertical stack | |
pts = np.vstack([pts, [r_max, c_max]]) | |
# Append some random points to fill empty spaces | |
pts = np.vstack([pts, np.random.randint(0, 2000, size=(100, 2))]) | |
# print(len(pts)) | |
# pts = my_reduce(pts, 5) | |
# print(len(pts)) | |
dets = detector(im, 1) | |
# print("Number of faces detected: {}".format(len(dets))) | |
if show: | |
win.clear_overlay() | |
win.set_image(im) | |
for k, d in enumerate(dets): | |
shape = predictor(im, d) | |
for i in range(shape.num_parts): | |
pts = np.vstack([pts, [shape.part(i).x, shape.part(i).y]]) | |
if show: | |
win.add_overlay(shape) | |
if show: | |
win.add_overlay(dets) | |
dlib.hit_enter_to_continue() | |
# Construct Delaunay Triangulation from these set of points. | |
# Reference: https://en.wikipedia.org/wiki/Delaunay_triangulation | |
tris = Delaunay(pts, incremental=True) | |
# tris_vertices = pts[tris.simplices] | |
# for tri in range(tris_vertices.shape[0]): | |
# x_coords = [] | |
# y_coords = [] | |
# print(tris_vertices[tri]) | |
# for coord in range(tris_vertices.shape[1]): | |
# x_coords.append(tris_vertices[tri][coord][0]) | |
# y_coords.append(tris_vertices[tri][coord][1]) | |
# divideHighVariance(tris, im) | |
tris.close() | |
# exit(0) | |
# Return triangulation | |
return tris | |
def pre_process(highpoly_image, newSize=None): | |
'''Preprocessing helper''' | |
print('Preprocessing') | |
# Handle grayscale images | |
if highpoly_image.shape[2] == 1: | |
# 'dstack' concatenates images along the third dimension | |
# Similar to np.concatenate(tup, axis=2) | |
# So basically, extending a gray scale image to a 3 channel image | |
highpoly_image = highpoly_image.dstack( | |
[highpoly_image, highpoly_image, highpoly_image]) | |
# Resize image. Easier to process. | |
if newSize is not None: | |
if newSize < np.max(highpoly_image.shape[:2]): | |
scale = newSize / float(np.max(highpoly_image.shape[:2])) | |
highpoly_image = cv2.resize( | |
highpoly_image, (0, 0), fx=scale, fy=scale, | |
interpolation=cv2.INTER_AREA) | |
# Reduce noise in image using cv::cuda::fastNlMeansDenoisingColored | |
# Reference: http://www.ipol.im/pub/art/2011/bcm_nlm/ | |
noiseless_highpoly_image = cv2.fastNlMeansDenoisingColored( | |
highpoly_image, None, 10, 10, 7, 21) | |
print('Preprocessing complete') | |
return highpoly_image, noiseless_highpoly_image | |
def helper(inImage, c=0.3, outImage=None, show=False): | |
'''Helper function''' | |
# Read the input image | |
highpoly_image = cv2.imread(inImage) | |
# Call 'pre_process' function | |
highpoly_image, noiseless_highpoly_image = pre_process(highpoly_image, newSize=750) | |
print('Begin thresholding') | |
# Use Otsu's method for calculating thresholds | |
gray_image = cv2.cvtColor(noiseless_highpoly_image, cv2.COLOR_BGR2GRAY) | |
ycbcr_image = cv2.cvtColor(noiseless_highpoly_image, cv2.COLOR_RGB2YCrCb) | |
for xdim in range(ycbcr_image.shape[0]): | |
for ydim in range(ycbcr_image.shape[1]): | |
ycbcr_image[xdim][ydim] = ycbcr_image[xdim][ydim][0] | |
ycbcr_image = ycbcr_image[:, :, 0] | |
clahe = cv2.createCLAHE() | |
normalized_gray_image = clahe.apply(gray_image) | |
if show: | |
compare = np.hstack([gray_image, ycbcr_image]) | |
cv2.imshow('gray images', compare) | |
cv2.waitKey(0) | |
cv2.destroyAllWindows() | |
high_thresh, thresh_im = cv2.threshold( | |
ycbcr_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) | |
# thresh_im = cv2.adaptiveThreshold( | |
# gray_image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) | |
if show: | |
cv2.imshow('otsu image', thresh_im) | |
cv2.waitKey(0) | |
cv2.destroyAllWindows() | |
low_thresh = 0.5 * high_thresh | |
blurred_gray_image = cv2.GaussianBlur(gray_image, (0, 0), 3) | |
sharp_gray_image = cv2.addWeighted(gray_image, 2.5, blurred_gray_image, -1, 0) | |
if show: | |
cv2.imshow('Sharp gray image', sharp_gray_image) | |
cv2.waitKey(0) | |
cv2.destroyAllWindows() | |
print('Triangulating') | |
# Call 'get_triangulation' function | |
tris = get_triangulation(highpoly_image, sharp_gray_image, low_thresh, high_thresh, c, show) | |
print('Triangulation complete. Begin rendering...') | |
# Call 'get_lowpoly' function | |
lowpoly_image = get_lowpoly(tris, highpoly_image) | |
print('Rendering complete') | |
if np.max(highpoly_image.shape[:2]) < 2000: | |
scale = 2000 / float(np.max(highpoly_image.shape[:2])) | |
lowpoly_image = cv2.resize(lowpoly_image, None, fx=scale, | |
fy=scale, interpolation=cv2.INTER_CUBIC) | |
if show: | |
compare = np.hstack([highpoly_image, lowpoly_image]) | |
cv2.imshow('Compare', compare) | |
cv2.waitKey(0) | |
cv2.destroyAllWindows() | |
if outImage is not None: | |
#cv2.imshow('asdf',lowpoly_image) | |
print(outImage) | |
cv2.imwrite(outImage, lowpoly_image) | |
print('Done') | |
pics = glob.glob(r"loonapics\*.*") | |
count=0 | |
for i in pics: | |
print([i+"asd.jpg"]) | |
helper(inImage=i,c=.6,outImage=str(count)+".jpg",show=False) | |
count+=1 | |
''' | |
def main(args): | |
#Main function | |
# No input image | |
if len(args) < 1: | |
print('Invalid') | |
# Input image specified | |
else: | |
input_image = args[0] | |
output_image = None | |
fraction = 0.15 | |
# Output destination specified | |
if len(args) == 2: | |
output_image = args[1] | |
if len(args) == 3: | |
output_image = args[1] | |
fraction = float(args[2]) | |
print("Processing started") | |
# Call helper function | |
helper(inImage=input_image, c=fraction, | |
outImage=output_image, show=False) | |
if __name__ == '__main__': | |
warnings.filterwarnings("ignore") | |
main(sys.argv[1:]) | |
''' | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment