├── .gitignore
├── README.md
├── logo.png
└── photomosaic
├── main.py
└── pythomosaic
├── Bucket.py
├── BucketsHandler.py
├── Element.py
├── ImageLoader.py
├── MosaicMakerStyle1.py
├── MosaicMakerStyle2.py
├── PostProcess.py
├── PreProcess.py
├── __init__.py
├── constants.py
└── utils.py
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | *.py[cod]
3 | *$py.class
4 |
5 | # C extensions
6 | *.so
7 |
8 | # Distribution / packaging
9 | .Python
10 | build/
11 | develop-eggs/
12 | dist/
13 | downloads/
14 | eggs/
15 | .eggs/
16 | lib/
17 | lib64/
18 | parts/
19 | sdist/
20 | var/
21 | wheels/
22 | share/python-wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .nox/
42 | .coverage
43 | .coverage.*
44 | .cache
45 | nosetests.xml
46 | coverage.xml
47 | *.cover
48 | *.py,cover
49 | .hypothesis/
50 | .pytest_cache/
51 | cover/
52 |
53 | # Translations
54 | *.mo
55 | *.pot
56 |
57 | # Django stuff:
58 | *.log
59 | local_settings.py
60 | db.sqlite3
61 | db.sqlite3-journal
62 |
63 | # Flask stuff:
64 | instance/
65 | .webassets-cache
66 |
67 | # Scrapy stuff:
68 | .scrapy
69 |
70 | # Sphinx documentation
71 | docs/_build/
72 |
73 | # PyBuilder
74 | .pybuilder/
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | # For a library or package, you might want to ignore these files since the code is
86 | # intended to run in multiple environments; otherwise, check them in:
87 | # .python-version
88 |
89 | # pipenv
90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
93 | # install all needed dependencies.
94 | #Pipfile.lock
95 |
96 | # poetry
97 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
98 | # This is especially recommended for binary packages to ensure reproducibility, and is more
99 | # commonly ignored for libraries.
100 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
101 | #poetry.lock
102 |
103 | # pdm
104 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
105 | #pdm.lock
106 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
107 | # in version control.
108 | # https://pdm.fming.dev/#use-with-ide
109 | .pdm.toml
110 |
111 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
112 | __pypackages__/
113 |
114 | # Celery stuff
115 | celerybeat-schedule
116 | celerybeat.pid
117 |
118 | # SageMath parsed files
119 | *.sage.py
120 |
121 | # Environments
122 | .env
123 | .venv
124 | env/
125 | venv/
126 | ENV/
127 | env.bak/
128 | venv.bak/
129 |
130 | # Spyder project settings
131 | .spyderproject
132 | .spyproject
133 |
134 | # Rope project settings
135 | .ropeproject
136 |
137 | # mkdocs documentation
138 | /site
139 |
140 | # mypy
141 | .mypy_cache/
142 | .dmypy.json
143 | dmypy.json
144 |
145 | # Pyre type checker
146 | .pyre/
147 |
148 | # pytype static type analyzer
149 | .pytype/
150 |
151 | # Cython debug symbols
152 | cython_debug/
153 |
154 | # PyCharm
155 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
156 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
157 | # and can be added to the global gitignore or merged into this file. For a more nuclear
158 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
159 | #.idea/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Pythosaic
6 |
7 |
8 | # Table of Contents
9 | - [About the Project](#about-the-project)
10 | * [Context](#context)
11 | * [Made with](#made-with)
12 | - [Examples](#examples)
13 | - [Getting Started](#getting-started)
14 | * [Prerequisites](#prerequisites)
15 | * [Installation](#installation)
16 | - [Usage](#usage)
17 | * [Commands](#commands)
18 | * [Use the editor](#use-the-editor)
19 | - [License](#license)
20 | - [Author](#author)
21 |
22 |
23 | # About the project
24 |
25 | ## Context
26 |
27 |
28 | ## Made with
29 |
30 | # Examples
31 |
32 |
33 | # Getting Started
34 |
35 | ## Prerequisites
36 |
37 | ## Installation
38 |
39 | # Usage
40 |
41 | ## Commands
42 |
43 | ## Use the editor
44 |
45 | # License
46 |
47 | # Author
48 |
49 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HugoM25/Pythosaic/2723ae95a25ca9e3a70c6b2f54743493029989a5/logo.png
--------------------------------------------------------------------------------
/photomosaic/main.py:
--------------------------------------------------------------------------------
1 | import pythomosaic as pm
2 |
3 | import cv2
4 | import glob
5 | import time
6 | import numpy as np
7 |
8 |
9 | def style1():
10 | time_process = time.time()
11 |
12 | # Load data
13 | loader = pm.ImageLoader()
14 | # loader.load_tileset_image('assets/dataset_emoji.png', 66)
15 | loader.load_folder_images('assets/Cats/')
16 |
17 | print('Loading time: ', time.time() - time_process)
18 |
19 | # Prepare data
20 | time_process = time.time()
21 | maker = pm.Maker(loader)
22 |
23 | print('Prepare time : ', time.time() - time_process)
24 |
25 | # Load image to mosaic
26 | image = cv2.imread('target/logo.png', cv2.IMREAD_UNCHANGED)
27 | image = cv2.resize(image, (30, 30))
28 |
29 | time_process = time.time()
30 |
31 | # Mosaic image
32 | image = maker.make(image)
33 |
34 | print('Time process: ', time.time() - time_process)
35 |
36 | # Save the result
37 | cv2.imwrite('result/result.png', image)
38 |
39 |
40 | def style2():
41 | # Load the image
42 | image = cv2.imread("target/bulb.png", cv2.IMREAD_UNCHANGED)
43 | image = cv2.cvtColor(image, cv2.COLOR_BGR2BGRA)
44 | image = cv2.resize(image, (3000, 3000))
45 |
46 | # Load data
47 | loader = pm.ImageLoader()
48 | loader.load_folder_images('assets/pokemon-cards/')
49 | # loader.load_tileset_image('assets/dataset_pokemon.png', 80)
50 | # Prepare data
51 | maker = pm.MakerStyle2(loader)
52 |
53 | time_process = time.time()
54 | # Mosaic image
55 | processed_img = maker.make(image)
56 |
57 | print('Time process: ', time.time() - time_process)
58 |
59 | # Save the result
60 | cv2.imwrite('result/result_test.png', processed_img)
61 |
62 |
63 | if __name__ == "__main__":
64 |
65 | style2()
66 |
--------------------------------------------------------------------------------
/photomosaic/pythomosaic/Bucket.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 | import random
3 | from . import utils
4 | from . import Element
5 |
6 |
7 | class Bucket:
8 | def __init__(self, elements=[]):
9 | self.elements = elements
10 | self.average_color = self.calculate_average_color()
11 |
12 | def reset_count(self) -> None:
13 | """
14 | Reset the use count of all the elements in the bucket
15 | """
16 | for element in self.elements:
17 | element.use_count = 0
18 |
19 | def calculate_average_color(self) -> tuple:
20 | """
21 | Calculate the average color of the elements in the bucket
22 | :return: the average color
23 | """
24 | if len(self.elements) == 0:
25 | return (0, 0, 0, 1)
26 |
27 | average_color = [0, 0, 0, 1]
28 | for element in self.elements:
29 | average_color[0] += element.color[0]
30 | average_color[1] += element.color[1]
31 | average_color[2] += element.color[2]
32 |
33 | average_color[0] /= len(self.elements)
34 | average_color[1] /= len(self.elements)
35 | average_color[2] /= len(self.elements)
36 |
37 | return average_color
38 |
39 | def add_element(self, element: Element) -> None:
40 | """
41 | Add an element to the bucket and update the average color
42 | :param element: the element to add
43 | """
44 | self.elements.append(element)
45 | self.average_color = self.calculate_average_color()
46 |
47 | def get_element(self, method: str = "random", color: tuple = None) -> Element:
48 | """
49 | Get an element from the bucket
50 | :param method: "random", "least_used", "closest"
51 | :param color: the color to compare to
52 | :return: an element
53 | """
54 | if method == "least_used":
55 | return self.get_least_used_element()
56 | elif method == "closest" and color is not None:
57 | return self.get_closest_element(color)
58 | else:
59 | return self.get_random_element()
60 |
61 | def get_random_element(self) -> Element:
62 | """
63 | Get a random element from the bucket
64 | :return: an element
65 | """
66 | return random.choice(self.elements)
67 |
68 | def get_least_used_element(self) -> Element:
69 | """
70 | Get the element that has been used the least and update its use count
71 | :return: an element
72 | """
73 | element = min(self.elements, key=lambda x: x.use_count)
74 | element.use_count += 1
75 | return element
76 |
77 | def get_closest_element(self, color: tuple) -> Element:
78 | """
79 | Get the element that is closest to the given color
80 | :param color: the color to compare to
81 | :return: an element
82 | """
83 | return min(self.elements, key=lambda x: utils.euclidean_distance(x.color, color))
84 |
85 | def __str__(self) -> str:
86 | return "Bucket: " + str(self.average_color) + " " + str(len(self.elements))
87 |
88 | def __repr__(self) -> str:
89 | return str(self)
90 |
--------------------------------------------------------------------------------
/photomosaic/pythomosaic/BucketsHandler.py:
--------------------------------------------------------------------------------
1 | from .constants import DIST_MAX_COLOR_BUCKET
2 | from .utils import euclidean_distance
3 | from .Bucket import Bucket
4 | from .Element import Element
5 |
6 | import numpy as np
7 |
8 |
9 | class BucketsHandler:
10 | def __init__(self, buckets: list[Bucket] = None) -> None:
11 | self.buckets = buckets if buckets is not None else []
12 |
13 | self.buckets_average_colors = None
14 |
15 | def empty_buckets(self) -> None:
16 | self.buckets = []
17 |
18 | def elements_to_buckets(self, elements: list[Element]) -> None:
19 | """
20 | Sort the elements into buckets
21 | :param elements: the list of elements
22 | """
23 | for element in elements:
24 | # if the element is black only skip it
25 | if element.color[0] == 0 and element.color[1] == 0 and element.color[2] == 0:
26 | continue
27 |
28 | # if there are no buckets create one and add the element
29 | if len(self.buckets) == 0:
30 | bucket = Bucket([element])
31 | self.buckets.append(bucket)
32 | continue
33 |
34 | # if there are buckets find the closest one
35 | min_dist = 10000
36 | bucket_target = None
37 |
38 | for bucket in self.buckets:
39 | dist = euclidean_distance(
40 | bucket.average_color,
41 | element.color
42 | )
43 | if dist < min_dist:
44 | bucket_target = bucket
45 | min_dist = dist
46 |
47 | # check if the closest bucket is close enough to add the element else create a new bucket
48 | if min_dist < DIST_MAX_COLOR_BUCKET:
49 | bucket_target.add_element(element)
50 | else:
51 | bucket = Bucket([element])
52 | self.buckets.append(bucket)
53 |
54 | # When done put the buckets color in a numpy array
55 | self.buckets_average_colors = np.array(
56 | [bucket.average_color for bucket in self.buckets])
57 |
--------------------------------------------------------------------------------
/photomosaic/pythomosaic/Element.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 |
4 | class Element:
5 | def __init__(self, image: np, size) -> None:
6 | self.image = image
7 | self.color = self.compute_mean_color(image)
8 | self.size = size
9 | self.use_count = 0
10 |
11 | def compute_mean_color(self, image: np.ndarray) -> tuple:
12 | """
13 | Compute the mean color of an image
14 | :param image: the image
15 | :return: the mean color
16 | """
17 | mask = image[:, :, 3] != 0
18 | image_masked = image[mask]
19 |
20 | # Check if image_masked is empty
21 | if image_masked.size == 0:
22 | return (0, 0, 0)
23 |
24 | color = np.mean(image_masked, axis=0)
25 | return tuple(color[:3])
26 |
27 | def __str__(self) -> str:
28 | return "Element: " + str(self.color) + " " + str(self.size)
29 |
30 | def __repr__(self) -> str:
31 | return str(self)
32 |
--------------------------------------------------------------------------------
/photomosaic/pythomosaic/ImageLoader.py:
--------------------------------------------------------------------------------
1 | import cv2
2 | import numpy as np
3 | import glob
4 |
5 | from .Element import Element
6 |
7 |
8 | class ImageLoader:
9 |
10 | def __init__(self) -> None:
11 | self.elements = []
12 |
13 | def load_tileset_image(self, tileset_path: str, tile_size: int) -> None:
14 | """
15 | Load a tileset image and split it into tiles before converting them to elements
16 | :param tileset_path: the path of the tileset image
17 | :param tile_size: the size of the tiles
18 | :return: None
19 | """
20 | self.elements = []
21 |
22 | tileset_img = cv2.imread(tileset_path, cv2.IMREAD_UNCHANGED)
23 | tileset_img = cv2.cvtColor(tileset_img, cv2.COLOR_BGR2BGRA)
24 |
25 | for row in range(0, tileset_img.shape[0], tile_size):
26 | for col in range(0, tileset_img.shape[1], tile_size):
27 | image = tileset_img[row:row + tile_size, col:col + tile_size]
28 |
29 | self.elements.append(self.image_to_element(image))
30 |
31 | def load_folder_images(self, path: str) -> None:
32 | """
33 | Load all images from a folder and convert them to elements
34 | :param path: the path of the folder
35 | :return: None
36 | """
37 | self.elements = []
38 |
39 | files_path = glob.glob(path + '*[.jpg, .png, .jpeg]')
40 |
41 | for file_path in files_path:
42 | image = cv2.imread(file_path, cv2.IMREAD_UNCHANGED)
43 | image = cv2.cvtColor(image, cv2.COLOR_BGR2BGRA)
44 |
45 | self.elements.append(self.image_to_element(image))
46 |
47 | def image_to_element(self, image: np.ndarray) -> Element:
48 | """
49 | Convert an image to an element
50 | :param image: the image
51 | :return: the element
52 | """
53 | return Element(image, image.shape)
54 |
--------------------------------------------------------------------------------
/photomosaic/pythomosaic/MosaicMakerStyle1.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 | import cv2
3 | import numpy as np
4 | from .ImageLoader import ImageLoader
5 | from .BucketsHandler import BucketsHandler
6 |
7 |
8 | class MosaicMakerStyle1:
9 | def __init__(self, image_loader: ImageLoader, bucket_pick_method: str = "random") -> None:
10 | self.image_loader = image_loader
11 | # Load elements in buckets
12 | self.buckets_handler = BucketsHandler()
13 | self.buckets_handler.elements_to_buckets(self.image_loader.elements)
14 |
15 | self.bucket_pick_method = bucket_pick_method
16 |
17 | def build_construction_matrix(self, image: np.ndarray) -> np.ndarray:
18 | """
19 | Build the construction matrix
20 | :param image: the image to build the matrix from
21 | :return: the construction matrix
22 | """
23 | # get the average color of each bucket
24 | bucket_colors = np.array(
25 | [bucket.average_color for bucket in self.buckets_handler.buckets])
26 |
27 | # calculate the distance between each bucket color and each pixel in the image
28 | dist_result = np.sqrt(
29 | ((bucket_colors[:, np.newaxis, np.newaxis, :] - image) ** 2).sum(axis=3))
30 |
31 | # get the index of the minimum distance for each pixel
32 | closest_bucket_indices = np.argmin(dist_result, axis=0)
33 |
34 | # reshape the matrix to the size of the image
35 | matrix = closest_bucket_indices.reshape(
36 | (image.shape[0], image.shape[1]))
37 |
38 | # if the pixel is transparent, we set the value to -1
39 | # activate this line if you want to keep the transparency
40 | matrix = np.where(image[:, :, 3] == 0, -1, matrix)
41 |
42 | return matrix
43 |
44 | def build_img_from_matrix(self, matrix: np.ndarray, image: np.ndarray, size_img: tuple) -> np.ndarray:
45 | """
46 | Build the final image from the construction matrix
47 | :param matrix: the construction matrix
48 | :param buckets: the list of buckets
49 | :param size_img: the size of the image
50 | :return: the final image
51 | """
52 | # Calculating the size of the final image
53 | height, width = matrix.shape
54 | height_final = height * size_img[0]
55 | width_final = width * size_img[1]
56 |
57 | final_image_result = np.zeros(
58 | (height_final, width_final, 4), dtype=np.uint8)
59 |
60 | # Building the final image
61 | for i in range(height):
62 | for j in range(width):
63 | # If the pixel is transparent, we skip it
64 | if matrix[i, j] == -1:
65 | continue
66 | bucket = self.buckets_handler.buckets[matrix[i, j]]
67 | element = bucket.get_element(
68 | method="random", color=image[i, j])
69 | final_image_result[i*size_img[0]:(i+1)*size_img[0],
70 | j*size_img[1]:(j+1)*size_img[1], :] = cv2.resize(element.image, size_img)
71 | return final_image_result
72 |
73 | def make(self, image: np.ndarray) -> np.ndarray:
74 | """
75 | Make the photomosaic
76 | :param image: the image to make the photomosaic from
77 | :return: the final image
78 | """
79 | matrix = self.build_construction_matrix(image)
80 | img_result = self.build_img_from_matrix(
81 | matrix,
82 | image,
83 | (30, 30)
84 | )
85 | return img_result
86 |
--------------------------------------------------------------------------------
/photomosaic/pythomosaic/MosaicMakerStyle2.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import cv2
3 |
4 | from .ImageLoader import ImageLoader
5 | from .BucketsHandler import BucketsHandler
6 |
7 |
8 | MIN_TILE_SIZE = 100
9 | MIN_VARIATION = 5
10 | TILE_NB = 10
11 |
12 | MIN_DEVIANCE = 10
13 | MAX_SPLIT = 2
14 |
15 |
16 | class MakerStyle2:
17 | def __init__(self, image_loader: ImageLoader, bucket_pick_method: str = "random") -> None:
18 | self.image_loader = image_loader
19 | # Load elements in buckets
20 | self.buckets_handler = BucketsHandler()
21 | self.buckets_handler.elements_to_buckets(self.image_loader.elements)
22 | self.bucket_pick_method = bucket_pick_method
23 |
24 | def make(self, image: np.ndarray):
25 | # Split the image in n squares
26 | n = TILE_NB
27 | height, width, _ = image.shape
28 | height_square = height//n
29 | width_square = width//n
30 |
31 | # Apply the recursive part fill on each square
32 | for i in range(n):
33 | for j in range(n):
34 | image[i*height_square:(i+1)*height_square, j*width_square:(j+1)*width_square] = self.recursive_part_fill(
35 | image[i*height_square:(i+1)*height_square, j*width_square:(j+1)*width_square])
36 |
37 | return image
38 |
39 | def compute_mean_color(self, image: np.ndarray) -> tuple:
40 | """
41 | Compute the mean color of an image
42 | :param image: the image
43 | :return: the mean color
44 | """
45 | mask = image[:, :, 3] != 0
46 | image_masked = image[mask]
47 |
48 | # Check if image_masked is empty
49 | if image_masked.size == 0:
50 | return (0, 0, 0, 0)
51 |
52 | color = np.mean(image_masked, axis=0)
53 | return tuple(color)
54 |
55 | def compute_standard_deviation_val(self, pixel_array: np.ndarray) -> float:
56 | """
57 | Compute the standard deviation of the color of an image
58 | :param image: the image
59 | :return: the standard deviation of the color
60 | """
61 | mask = pixel_array[:, :, 3] != 0
62 |
63 | image_masked = pixel_array[mask]
64 |
65 | # Check if image_masked is empty
66 | if image_masked.shape[0] == 0:
67 | return -1
68 |
69 | color = np.std(image_masked, axis=0)
70 | deviance = np.mean(color)
71 |
72 | return deviance
73 |
74 | def is_there_too_much_transparent(self, image_part: np.ndarray) -> bool:
75 | """
76 | Check if there is too much transparent pixels in an image
77 | :param image_part: the image
78 | :return: True if there is too much transparent pixels, False otherwise
79 | """
80 | # Calculate the proportion of non fully transparent pixels
81 | mask = image_part[:, :, 3] != 0
82 | image_masked = image_part[mask]
83 |
84 | # Check if image_masked is empty
85 | if image_masked.shape[0] == 0:
86 | return False
87 |
88 | proportion = image_masked.shape[0] / \
89 | (image_part.shape[0]*image_part.shape[1])
90 |
91 | # If the proportion is too low, we split the image
92 | if proportion < 0.5:
93 | return True
94 | else:
95 | return False
96 |
97 | def recursive_part_fill(self, image_part, depth=0):
98 | height, width, _ = image_part.shape
99 |
100 | deviation_image = self.compute_standard_deviation_val(image_part)
101 | is_mostly_transparent = self.is_there_too_much_transparent(image_part)
102 |
103 | if depth >= MAX_SPLIT or (deviation_image < MIN_DEVIANCE and is_mostly_transparent == False):
104 | # Find the closest bucket to the color of the image part
105 | needed_color = self.compute_mean_color(image_part)
106 |
107 | # Add theses lines to keep the background transparent
108 | if needed_color[3] < 1:
109 | return np.array([[[0, 0, 0, 0]]*width]*height)
110 |
111 | # Calculate the dist of every buckets to the color of the image part
112 | dists = np.sum(
113 | np.square(self.buckets_handler.buckets_average_colors - needed_color), axis=1)
114 | # Get the index of the closest bucket
115 | closest_bucket_index = np.argmin(dists)
116 | # Get the closest bucket
117 | closest_bucket = self.buckets_handler.buckets[closest_bucket_index]
118 | # Get a random element from the closest bucket
119 | image = closest_bucket.get_element(
120 | method="random", color=needed_color).image
121 |
122 | return cv2.resize(image, (width, height))
123 | else:
124 | for i in range(2):
125 | for j in range(2):
126 | image_part[i*height//2:(i+1)*height//2, j*width//2:(j+1)*width//2] = self.recursive_part_fill(
127 | image_part[i*height//2:(i+1)*height//2, j*width//2:(j+1)*width//2], depth=depth+1)
128 | return image_part
129 |
--------------------------------------------------------------------------------
/photomosaic/pythomosaic/PostProcess.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import cv2
3 | # Description: PostProcess class used to post process the mosaic image after building it
4 |
5 |
6 | class PostProcess:
7 | def __init__(self) -> None:
8 | pass
9 |
10 | def overlay_model_mosaic(self, image_model, image_mosaic) -> np.ndarray:
11 | """
12 | Overlay the model image on the mosaic image to get better colors
13 | :param image_model: the model image
14 | :param image_mosaic: the mosaic image
15 | :return: the final image
16 | """
17 | final_image = None
18 | return final_image
19 |
20 | def blend_normal(self):
21 | pass
22 |
--------------------------------------------------------------------------------
/photomosaic/pythomosaic/PreProcess.py:
--------------------------------------------------------------------------------
1 | class PreProcess:
2 | def __init__(self) -> None:
3 | pass
4 |
5 | def dither_image(self, image: np.ndarray) -> np.ndarray:
6 | """
7 | Dither the image
8 | :param image: the image
9 | :return: the dithered image
10 | """
11 | dithered_image = None
12 | return dithered_image
13 |
--------------------------------------------------------------------------------
/photomosaic/pythomosaic/__init__.py:
--------------------------------------------------------------------------------
1 | from .BucketsHandler import *
2 | from .ImageLoader import *
3 | from .Bucket import *
4 | from .Element import *
5 | from .utils import *
6 | from .constants import *
7 | from .MosaicMakerStyle2 import *
8 | from .MosaicMakerStyle1 import *
9 |
--------------------------------------------------------------------------------
/photomosaic/pythomosaic/constants.py:
--------------------------------------------------------------------------------
1 | DIST_MAX_COLOR_BUCKET = 20
2 |
--------------------------------------------------------------------------------
/photomosaic/pythomosaic/utils.py:
--------------------------------------------------------------------------------
1 | def euclidean_distance(color1: tuple, color2: tuple) -> float:
2 | return ((color1[0] - color2[0]) ** 2 + (color1[1] - color2[1]) ** 2 + (color1[2] - color2[2]) ** 2) ** 0.5
3 |
--------------------------------------------------------------------------------