Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement #11

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 31 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# linedraw
Convert images to vectorized line drawings for plotters.
![Alt text](./screenshots/1.png?raw=true "")
![Alt text](./docs/assets/1.png?raw=true "")

- Exports polyline-only svg file with optimized stroke order for plotters;
- Sketchy style powered by Perlin noise;
Expand All @@ -9,44 +9,57 @@ Convert images to vectorized line drawings for plotters.
## Dependencies
Python 2 or 3, PIL/Pillow, numpy, OpenCV (Optional for better performance)

```shell
pip install -r requirements.txt
```

## Usage
Convert an image to line drawing and export .SVG format:

```shell
$ python linedraw.py -i input.jpg -o output.svg
python linedraw.py -i input.jpg -o output.svg
```
Command specs:

```
usage: linedraw.py [-h] [-i [INPUT_PATH]] [-o [OUTPUT_PATH]] [-b] [-nc] [-nh]
[--no_cv] [--hatch_size [HATCH_SIZE]]
[--contour_simplify [CONTOUR_SIMPLIFY]]
usage: linedraw.py [-h] [-i [INPUT_PATH]] [-o [OUTPUT_PATH]] [-r [RESOLUTION]] [-b] [-nc] [-nh] [--no-cv] [--hatch-size [HATCH_SIZE]] [--contour-simplify [CONTOUR_SIMPLIFY]] [-v]
[--save-settings]

Convert image to vectorized line drawing for plotters.

optional arguments:
options:
-h, --help show this help message and exit
-i [INPUT_PATH], --input [INPUT_PATH]
Input path
Input image path
-o [OUTPUT_PATH], --output [OUTPUT_PATH]
Output path.
-b, --show_bitmap Display bitmap preview.
-nc, --no_contour Don't draw contours.
-nh, --no_hatch Disable hatching.
--no_cv Don't use openCV.
--hatch_size [HATCH_SIZE]
Output image path
-r [RESOLUTION], --resolution [RESOLUTION]
Resolution of the output image
-b, --show-bitmap Display bitmap preview.
-nc, --no-contour Don't draw contours.
-nh, --no-hatch Disable hatching.
--no-cv Don't use openCV.
--hatch-size [HATCH_SIZE]
Patch size of hatches. eg. 8, 16, 32
--contour_simplify [CONTOUR_SIMPLIFY]
--contour-simplify [CONTOUR_SIMPLIFY]
Level of contour simplification. eg. 1, 2, 3
-v, --visualize Visualize the output using turtle
--save-settings To Save the settings to a json file

```
Python:

```python
import linedraw

linedraw.argument.resolution = 512 # set arguments
lines = linedraw.sketch("path/to/img.jpg") # return list of polylines, eg.
# [[(x,y),(x,y),(x,y)],[(x,y),(x,y),...],...]
linedraw.visualize(lines) # simulates plotter behavior
# draw the lines in order using turtle graphics.
# [[(x,y),(x,y),(x,y)],[(x,y),(x,y),...],...]

linedraw.visualize(lines) # simulates plotter behavior
# draw the lines in order using turtle graphics.
```

## Future Plans
1. Rasterised Output
2. GUI for the tool
File renamed without changes
Binary file removed images/cameraman.tif
Binary file not shown.
Binary file removed images/lenna.png
Binary file not shown.
Binary file removed images/peppers.png
Binary file not shown.
Binary file removed images/test.jpg
Binary file not shown.
1 change: 1 addition & 0 deletions line_draw/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from line_draw.helper import sketch
29 changes: 29 additions & 0 deletions line_draw/default.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import json

class Default:
export_path = "output/out.svg"
show_bitmap = False
draw_contours = True
draw_hatch = True
no_cv = False
hatch_size = 16
contour_simplify = 2
resolution = 1024
save_settings = False

def save(self,settings_path):
print("Savings settings to a JSON file")
file = open(settings_path, 'w')
data = {
"resolution": self.resolution,
"show_bitmap": self.show_bitmap,
"draw_contours": self.draw_contours,
"draw_hatch": self.draw_hatch,
"use_opencv": not self.no_cv,
"hatch_size": self.hatch_size,
"contour_simplify": self.contour_simplify
}
json.dump(data,file)
file.close()

argument = Default()
File renamed without changes.
235 changes: 235 additions & 0 deletions line_draw/helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
from PIL import Image, ImageOps, ImageDraw
import line_draw.perlin as perlin
from datetime import datetime
import os

