├── .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 | Logo pythosaic 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 | --------------------------------------------------------------------------------