diff --git a/trivector/__init__.py b/trivector/__init__.py index f7e6c38..966c902 100644 --- a/trivector/__init__.py +++ b/trivector/__init__.py @@ -11,7 +11,4 @@ + :mod:`.trivector` - Image conversion functionality for trivector """ -from trivector.trivector import trivector - - __version__ = (1, 1, 1) diff --git a/trivector/__main__.py b/trivector/__main__.py index febdc0d..0360fce 100644 --- a/trivector/__main__.py +++ b/trivector/__main__.py @@ -6,7 +6,7 @@ import argparse import sys -from trivector.trivector import trivector, DiagonalStyle +from trivector.vectorizer import TriVectorizer, DiagonalStyle def get_parser(): @@ -24,7 +24,7 @@ def get_parser(): help="size in pixels for each triangle sector") group.add_argument("-d", "--diagonal-style", dest="diagonal_style", type=DiagonalStyle, choices=list(DiagonalStyle), - default=DiagonalStyle.alternating.value, + default=DiagonalStyle.left_alternating.value, help="diagonal arrangement of the triangle sectors") return parser @@ -34,13 +34,14 @@ def main(argv=sys.argv[1:]): parser = get_parser() args = parser.parse_args(argv) - trivector( + tri_vectorizer = TriVectorizer( image_path=args.image, - cut_size=args.sector_size, - output_path=args.output, - diagonal_style=args.diagonal_style + diagonal_style=args.diagonal_style, + sector_size=args.sector_size ) + with open(args.output, "w") as f: + tri_vectorizer.vectorize().write(f) return 0 diff --git a/trivector/trivector.py b/trivector/trivector.py deleted file mode 100644 index 4ae67f2..0000000 --- a/trivector/trivector.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""Image conversion functionality for trivector""" - -from enum import Enum - -import numpy as np -import svgwrite - -import cv2 -import progressbar - - -def upper_tri_sum(d3array: np.ndarray) -> np.ndarray: - """Get a 3D image array's upper diagonal's pixel color average - - :param d3array: 3D image array derived from :func:`cv2.imread` - - Treat the 3D array as 2d array. Having the innermost array (pixel BGR - values) be considered base values to be averaged. - - :return: BGR array of the average color of the upper diagonal of the - 3D image array - """ - x, y, _ = d3array.shape - tri = [] - for i in range(x): - if i > y: - break - for j in range(y - i): - tri.append(d3array[i][i + j]) - return np.sum(tri, axis=0) // len(tri) - - -def lower_tri_sum(d3array: np.ndarray) -> np.ndarray: - """Get a 3D image array's lower diagonal's pixel color average - - :param d3array: 3D image array derived from :func:`cv2.imread` - - Treat the 3D array as 2d array. Having the innermost array (pixel BGR - values) be considered base values to be averaged. - - .. note:: - - If the lower diagonal cannot be computed (eg: flat/malformed 3D array) - use the 3D image array's upper diagonal's pixel color average instead. - - :return: BGR array of the average color of the lower diagonal of the - 3D image array - """ - x, y, _ = d3array.shape - tri = [] - for i in range(x): - if i > y: - break - for j in range(i): - tri.append(d3array[i][j]) - - # if bottom tri is empty use the upper tri's sum - if not tri: - return upper_tri_sum(d3array) - return np.sum(tri, axis=0) // len(tri) - - -def vectorize_sector_left(sub_img: np.ndarray, svg_drawing: svgwrite.Drawing, - x: int, y: int, cut_size: int): - """Add two triangles to ``svg_drawing`` whose colors are derived from - the color averages from the top and bottom diagonals of the 3D BGR image - array of the sub image""" - b, g, r = upper_tri_sum(sub_img) - svg_drawing.add( - svg_drawing.polygon( - [(x, y), (x + cut_size, y), (x + cut_size, y + cut_size)], - fill=svgwrite.rgb(r, g, b, "RGB") - ) - ) - b, g, r = lower_tri_sum(sub_img) - svg_drawing.add( - svg_drawing.polygon( - [(x, y), (x, y + cut_size), (x + cut_size, y + cut_size)], - fill=svgwrite.rgb(r, g, b, "RGB") - ) - ) - - -def vectorize_sector_right(sub_img: np.ndarray, svg_drawing: svgwrite.Drawing, - x: int, y: int, cut_size: int): - """Add two triangles to ``svg_drawing`` whose colors are derived from - the color averages from the top and bottom diagonals of the 3D BGR image - array of the sub image""" - b, g, r = upper_tri_sum(sub_img) - svg_drawing.add( - svg_drawing.polygon( - [(x, y + cut_size), (x + cut_size, y + cut_size), (x + cut_size, y)], - fill=svgwrite.rgb(r, g, b, "RGB") - ) - ) - b, g, r = lower_tri_sum(sub_img) - svg_drawing.add( - svg_drawing.polygon( - [(x, y + cut_size), (x, y), (x + cut_size, y)], - fill=svgwrite.rgb(r, g, b, "RGB") - ) - ) - - -class DiagonalStyle(Enum): - """Styling options noting the diagonal arrangement of the - triangle sectors""" - right = "right" - left = "left" - alternating = "alternating" - - def __str__(self): - return self.value - - -def trivector(image_path: str, cut_size: int, output_path: str, - diagonal_style: DiagonalStyle = DiagonalStyle.alternating): - """Convert an image into a SVG vector image composed of triangular sectors - - :param image_path: path to the image to trivector - :param cut_size: size in pixels for each triangle sector - :param diagonal_style: diagonal arrangement of the triangle sectors - :param output_path: path to write the trivectored image - """ - image = cv2.imread(image_path) # pylint:disable=no-member - - height, width, _ = image.shape - - width_slices = range(0, width, cut_size) - height_slices = range(0, height, cut_size) - svg_drawing = svgwrite.Drawing( - output_path, - profile="full", - size=(len(width_slices)*cut_size, len(height_slices)*cut_size) - ) - - # start up the progress bar - # each image sector is one tick one the progress bar - bar = progressbar.ProgressBar(max_value=len(width_slices)*len(height_slices)) - counter_2 = 0 - sector_num = 0 - for y in height_slices: - counter_1 = counter_2 - counter_2 += 1 - for x in width_slices: - sector_image = image[y:y + cut_size, x:x + cut_size] - if (diagonal_style == DiagonalStyle.left) or \ - (diagonal_style == DiagonalStyle.alternating and counter_1 % 2): - vectorize_sector_left(sector_image, svg_drawing, x, y, cut_size) - else: - sector_image = np.rot90(sector_image, axes=(0, 1)) - vectorize_sector_right(sector_image, svg_drawing, x, y, cut_size) - sector_num += 1 - counter_1 += 1 - bar.update(sector_num) - - svg_drawing.save() diff --git a/trivector/vectorizer.py b/trivector/vectorizer.py new file mode 100644 index 0000000..7b64c30 --- /dev/null +++ b/trivector/vectorizer.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Image conversion functionality for trivector""" + +from enum import Enum +from typing import Generator, Tuple + +import numpy as np +import svgwrite + +import cv2 +from svgwrite.shapes import Rect, Circle + + +class Vectorizer: + def __init__(self, image_path: str, sector_size: int, **kwargs): + self.image = cv2.cvtColor(cv2.imread(image_path), cv2.COLOR_BGR2RGB) # pylint: disable=no-member + self.sector_size = sector_size + + height, width, _ = self.image.shape + self.width_slices = list(range(0, width, self.sector_size)) + self.height_slices = list(range(0, height, self.sector_size)) + self.svg_drawing = svgwrite.Drawing( + profile="full", + size=(len(self.width_slices)*self.sector_size, + len(self.height_slices)*self.sector_size) + ) + + def vectorize(self) -> svgwrite.Drawing: + pass + + def get_sector(self, x: int, y: int) -> np.ndarray: + return self.image[y:y + self.sector_size, x:x + self.sector_size] + + @property + def sectors(self) -> Generator[Tuple[int, int, np.ndarray], None, None]: + for y in self.height_slices: + for x in self.width_slices: + yield x, y, self.get_sector(x, y) + + +def upper_tri_sum(d3array: np.ndarray) -> np.ndarray: + """Get a 3D image array's upper diagonal's pixel color average + + :param d3array: 3D image array derived from :func:`cv2.imread` + + Treat the 3D array as 2d array. Having the innermost array (pixel RGB + values) be considered base values to be averaged. + + :return: RGB array of the average color of the upper diagonal of the + 3D image array + """ + x, y, _ = d3array.shape + tri_elem = [] + for i in range(x): + if i > y: + break + for j in range(y - i): + tri_elem.append(d3array[i][i + j]) + return np.average(tri_elem, axis=0) + + +def lower_tri_sum(d3array: np.ndarray) -> np.ndarray: + """Get a 3D image array's lower diagonal's pixel color average + + :param d3array: 3D image array derived from :func:`cv2.imread` + + Treat the 3D array as 2d array. Having the innermost array (pixel RGB + values) be considered base values to be averaged. + + .. note:: + + If the lower diagonal cannot be computed (eg: flat/malformed 3D array) + use the 3D image array's upper diagonal's pixel color average instead. + + :return: RGB array of the average color of the lower diagonal of the + 3D image array + """ + x, y, _ = d3array.shape + tri_elem = [] + for i in range(x): + if i > y: + break + for j in range(i): + tri_elem.append(d3array[i][j]) + + # if bottom tri is empty use the upper tri's sum + if not tri_elem: + return upper_tri_sum(d3array) + return np.average(tri_elem, axis=0) + + +def vectorize_sector_left(sub_img: np.ndarray, + svg_drawing: svgwrite.Drawing, + x: int, y: int, sector_size: int): + """Add two triangles to ``svg_drawing`` whose colors are derived from + the color averages from the top and bottom diagonals of the 3D RGB image + array of the sub image""" + r, g, b = upper_tri_sum(sub_img) + svg_drawing.add( + svg_drawing.polygon( + [(x, y), (x + sector_size, y), (x + sector_size, y + sector_size)], + fill=svgwrite.rgb(r, g, b, "RGB") + ) + ) + r, g, b = lower_tri_sum(sub_img) + svg_drawing.add( + svg_drawing.polygon( + [(x, y), (x, y + sector_size), (x + sector_size, y + sector_size)], + fill=svgwrite.rgb(r, g, b, "RGB") + ) + ) + + +def vectorize_sector_right(sub_img: np.ndarray, + svg_drawing: svgwrite.Drawing, + x: int, y: int, sector_size: int): + """Add two triangles to ``svg_drawing`` whose colors are derived from + the color averages from the top and bottom diagonals of the 3D RGB image + array of the sub image""" + sub_img = np.rot90(sub_img, axes=(0, 1)) + r, g, b = upper_tri_sum(sub_img) + svg_drawing.add( + svg_drawing.polygon( + [(x, y + sector_size), (x + sector_size, y + sector_size), (x + sector_size, y)], + fill=svgwrite.rgb(r, g, b, "RGB") + ) + ) + r, g, b = lower_tri_sum(sub_img) + svg_drawing.add( + svg_drawing.polygon( + [(x, y + sector_size), (x, y), (x + sector_size, y)], + fill=svgwrite.rgb(r, g, b, "RGB") + ) + ) + + +class DiagonalStyle(Enum): + """Styling options noting the diagonal arrangement of the + triangle sectors""" + right = "right" + left = "left" + left_alternating = "left_alternating" + right_alternating = "right_alternating" + + def __str__(self): + return self.value + + +class TriVectorizer(Vectorizer): + def __init__(self, diagonal_style: DiagonalStyle = DiagonalStyle.left_alternating, **kwargs): + self.diagonal_style = diagonal_style + super().__init__(**kwargs) + + def vectorize(self) -> svgwrite.Drawing: + for x, y, sector_image in self.sectors: + x_idx = x // self.sector_size + y_idx = y // self.sector_size + # (self.diagonal_style == DiagonalStyle.left_alternating and (x/self.sector_size == 1 and not (y_idx) % 2) or (x/self.sector_size) % 2) or \ # wave + if self.diagonal_style == DiagonalStyle.right: + vectorize_sector_right(sector_image, self.svg_drawing, x, y, + self.sector_size) + elif (self.diagonal_style == DiagonalStyle.left) or \ + (self.diagonal_style == DiagonalStyle.right_alternating and + ((x_idx % 2 and not y_idx % 2) or + (not x_idx % 2 and y_idx % 2))) or \ + (self.diagonal_style == DiagonalStyle.left_alternating and + not ((x_idx % 2 and not y_idx % 2) or + (not x_idx % 2 and y_idx % 2))): + vectorize_sector_left(sector_image, self.svg_drawing, x, y, + self.sector_size) + else: + vectorize_sector_right(sector_image, self.svg_drawing, x, y, + self.sector_size) + return self.svg_drawing + + +def square_vectorize_sector(sector_image: np.ndarray, + svg_drawing: svgwrite.Drawing, + x: int, y: int, sector_size: int): + r, g, b = np.average(sector_image, (0, 1)) + svg_drawing.add( + Rect( + insert=(x, y), + size=(sector_size, sector_size), + fill=svgwrite.rgb(r, g, b, "RGB") + ) + ) + + +class SquareVectorizer(Vectorizer): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def vectorize(self) -> svgwrite.Drawing: + for x, y, sector_image in self.sectors: + square_vectorize_sector( + sector_image, self.svg_drawing, x, y, self.sector_size) + return self.svg_drawing + + +def circle_vectorize_sector(sector_image: np.ndarray, + svg_drawing: svgwrite.Drawing, + x: int, y: int, sector_size: int): + r, g, b = np.average(sector_image, (0, 1)) + svg_drawing.add( + Circle( + center=(x + (sector_size / 2), y + (sector_size / 2)), + r=sector_size / 1.3, + fill=svgwrite.rgb(r, g, b, "RGB") + ) + ) + + +class CircleVectorizer(Vectorizer): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def vectorize(self) -> svgwrite.Drawing: + for x, y, sector_image in self.sectors: + circle_vectorize_sector( + sector_image, self.svg_drawing, x, y, self.sector_size) + return self.svg_drawing