from line_draw.filters import appmask, F_SobelX, F_SobelY
from line_draw.default import argument
from line_draw.util import distsum, is_image_file, extract_file_name_and_extension
from line_draw.strokesort import sortlines


def sketch(input_path, output_path:str):
IMAGE = None

if not is_image_file(input_path):
return print("Please provide the path for an image.")

out_file, out_ext = extract_file_name_and_extension(output_path)

if not out_file:
in_file, in_ext = extract_file_name_and_extension(input_path)
out_ext = '.svg'
if not output_path.endswith('/'):
output_path += '/'
output_path += in_file + out_ext

if out_ext != '.svg':
return print("Currently we can only save as svg file")

try:
IMAGE = Image.open(input_path)
except FileNotFoundError:
return print("The Input File wasn't found. Check Path")

width, height = IMAGE.size

IMAGE = IMAGE.convert("L")
IMAGE = ImageOps.autocontrast(IMAGE, 10)

lines = []

if argument.draw_contours:
lines += get_contours(IMAGE.resize((argument.resolution // argument.contour_simplify,
argument.resolution // argument.contour_simplify * height // width)))

if argument.draw_hatch:
lines += hatch(IMAGE.resize(
(argument.resolution // argument.hatch_size, argument.resolution // argument.hatch_size * height // width)))

lines = sortlines(lines)

if argument.show_bitmap:
disp = Image.new("RGB", (argument.resolution, argument.resolution * height // width), (255, 255, 255))
draw = ImageDraw.Draw(disp)
for l in lines:
draw.line(l, (0, 0, 0), 5)
disp.show()

# if out_ext != '.svg':
# now = datetime.now()
# svg_path = output_path.rsplit('/', 1)[0] + now.strftime("%Y%m%d%H%M%S%f") + '.svg'
# else:
# svg_path = output_path

file = open(output_path, 'w')
file.write(make_svg(lines))
file.close()

# if out_ext != '.svg':
# if not is_image_file(output_path):
# return "Output path is not an image path"
# rasterise_image(svg_path,output_path)
# os.remove(svg_path)
print(len(lines), "strokes.")
if argument.save_settings:
argument.save(os.path.dirname(output_path) + '/settings.json')
print("done.")
return lines


def get_contours(image):
print("Generating Contours....")
image = find_edges(image)
image_copy1 = image.copy()
image_copy2 = image.rotate(-90, expand=True).transpose(Image.FLIP_LEFT_RIGHT)
image_copy1_dots = get_dots(image_copy1)
image_copy1_contours = connect_dots(image_copy1_dots)
image_copy2_dots = get_dots(image_copy2)
image_copy2_contours = connect_dots(image_copy2_dots)

for i in range(len(image_copy2_contours)):
image_copy2_contours[1] = [(c[1], c[0]) for c in image_copy2_contours[i]]
contours = image_copy1_contours + image_copy2_contours

for i in range(len(contours)):
for j in range(len(contours)):
if len(contours[i]) > 0 and len(contours[j]) > 0:
if distsum(contours[j][0], contours[i][-1]) < 8:
contours[i] = contours[i] + contours[j]
contours[j] = []

for i in range(len(contours)):
contours[i] = [contours[i][j] for j in range(0, len(contours[i]), 8)]

contours = [c for c in contours if len(c) > 1]

for i in range(0, len(contours)):
contours[i] = [(v[0] * argument.contour_simplify, v[1] * argument.contour_simplify) for v in contours[i]]

for i in range(0, len(contours)):
for j in range(0, len(contours[i])):
contours[i][j] = int(contours[i][j][0] + 10 * perlin.noise(i * 0.5, j * 0.1, 1)), int(
contours[i][j][1] + 10 * perlin.noise(i * 0.5, j * 0.1, 2))

return contours


def find_edges(image):
print("Fining Edges....")
if argument.no_cv:
appmask(image, [F_SobelX, F_SobelY])
else:
import numpy as np
import cv2
image = np.array(image)
image = cv2.GaussianBlur(image, (3, 3), 0)
image = cv2.Canny(image, 100, 200)
image = Image.fromarray(image)
return image.point(lambda p: p > 128 and 255)


def get_dots(image):
print("Getting contour points...")
PX = image.load()
dots = []
width, height = image.size
for y in range(height - 1):
row = []
for x in range(1, width):
if PX[x, y] == 255:
if len(row) > 0:
if x - row[-1][0] == row[-1][-1] + 1:
row[-1] = (row[-1][0], row[-1][-1] + 1)
else:
row.append((x, 0))
else:
row.append((x, 0))
dots.append(row)
return dots


def connect_dots(dots):
print("Connecting contour points....")
contours = []
for y in range(len(dots)):
for x, v in dots[y]:
if v > -1:
if y == 0:
contours.append([(x, y)])
else:
closest = -1
cdist = 100
for x0, v0 in dots[y - 1]:
if abs(x0 - x) < cdist:
cdist = abs(x0 - x)
closest = x0
if cdist > 3:
contours.append([(x, y)])
else:
found = 0
for i in range(len(contours)):
if contours[i][-1] == (closest, y - 1):
contours[i].append((x, y,))
found = 1
break
if found == 0:
contours.append([(x, y)])
for c in contours:
if c[-1][1] < y - 1 and len(c) < 4:
contours.remove(c)
return contours


def hatch(image):
print("Hatching....")
PX = image.load()
width, height = image.size
lg1 = []
lg2 = []
for x0 in range(width):
for y0 in range(height):
x = x0 * argument.hatch_size
y = y0 * argument.hatch_size
if PX[x0, y0] > 144:
pass
elif PX[x0, y0] > 64:
lg1.append([(x, y + argument.hatch_size / 4), (x + argument.hatch_size, y + argument.hatch_size / 4)])
elif PX[x0, y0] > 16:
lg1.append([(x, y + argument.hatch_size / 4), (x + argument.hatch_size, y + argument.hatch_size / 4)])
lg2.append([(x + argument.hatch_size, y), (x, y + argument.hatch_size)])
else:
lg1.append([(x, y + argument.hatch_size / 4), (x + argument.hatch_size, y + argument.hatch_size / 4)])
lg1.append([(x, y + argument.hatch_size / 2 + argument.hatch_size / 4),
(x + argument.hatch_size, y + argument.hatch_size / 2 + argument.hatch_size / 4)])
lg2.append([(x + argument.hatch_size, y), (x, y + argument.hatch_size)])
lines = [lg1, lg2]
for k in range(0, len(lines)):
for i in range(0, len(lines[k])):
for j in range(0, len(lines[k])):
if lines[k][i] != [] and lines[k][j] != []:
if lines[k][i][-1] == lines[k][j][0]:
lines[k][i] = lines[k][i] + lines[k][j][1:]
lines[k][j] = []
lines[k] = [l for l in lines[k] if len(l) > 0]
lines = lines[0] + lines[1]
for i in range(0, len(lines)):
for j in range(0, len(lines[i])):
lines[i][j] = int(lines[i][j][0] + argument.hatch_size * perlin.noise(i * 0.5, j * 0.1, 1)), int(
lines[i][j][1] + argument.hatch_size * perlin.noise(i * 0.5, j * 0.1, 2)) - j
return lines


def make_svg(lines):
print("Generating SVG file....")
out = '<svg xmlns="http://www.w3.org/2000/svg" version="1.1">'
for l in lines:
l = ",".join([str(p[0] * 0.5) + "," + str(p[1] * 0.5) for p in l])
out += '<polyline points="' + l + '" stroke="black" stroke-width="2" fill="none" />\n'
out += '</svg>'
return out


def rasterise_image(svg_image, raster_image):
print("Converting image....")
# to be implemented
File renamed without changes.
6 changes: 3 additions & 3 deletions strokesort.py → line_draw/strokesort.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from random import *
from PIL import Image, ImageDraw, ImageOps
from util import *
from line_draw.util import *


def sortlines(lines):
Expand Down Expand Up @@ -38,8 +38,8 @@ def visualize(lines):
turtle.mainloop()

if __name__=="__main__":
import linedraw
import line_draw
#linedraw.draw_hatch = False
lines = linedraw.sketch("Lenna")
lines = line_draw.sketch("Lenna")
#lines = sortlines(lines)
visualize(lines)
22 changes: 22 additions & 0 deletions line_draw/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import os
def midpt(*args):
xs,ys = 0,0
for p in args:
xs += p[0]
ys += p[1]
return xs/len(args),ys/len(args)

def distsum(*args):
return sum([ ((args[i][0]-args[i-1][0])**2 + (args[i][1]-args[i-1][1])**2)**0.5 for i in range(1,len(args))])


def is_image_file(file_path):
image_extensions = ['.jpg', '.jpeg', '.png']
tmp,ext = extract_file_name_and_extension(file_path)
return ext in image_extensions


def extract_file_name_and_extension(file_path):
file_name_with_extension = os.path.basename(file_path)
file_name, file_extension = os.path.splitext(file_name_with_extension)
return file_name, file_extension
Loading