diff --git a/.circleci/config.yml b/.circleci/config.yml index 8485b6f..92e1102 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -120,10 +120,10 @@ jobs: visualize_dependency_graphs: executor: py38-docker-image environment: - NEURAL_STYLE_TRANSFER_DEPS_GRAPHS: dependencies-graphs + NST_DEPS_GRAPHS: dependency-graphs steps: - checkout - - run: sudo apt-get update -y + - run: sudo apt-get update -y --allow-releaseinfo-change - run: python -m pip install -U pip - run: name: Install the dot binary included in the graphviz package/distribution @@ -135,7 +135,7 @@ jobs: name: Visualize dependency graphs as .svg files command: tox -e graphs -vv - store_artifacts: - path: dependencies-graphs + path: dependency-graphs destination: dep-graphs - run: name: Visualize uml diagrams as .svg files @@ -164,13 +164,13 @@ workflows: filters: tags: only: /.*/ - # - visualize_dependency_graphs: - # filters: - # branches: - # only: - # - master - # - dev - # - release-staging + - visualize_dependency_graphs: + filters: + branches: + only: + - master + - dev + - release-staging # - build-documentation: # filters: # branches: diff --git a/README.rst b/README.rst index 190b723..ad6a93b 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,11 @@ Neural Style Transfer - CLI =========================== +Create artificial artwork by transfering the appearance of one image (eg a famous painting) to another +user-supplied image (eg your favourite photograph). + +Uses a Neural Style Transfer algorithm to transfer the appearance, which you can run though a CLI program. + `Neural Style Tranfer` (NST) is an algorithm that applies the `style` of an image to the `contents` of another and produces a `generated` image. The idea is to find out how someone, with the `painting style` shown in one image, would depict the `contents` shown in another image. @@ -17,6 +22,11 @@ This Python package runs a Neural Style Tranfer algorithm on input `content` and * - tests - | |circleci| |codecov| + * - package + - | |pypi| |wheel| |py_versions| |commits_since| + + * - containerization + - | |docker| |image_size| * - code quality - |better_code_hub| |code_climate| |maintainability| |codacy| |scrutinizer| @@ -40,9 +50,9 @@ Key features of the package: Installation ------------ -| The Neural Style Transfer - CLI heavely depends on Tensorflow (tf) and therefor it is crucial that tf is installed correctly in your Python environment. +| The Neural Style Transfer - CLI heavely depends on Tensorflow (tf) and therefore it is crucial that tf is installed correctly in your Python environment. -Sample commands to install NST CLI using a terminal: +Sample commands to install the NST CLI from source, using a terminal: :: @@ -60,6 +70,18 @@ Sample commands to install NST CLI using a terminal: # Install NST CLI (in virtual environment) pip install -e . + +Alternative command to install the NST CLI by downloading the `artificial_artwork` python package from pypi: + +:: + + pip install artificial_artwork + + +Make the cli available for your host system: + +:: + # Setup a symbolic link (in your host system) in a location in your PATH # Assuming ~/.local/bin is in your PATH ln -s $PWD/env/bin/neural-style-transfer ~/.local/bin/neural-style-transfer @@ -71,7 +93,7 @@ Sample commands to install NST CLI using a terminal: Usage ----- -Download the Vgg-Verydeep-19 pretrained `model` from https://mega.nz/file/i5xDWI4Y. +Download the Vgg-Verydeep-19 pretrained `model` from https://drive.protonmail.com/urls/7RXGN23ZRR#hsw4STil0Hgc. Exctract the model (weights and layer architecture). @@ -115,6 +137,21 @@ image generated on a different iteration while running the algorithm. The bigger Check out your artificial artwork! +Docker image +------------ + +We have included a docker file that we use to build an image where both the `artificial_artwork` package (source code) +and the pretrained model are present. That way you can immediately start creating artwork! + +:: + + docker pull boromir674/neural-style-transfer + + mkdir nst-output + + docker run -it --rm -v nst-output:/app/nst-output boromir674/neural-style-transfer + + .. |circleci| image:: https://img.shields.io/circleci/build/github/boromir674/neural-style-transfer/master?logo=circleci @@ -124,9 +161,28 @@ Check out your artificial artwork! .. |codecov| image:: https://codecov.io/gh/boromir674/neural-style-transfer/branch/master/graph/badge.svg?token=3POTVNU0L4 :alt: Codecov - :target: https://codecov.io/gh/boromir674/neural-style-transfer + :target: https://codecov.io/gh/boromir674/neural-style-transfer + +.. |pypi| image:: https://img.shields.io/pypi/v/artificial-artwork?color=blue&label=pypi&logo=pypi&logoColor=%23849ed9 + :alt: PyPI + :target: https://pypi.org/project/artificial-artwork/ + +.. |wheel| image:: https://img.shields.io/pypi/wheel/artificial-artwork?logo=python&logoColor=%23849ed9 + :alt: PyPI - Wheel + :target: https://pypi.org/project/artificial-artwork + +.. |py_versions| image:: https://img.shields.io/pypi/pyversions/artificial-artwork?color=blue&logo=python&logoColor=%23849ed9 + :alt: PyPI - Python Version + :target: https://pypi.org/project/artificial-artwork + +.. |commits_since| image:: https://img.shields.io/github/commits-since/boromir674/neural-style-transfer/v0.5/master?color=blue&logo=Github + :alt: GitHub commits since tagged version (branch) + :target: https://github.com/boromir674/neural-style-transfer/compare/v0.5..master + + + .. |better_code_hub| image:: https://bettercodehub.com/edge/badge/boromir674/neural-style-transfer?branch=master :alt: Better Code Hub :target: https://bettercodehub.com/ @@ -161,3 +217,11 @@ Check out your artificial artwork! :alt: Supported versions :target: https://pypi.org/project/topic-modeling-toolkit + + +.. |docker| image:: https://img.shields.io/docker/v/boromir674/neural-style-transfer/latest?logo=docker&logoColor=%23849ED9 + :alt: Docker Image Version (tag latest semver) + :target: https://hub.docker.com/r/boromir674/neural-style-transfer + +.. |image_size| image:: https://img.shields.io/docker/image-size/boromir674/neural-style-transfer/latest?logo=docker&logoColor=%23849ED9 + :alt: Docker Image Size (tag) \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 2d5d89c..ee468e8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,10 +1,8 @@ [metadata] ## Setuptools specific information name = artificial_artwork -version = 0.5 -description = Create artificial artwork by transfering the appearance of - one image (eg a famous painting) to another user-supplied image (eg your favourite photograph). - Uses a Neural Style Transfer algorithm to transfer the appearance which you can run though a CLI program. +version = 0.6 +description = Create artificial artwork by transfering the appearance of one image (eg a famous painting) to another user-supplied image (eg your favourite photograph). long_description = file: README.rst long_description_content_type = text/x-rst license = AGPLv3 diff --git a/src/artificial_artwork/algorithm.py b/src/artificial_artwork/algorithm.py index 7cb9862..68af6ca 100644 --- a/src/artificial_artwork/algorithm.py +++ b/src/artificial_artwork/algorithm.py @@ -1,43 +1,11 @@ -from abc import ABC import attr - -class AlgorithmInterface(ABC): - """An algorithm is a series of execution steps, aiming to solve a problem. - - This interface provides a 'run' method that sequentially runs the - algorithm's steps. - """ - def run(self, *args, **kwargs) -> None: - """Run the algorithm.""" - raise NotImplementedError - - -class IterativeAlgorithm(AlgorithmInterface): - - def run(self, *args, **kwargs) -> None: - pass - - -class LearningAlgorithm(IterativeAlgorithm): - - def run(self, *args, **kwargs) -> None: - pass - - def compute_cost(self, *args, **kwargs) -> float: - raise NotImplementedError +from .style_layer_selector import NSTLayersSelection @attr.s -class NSTAlgorithm(IterativeAlgorithm): +class NSTAlgorithm: parameters = attr.ib() - image_config = attr.ib() - - def run(self, *args, **kwargs) -> None: - return super().run(*args, **kwargs) - - -from .style_layer_selector import NSTLayersSelection @attr.s @@ -47,7 +15,6 @@ class AlogirthmParameters: # from the algo input (runtime objects that are the INPUT to the algo) content_image = attr.ib() style_image = attr.ib() - cv_model = attr.ib() style_layers = attr.ib(converter=NSTLayersSelection.from_tuples) termination_condition = attr.ib() output_path = attr.ib() diff --git a/src/artificial_artwork/algorithm_progress.py b/src/artificial_artwork/algorithm_progress.py deleted file mode 100644 index 641352b..0000000 --- a/src/artificial_artwork/algorithm_progress.py +++ /dev/null @@ -1,49 +0,0 @@ -import attr - - -@attr.s -class NSTAlgorithmProgress: - tracked_metrics: dict = attr.ib(converter=lambda x: {k: [v] for k, v in dict(x).items()}) - _callbacks = attr.ib(default=attr.Factory(lambda self: { - True: self._append, - False: self._set, - }, takes_self=True)) - - def update(self, *args, **kwargs): - metrics = args[0].state.metrics - for metric_key, value in metrics.items(): - self._callbacks[metric_key in self.tracked_metrics](metric_key, value) - - def _set(self, key, value): - self.tracked_metrics[key] = [value] - - def _append(self, key, value): - self.tracked_metrics[key].append(value) - - # Properties - - @property - def iterations(self): - """Iterations completed.""" - return self.tracked_metrics.get('iterations', [None])[-1] - - @property - def duration(self): - """Time in seconds the iterative algorithm has been running.""" - return self.tracked_metrics.get('duration', [None])[-1] - - @property - def cost_improvement(self): - """Difference of loss function between the last 2 measurements. - - Positive value indicates that the loss went down and that the learning - process moved towards the (local) minimum (in terms of minimizing the - loss/cost function). - - So roughly, positive values indicate improvement [moving towards (local) - minimum] and negative indicate moving away from minimum. - - Moving refers to the learning parameters. - """ - if 1 < len(self.tracked_metrics.get('cost', [])): - return self.tracked_metrics['cost'][-2] - self.tracked_metrics['cost'][-1] diff --git a/src/artificial_artwork/cli.py b/src/artificial_artwork/cli.py index 24300d9..cb71cbd 100644 --- a/src/artificial_artwork/cli.py +++ b/src/artificial_artwork/cli.py @@ -1,34 +1,12 @@ - -import os import click from .disk_operations import Disk from .styling_observer import StylingObserver from .algorithm import NSTAlgorithm, AlogirthmParameters from .nst_tf_algorithm import NSTAlgorithmRunner -from .image import ImageFactory, ImageProcessingConfig -from .algorithm_progress import NSTAlgorithmProgress from .termination_condition.termination_condition import TerminationConditionFacility from .termination_condition_adapter import TerminationConditionAdapterFactory - - -def get_vgg_verydeep_19_model(): - try: - return os.environ['AA_VGG_19'] - except KeyError: - file_path = os.path.join(os.getcwd(), 'imagenet-vgg-verydeep-19.mat') - if os.path.exists(file_path): - return file_path - file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'imagenet-vgg-verydeep-19.mat') - if os.path.exists(file_path): - return file_path - raise NoImageModelSpesifiedError('No pretrained image model found. ' - 'Please download it and set the AA_VGG_19 environment variable with the' - 'path where ou stored the model (*.mat file), to indicate to wher to ' - 'locate and load it') - - -class NoImageModelSpesifiedError(Exception): pass +from .nst_image import ImageManager @click.command() @@ -39,7 +17,6 @@ class NoImageModelSpesifiedError(Exception): pass @click.option('--location', '-l', type=str, default='.') def cli(content_image, style_image, interactive, iterations, location): - IMAGE_MODEL_PATH = get_vgg_verydeep_19_model() TERMINATION_CONDITION = 'max-iterations' STYLE_LAYERS = [ ('conv1_1', 0.2), @@ -49,42 +26,68 @@ def cli(content_image, style_image, interactive, iterations, location): ('conv5_1', 0.2), ] - image_factory = ImageFactory(Disk.load_image) - - # for now we have hardcoded the config to receive 300 x 400 images with 3 color channels - image_process_config = ImageProcessingConfig.from_image_dimensions() - - termination_condition = TerminationConditionFacility.create(TERMINATION_CONDITION, iterations) - termination_condition_adapter = TerminationConditionAdapterFactory.create(TERMINATION_CONDITION, termination_condition) + import numpy as np + from artificial_artwork.image.image_operations import noisy, reshape_image, subtract, convert_to_uint8 + + # todo dynamically find means + means = np.array([123.68, 116.779, 103.939]).reshape((1,1,1,3)) # means + + image_manager = ImageManager([ + lambda matrix: reshape_image(matrix, ((1,) + matrix.shape)), + lambda matrix: subtract(matrix, means), # input image must have 3 channels! + ]) + + # probably load each image in separate thread and then join + image_manager.load_from_disk(content_image, 'content') + image_manager.load_from_disk(style_image, 'style') + + if not image_manager.images_compatible: + print("Given CONTENT image '{content_image}' has 'height' x 'width' x " + f"'color_channels': {image_manager.content_image.matrix.shape}") + print("Given STYLE image '{style_image}' has 'height' x 'width' x " + f"'color_channels': {image_manager.style_image.matrix.shape}") + print('Expected to receive images (matrices) of identical shape') + print('Exiting..') + exit(1) + + # image_factory = ImageFactory(Disk.load_image) + # content_image = image_factory.from_disk(content_image, reshape_and_normalize_pipeline) + # style_image = image_factory.from_disk(style_image, reshape_and_normalize_pipeline) + + termination_condition = TerminationConditionFacility.create( + TERMINATION_CONDITION, iterations) + termination_condition_adapter = TerminationConditionAdapterFactory.create( + TERMINATION_CONDITION, termination_condition) print(f' -- Termination Condition: {termination_condition}') algorithm_parameters = AlogirthmParameters( - image_factory.from_disk(content_image), - image_factory.from_disk(style_image), - IMAGE_MODEL_PATH, + image_manager.content_image, + image_manager.style_image, STYLE_LAYERS, termination_condition_adapter, location, ) - algorithm = NSTAlgorithm(algorithm_parameters, image_process_config) + algorithm = NSTAlgorithm(algorithm_parameters) - algorithm_runner = NSTAlgorithmRunner.default(algorithm, image_factory.image_processor.noisy) + # algorithm_runner = NSTAlgorithmRunner.default( + # algorithm, + # image_factory.image_processor.noisy + # ) + + noisy_ratio = 0.6 # ratio + + # NEW + algorithm_runner = NSTAlgorithmRunner.default( + algorithm, + lambda matrix: noisy(matrix, noisy_ratio) + ) - algorithm_progress = NSTAlgorithmProgress({}) - styling_observer = StylingObserver(Disk.save_image) - algorithm_runner.progress_subject.add( - algorithm_progress, termination_condition_adapter, ) algorithm_runner.peristance_subject.add( - styling_observer + StylingObserver(Disk.save_image, convert_to_uint8) ) - algorithm_runner.run() - - -if __name__ == '__main__': - cli() diff --git a/src/artificial_artwork/cost_computer.py b/src/artificial_artwork/cost_computer.py index ee1eae2..4e7c240 100644 --- a/src/artificial_artwork/cost_computer.py +++ b/src/artificial_artwork/cost_computer.py @@ -1,5 +1,5 @@ - import tensorflow as tf +from .nst_math import gram_matrix class NSTCostComputer: @@ -48,7 +48,7 @@ def compute(cls, a_C, a_G): image is forward propagated (passed through) in the network. 3. The above activations are a n_H x n_W x n_C tensor - OR Height x Width x Number_of_Channers + OR Height x Width x Number_of_Channels Pseudo code for latex expression of the mathematical equation: @@ -77,8 +77,6 @@ def compute(cls, a_C, a_G): return J_content -from .math import gram_matrix - class GramMatrixComputer(type): def __new__(mcs, *args, **kwargs): class_object = super().__new__(mcs, *args, **kwargs) diff --git a/src/artificial_artwork/image.py b/src/artificial_artwork/image.py deleted file mode 100644 index 6157e28..0000000 --- a/src/artificial_artwork/image.py +++ /dev/null @@ -1,144 +0,0 @@ -from abc import ABC, abstractproperty -from typing import List, Callable - -import attr -import numpy as np -from numpy.typing import NDArray - -ImageLoaderFunctionType = Callable[[str], NDArray] - - -# class CONFIG: -# IMAGE_WIDTH = 400 -# IMAGE_HEIGHT = 300 -# COLOR_CHANNELS = 3 -# NOISE_RATIO = 0.6 -# MEANS = np.array([123.68, 116.779, 103.939]).reshape((1,1,1,3)) - - - -@attr.s -class Image: - """An image loaded from disk, represented as a 2D mathematical matrix.""" - file_path: str = attr.ib() - matrix: NDArray = attr.ib(default=None) - - @classmethod - def load_matrix(cls, image_path: str, image_loader: ImageLoaderFunctionType): - return image_loader(image_path) - - -class ImageProcessingConfigInterface(ABC): - - @abstractproperty - def image_width(self) -> int: - raise NotImplementedError - - @abstractproperty - def image_height(self) -> int: - raise NotImplementedError - - @abstractproperty - def color_channels(self) -> int: - raise NotImplementedError - - @abstractproperty - def noise_ratio(self) -> float: - raise NotImplementedError - - @abstractproperty - def means(self) -> NDArray: - raise NotImplementedError - - -@attr.s -class ImageProcessingConfig(ImageProcessingConfigInterface): - _image_width = attr.ib() - _image_height = attr.ib() - _color_channels = attr.ib() - _noise_ratio = attr.ib() - _means = attr.ib() - - @property - def image_width(self) -> int: - return self._image_width - - @property - def image_height(self) -> int: - return self._image_height - - @property - def color_channels(self) -> int: - return self._color_channels - - @property - def noise_ratio(self) -> float: - return self._noise_ratio - - @property - def means(self) -> NDArray: - return self._means - - @classmethod - def from_image_dimensions(cls, width=400, height=300) -> ImageProcessingConfigInterface: - return ImageProcessingConfig( - width, - height, - 3, - 0.6, - np.array([123.68, 116.779, 103.939]).reshape((1,1,1,3)) - ) - - -@attr.s -class ImageProcessor: - config: ImageProcessingConfigInterface = attr.ib() - - def reshape_and_normalize_image(self, image: NDArray) -> NDArray: - """Reshape and normalize the input image (content or style)""" - # Reshape image to mach expected input of VGG16 - # print('DEBUG', 'Input Image Matrix SHAPE:', image.shape) - image = np.reshape(image, ((1,) + image.shape)) - # print('DEBUG', 'RESHAPED SHAPE:', image.shape) - # Substract the mean to match the expected input of VGG16 - # print('DUBEG', 'MEANS SHAPE', self.config.means.shape) - try: - return image - self.config.means - except ValueError as numpy_broadcast_error: - raise ConfigMeansShapeMissmatchError( - f'Image processor configured for {self.config.color_channels} ' - f'color channels, but input image has {image.shape[-1]}. Please' - 'consider changing the config.means array (see self.config) for' - ' this ImageProcessor.') from numpy_broadcast_error - - def noisy(self, image: NDArray) -> NDArray: - """Generates a noisy image by adding random noise to the content_image""" - noise_image = np.random.uniform(-20, 20, (1, self.config.image_height, self.config.image_width, self.config.color_channels)).astype('float32') - - # Set the input_image to be a weighted average of the content_image and a noise_image - return noise_image * self.config.noise_ratio + image * (1 - self.config.noise_ratio) - - def process(self, pipeline: List[Callable[[NDArray], NDArray]], image: NDArray) -> NDArray: - if len(pipeline) > 0: - processor = pipeline[0] - pipeline = pipeline[1:] - return self.process(pipeline, processor(image)) - return image - - -class ConfigMeansShapeMissmatchError(Exception): pass - - -@attr.s -class ImageFactory: - image_loader: ImageLoaderFunctionType = attr.ib() - image_processor: ImageProcessor = attr.ib(default=attr.Factory(lambda: ImageProcessor(ImageProcessingConfig.from_image_dimensions()))) - - def from_disk(self, image_path: str, preprocess=True) -> Image: - matrix = Image.load_matrix(image_path, self.image_loader) - if preprocess: - image = Image(image_path, - self.image_processor.process([self.image_processor.reshape_and_normalize_image], matrix)) - else: - image = Image(image_path, matrix) - return image diff --git a/src/artificial_artwork/image/__init__.py b/src/artificial_artwork/image/__init__.py new file mode 100644 index 0000000..b89698c --- /dev/null +++ b/src/artificial_artwork/image/__init__.py @@ -0,0 +1 @@ +from .image_factory import ImageFactory diff --git a/src/artificial_artwork/image/image.py b/src/artificial_artwork/image/image.py new file mode 100644 index 0000000..4350e2a --- /dev/null +++ b/src/artificial_artwork/image/image.py @@ -0,0 +1,16 @@ +import attr +from numpy.typing import NDArray + + +@attr.s +class Image: + """An image loaded into memory, represented as a multidimension mathematical matrix/array. + + The 'file_path' attribute indicates a file in the disk that corresponds to the matrix + + Args: + file_path (str): + matrix (NDArray): + """ + file_path: str = attr.ib() + matrix: NDArray = attr.ib(default=None) diff --git a/src/artificial_artwork/image/image_factory.py b/src/artificial_artwork/image/image_factory.py new file mode 100644 index 0000000..5928264 --- /dev/null +++ b/src/artificial_artwork/image/image_factory.py @@ -0,0 +1,26 @@ +import attr +from typing import Protocol, Any, Callable, List +from numpy.typing import NDArray + +from .image_processor import ImageProcessor +from .image import Image + + +# Define type aliases +class ImageProtocol(Protocol): + file_path: str + matrix: NDArray + +# define type alias for a callable that takes any number of arguments +ImageLoaderFunctionType = Callable[..., NDArray] + + +@attr.s +class ImageFactory: + image_loader: ImageLoaderFunctionType = attr.ib() + image_processor: ImageProcessor = attr.ib(default=attr.Factory(ImageProcessor)) + + def from_disk(self, image_path: str, pipeline: List[Callable[[NDArray], NDArray]]=[], **kwargs) -> ImageProtocol: + matrix = self.image_loader(image_path, **kwargs) + matrix = self.image_processor.process(matrix, pipeline) + return Image(image_path, matrix) diff --git a/src/artificial_artwork/image/image_operations.py b/src/artificial_artwork/image/image_operations.py new file mode 100644 index 0000000..a1e89d7 --- /dev/null +++ b/src/artificial_artwork/image/image_operations.py @@ -0,0 +1,78 @@ +import numpy as np +from numpy.typing import NDArray +from typing import Tuple + + +def reshape_image(image: NDArray, shape: Tuple[int, ...]) -> NDArray: + return np.reshape(image, shape) + + +def subtract(image: NDArray, array: NDArray) -> NDArray: + """Normalize the input image. + + Args: + image (NDArray): [description] + + Raises: + ShapeMissmatchError: in case of ValueError due to numpy broadcasting failing + + Returns: + NDArray: [description] + """ + try: + return image - array + except ValueError as numpy_broadcast_error: + raise ShapeMissmatchError( + f'Expected arrays with matching shapes.') from numpy_broadcast_error + + +class ShapeMissmatchError(Exception): pass + + +def noisy(image: NDArray, ratio: float) -> NDArray: + """Generates a noisy image by adding random noise to the content_image""" + if ratio < 0 or 1 < ratio: + raise InvalidRatioError('Expected a ratio value x such that 0 <= x <= 1') + + prod_shape = image.shape + # assert prod_shape == (1, self.config.image_height, self.config.image_width, self.config.color_channels) + + noise_image = np.random.uniform(-20, 20, prod_shape).astype('float32') + + # Set the input_image to be a weighted average of the content_image and a noise_image + return noise_image * ratio + image * (1 - ratio) + + +class InvalidRatioError(Exception): pass + + +class ImageDTypeConverter: + + bit_2_data_type = {8: np.uint8} + + def __call__(self, image: NDArray): + return self._convert_to_uint8(image) + + def _convert_to_uint8(self, im): + bitdepth = 8 + out_type = type(self).bit_2_data_type[bitdepth] + mi = np.nanmin(im) + ma = np.nanmax(im) + if not np.isfinite(mi): + raise ValueError("Minimum image value is not finite") + if not np.isfinite(ma): + raise ValueError("Maximum image value is not finite") + if ma == mi: + return im.astype(out_type) + + # Make float copy before we scale + im = im.astype("float64") + # Scale the values between 0 and 1 then multiply by the max value + im = (im - mi) / (ma - mi) * (np.power(2.0, bitdepth) - 1) + 0.499999999 + assert np.nanmin(im) >= 0 + assert np.nanmax(im) < np.power(2.0, bitdepth) + im = im.astype(out_type) + return im + + +convert_to_uint8 = ImageDTypeConverter() diff --git a/src/artificial_artwork/image/image_processor.py b/src/artificial_artwork/image/image_processor.py new file mode 100644 index 0000000..4025d28 --- /dev/null +++ b/src/artificial_artwork/image/image_processor.py @@ -0,0 +1,11 @@ +from typing import List, Callable +from numpy.typing import NDArray + + +class ImageProcessor: + def process(self, image: NDArray, pipeline: List[Callable[[NDArray], NDArray]]) -> NDArray: + if len(pipeline) > 0: + processor = pipeline[0] + pipeline = pipeline[1:] + return self.process(processor(image), pipeline) + return image diff --git a/src/artificial_artwork/model_loader.py b/src/artificial_artwork/model_loader.py deleted file mode 100644 index f0c13b6..0000000 --- a/src/artificial_artwork/model_loader.py +++ /dev/null @@ -1,134 +0,0 @@ -### Part of this code is due to the MatConvNet team and is used to load the parameters of the pretrained VGG19 model in the notebook ### - -import numpy as np -import scipy.io -import tensorflow as tf - - -def load_vgg_model(path, config): - """ - Returns a model for the purpose of 'painting' the picture. - Takes only the convolution layer weights and wrap using the TensorFlow - Conv2d, Relu and AveragePooling layer. VGG actually uses maxpool but - the paper indicates that using AveragePooling yields better results. - The last few fully connected layers are not used. - Here is the detailed configuration of the VGG model: - 0 is conv1_1 (3, 3, 3, 64) - 1 is relu - 2 is conv1_2 (3, 3, 64, 64) - 3 is relu - 4 is maxpool - 5 is conv2_1 (3, 3, 64, 128) - 6 is relu - 7 is conv2_2 (3, 3, 128, 128) - 8 is relu - 9 is maxpool - 10 is conv3_1 (3, 3, 128, 256) - 11 is relu - 12 is conv3_2 (3, 3, 256, 256) - 13 is relu - 14 is conv3_3 (3, 3, 256, 256) - 15 is relu - 16 is conv3_4 (3, 3, 256, 256) - 17 is relu - 18 is maxpool - 19 is conv4_1 (3, 3, 256, 512) - 20 is relu - 21 is conv4_2 (3, 3, 512, 512) - 22 is relu - 23 is conv4_3 (3, 3, 512, 512) - 24 is relu - 25 is conv4_4 (3, 3, 512, 512) - 26 is relu - 27 is maxpool - 28 is conv5_1 (3, 3, 512, 512) - 29 is relu - 30 is conv5_2 (3, 3, 512, 512) - 31 is relu - 32 is conv5_3 (3, 3, 512, 512) - 33 is relu - 34 is conv5_4 (3, 3, 512, 512) - 35 is relu - 36 is maxpool - 37 is fullyconnected (7, 7, 512, 4096) - 38 is relu - 39 is fullyconnected (1, 1, 4096, 4096) - 40 is relu - 41 is fullyconnected (1, 1, 4096, 1000) - 42 is softmax - """ - - vgg = scipy.io.loadmat(path) - - vgg_layers = vgg['layers'] - - def _weights(layer, expected_layer_name): - """ - Return the weights and bias from the VGG model for a given layer. - """ - wb = vgg_layers[0][layer][0][0][2] - W = wb[0][0] - b = wb[0][1] - layer_name = vgg_layers[0][layer][0][0][0][0] - assert layer_name == expected_layer_name - return W, b - - def _relu(conv2d_layer): - """ - Return the RELU function wrapped over a TensorFlow layer. Expects a - Conv2d layer input. - """ - return tf.nn.relu(conv2d_layer) - - def _conv2d(prev_layer, layer, layer_name): - """ - Return the Conv2D layer using the weights, biases from the VGG - model at 'layer'. - """ - W, b = _weights(layer, layer_name) - W = tf.constant(W) - b = tf.constant(np.reshape(b, (b.size))) - # return tf.nn.conv2d(prev_layer, filter=W, strides=[1, 1, 1, 1], padding='SAME') + b - return tf.compat.v1.nn.conv2d(prev_layer, filter=W, strides=[1, 1, 1, 1], padding='SAME') + b - - - def _conv2d_relu(prev_layer, layer, layer_name): - """ - Return the Conv2D + RELU layer using the weights, biases from the VGG - model at 'layer'. - """ - return _relu(_conv2d(prev_layer, layer, layer_name)) - - def _avgpool(prev_layer): - """ - Return the AveragePooling layer. - """ - return tf.nn.avg_pool(prev_layer, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME') - - # Constructs the graph model. - graph = {} - # graph['input'] = tf.Variable(np.zeros((1, CONFIG.IMAGE_HEIGHT, CONFIG.IMAGE_WIDTH, CONFIG.COLOR_CHANNELS)), dtype = 'float32') - graph['input'] = tf.Variable(np.zeros((1, config.image_height, config.image_width, config.color_channels)), dtype = 'float32') - graph['conv1_1'] = _conv2d_relu(graph['input'], 0, 'conv1_1') - graph['conv1_2'] = _conv2d_relu(graph['conv1_1'], 2, 'conv1_2') - graph['avgpool1'] = _avgpool(graph['conv1_2']) - graph['conv2_1'] = _conv2d_relu(graph['avgpool1'], 5, 'conv2_1') - graph['conv2_2'] = _conv2d_relu(graph['conv2_1'], 7, 'conv2_2') - graph['avgpool2'] = _avgpool(graph['conv2_2']) - graph['conv3_1'] = _conv2d_relu(graph['avgpool2'], 10, 'conv3_1') - graph['conv3_2'] = _conv2d_relu(graph['conv3_1'], 12, 'conv3_2') - graph['conv3_3'] = _conv2d_relu(graph['conv3_2'], 14, 'conv3_3') - graph['conv3_4'] = _conv2d_relu(graph['conv3_3'], 16, 'conv3_4') - graph['avgpool3'] = _avgpool(graph['conv3_4']) - graph['conv4_1'] = _conv2d_relu(graph['avgpool3'], 19, 'conv4_1') - graph['conv4_2'] = _conv2d_relu(graph['conv4_1'], 21, 'conv4_2') - graph['conv4_3'] = _conv2d_relu(graph['conv4_2'], 23, 'conv4_3') - graph['conv4_4'] = _conv2d_relu(graph['conv4_3'], 25, 'conv4_4') - graph['avgpool4'] = _avgpool(graph['conv4_4']) - graph['conv5_1'] = _conv2d_relu(graph['avgpool4'], 28, 'conv5_1') - graph['conv5_2'] = _conv2d_relu(graph['conv5_1'], 30, 'conv5_2') - graph['conv5_3'] = _conv2d_relu(graph['conv5_2'], 32, 'conv5_3') - graph['conv5_4'] = _conv2d_relu(graph['conv5_3'], 34, 'conv5_4') - graph['avgpool5'] = _avgpool(graph['conv5_4']) - - return graph diff --git a/src/artificial_artwork/nst_image.py b/src/artificial_artwork/nst_image.py new file mode 100644 index 0000000..a7d681a --- /dev/null +++ b/src/artificial_artwork/nst_image.py @@ -0,0 +1,51 @@ +import attr +from .image import ImageFactory +from .disk_operations import Disk + + + +@attr.s +class ImageManager: + preprocessing_pipeline = attr.ib() + image_factory: ImageFactory = \ + attr.ib(init=False, default=attr.Factory(lambda: ImageFactory(Disk.load_image))) + images_compatible: bool = attr.ib(init=False, default=False) + + _known_types = attr.ib(init=False, default={'content', 'style'}) + + def __attrs_post_init__(self): + for image_type in self._known_types: + setattr(self, f'_{image_type}_image', None) + + def load_from_disk(self, file_path: str, image_type: str): + if image_type not in self._known_types: + raise ValueError(f'Expected type of image to be one of {self._known_types}; found {image_type}') + # dynamically call the appropriate content/style setter method + setattr(self, f'{image_type}_image', + self.image_factory.from_disk(file_path, self.preprocessing_pipeline + )) + + def _set_image(self, image, image_type: str): + # dynamically set appropriate content/style attribute + setattr(self, f'_{image_type}_image', image) + if not (self._content_image is None or self._style_image is None): + if self._content_image.matrix.shape == self._style_image.matrix.shape: + self.images_compatible = True + return + self.images_compatible = False + + @property + def content_image(self): + return self._content_image + + @content_image.setter + def content_image(self, image): + self._set_image(image, 'content') + + @property + def style_image(self): + return self._style_image + + @style_image.setter + def style_image(self, image): + self._set_image(image, 'style') diff --git a/src/artificial_artwork/math.py b/src/artificial_artwork/nst_math.py similarity index 100% rename from src/artificial_artwork/math.py rename to src/artificial_artwork/nst_math.py diff --git a/src/artificial_artwork/nst_tf_algorithm.py b/src/artificial_artwork/nst_tf_algorithm.py index 96030e6..55b270c 100644 --- a/src/artificial_artwork/nst_tf_algorithm.py +++ b/src/artificial_artwork/nst_tf_algorithm.py @@ -5,7 +5,7 @@ from .tf_session_runner import TensorflowSessionRunner -from .model_loader import load_vgg_model +from .pretrained_model import graph_factory from .cost_computer import NSTCostComputer, NSTContentCostComputer, NSTStyleCostComputer from .utils.notification import Subject @@ -20,15 +20,6 @@ class NSTAlgorithmRunner: nn_builder = attr.ib(init=False, default=None) nn_cost_builder = attr.ib(init=False, default=None) - # nn_builder = attr.ib(init=False, default=attr.Factory(lambda self: NeuralNetBuilder( - # self.image_model, self.session_runner.session - # ), takes_self=True)) - # nn_cost_builder = attr.ib(init=False, default=attr.Factory(lambda: CostBuilder( - # NSTCostComputer.compute, - # NSTContentCostComputer.compute, - # NSTStyleCostComputer.compute, - # ))) - # broadcast facilities to notify observers/listeners progress_subject = attr.ib(init=False, default=attr.Factory(Subject)) peristance_subject = attr.ib(init=False, default=attr.Factory(Subject)) @@ -42,19 +33,22 @@ def run(self): ## Prepare ## c_image = self.nst_algorithm.parameters.content_image s_image = self.nst_algorithm.parameters.style_image - + + image_specs = type('ImageSpecs', (), { + 'height': c_image.matrix.shape[1], + 'width': c_image.matrix.shape[2], + 'color_channels': c_image.matrix.shape[3] + })() + print(' --- Loading CV Image Model ---') - image_model = load_vgg_model( - self.nst_algorithm.parameters.cv_model, - self.nst_algorithm.image_config, - ) + style_network = graph_factory.create(image_specs) noisy_content_image_matrix = self.apply_noise(self.nst_algorithm.parameters.content_image.matrix) print(' --- Building Computations ---') self.nn_builder = NeuralNetBuilder( - image_model, + style_network, self.session_runner.session ) @@ -78,7 +72,7 @@ def run(self): # using the loaded cv model (which is a dict of layers) # the NSTStyleLayer ids attribute to query the dict for style_layer_id, nst_style_layer in self.nst_algorithm.parameters.style_layers: - nst_style_layer.neurons = image_model[style_layer_id] + nst_style_layer.neurons = style_network[style_layer_id] # TODO obviously encapsulate the above code elsewhere self.nn_cost_builder.build_style_cost( @@ -100,7 +94,7 @@ def run(self): self.session_runner.run(tf.compat.v1.global_variables_initializer()) # Run the noisy input image (initial generated image) through the model - self.session_runner.run(image_model['input'].assign(input_image)) + self.session_runner.run(style_network['input'].assign(input_image)) # Iterate print(' --- Running Iterative Algorithm ---') @@ -109,7 +103,7 @@ def run(self): self.time_started = time() while not self.nst_algorithm.parameters.termination_condition.satisfied: - generated_image = self.iterate(image_model) + generated_image = self.iterate(style_network) progress = self._progress(generated_image, completed_iterations=i+1) if i % 20 == 0: self._notify_persistance(progress) @@ -227,7 +221,7 @@ class CostBuilder: cost_function = attr.ib() compute_content_cost = attr.ib() compute_style_cost = attr.ib() - + cost = attr.ib(init=False, default=None) content_cost = attr.ib(init=False, default=None) style_cost = attr.ib(init=False, default=None) @@ -253,7 +247,7 @@ class Optimization: def optimize_against(self, cost): self._train_step = self.optimizer.minimize(cost) - + @property def train_step(self): return self._train_step diff --git a/src/artificial_artwork/pretrained_model/__init__.py b/src/artificial_artwork/pretrained_model/__init__.py new file mode 100644 index 0000000..c221c94 --- /dev/null +++ b/src/artificial_artwork/pretrained_model/__init__.py @@ -0,0 +1 @@ +from .model_loader import GraphFactory as graph_factory diff --git a/src/artificial_artwork/pretrained_model/image_model.py b/src/artificial_artwork/pretrained_model/image_model.py new file mode 100644 index 0000000..7436197 --- /dev/null +++ b/src/artificial_artwork/pretrained_model/image_model.py @@ -0,0 +1,38 @@ +"""This module contains the high-level architecture design of our 'style model'. + +As 'style model' we define a neural network (represented as a mathematical +graph) with several convolutional layers with weights extacted from a pretrained +image model (ie the vgg19 model trained for the task of image classification on +the imagenet dataset) and some average pooling layers with predefined weights. + +All weigths of the style model stay constants during optimization of the +training objective (aka cost function). + +Here we only take the convolution layer weights and define several new +AveragePooling. We opt for AveragePooling compared to MaxPooling, since it has +been shown to yield better results. +""" + +LAYERS = ( + 'conv1_1' , + 'conv1_2' , + 'avgpool1', + 'conv2_1' , + 'conv2_2' , + 'avgpool2', + 'conv3_1' , + 'conv3_2' , + 'conv3_3' , + 'conv3_4' , + 'avgpool3', + 'conv4_1' , + 'conv4_2' , + 'conv4_3' , + 'conv4_4' , + 'avgpool4', + 'conv5_1' , + 'conv5_2' , + 'conv5_3' , + 'conv5_4' , + 'avgpool5', +) diff --git a/src/artificial_artwork/pretrained_model/layers_getter.py b/src/artificial_artwork/pretrained_model/layers_getter.py new file mode 100644 index 0000000..fd30021 --- /dev/null +++ b/src/artificial_artwork/pretrained_model/layers_getter.py @@ -0,0 +1,15 @@ +from numpy.typing import NDArray +import attr + +from .vgg_architecture import LAYERS + + +@attr.s +class VggLayersGetter: + vgg_layers: NDArray = attr.ib() + _vgg_layer_id_2_layer = attr.ib(init=False, + default=attr.Factory(lambda self: {layer_id: self.vgg_layers[index] for index, layer_id in enumerate(LAYERS)}, takes_self=True)) + + @property + def id_2_layer(self): + return self._vgg_layer_id_2_layer diff --git a/src/artificial_artwork/pretrained_model/model_loader.py b/src/artificial_artwork/pretrained_model/model_loader.py new file mode 100644 index 0000000..7b1691a --- /dev/null +++ b/src/artificial_artwork/pretrained_model/model_loader.py @@ -0,0 +1,146 @@ +### Part of this code is due to the MatConvNet team and is used to load the parameters of the pretrained VGG19 model in the notebook ### + +import os +import re +from typing import Dict, Tuple, Any, Protocol + +import attr +from numpy.typing import NDArray +import numpy as np +import scipy.io +import tensorflow as tf + +from .layers_getter import VggLayersGetter +from .image_model import LAYERS as NETWORK_DESIGN + + +class ImageSpecs(Protocol): + width: int + height: int + color_channels: int + + +def load_vgg_model_parameters(path: str) -> Dict[str, NDArray]: + return scipy.io.loadmat(path) + + +class NoImageModelSpesifiedError(Exception): pass + + +def get_vgg_19_model_path(): + try: + return os.environ['AA_VGG_19'] + except KeyError as variable_not_found: + raise NoImageModelSpesifiedError('No pretrained image model found. ' + 'Please download it and set the AA_VGG_19 environment variable with the' + 'path where ou stored the model (*.mat file), to indicate to wher to ' + 'locate and load it') from variable_not_found + + +def load_default_model_parameters(): + path = get_vgg_19_model_path() + return load_vgg_model_parameters(path) + + +def get_layers(model_parameters: Dict[str, NDArray]) -> NDArray: + return model_parameters['layers'][0] + + +class GraphBuilder: + + def __init__(self): + self.graph = {} + self._prev_layer = None + + def _build_layer(self, layer_id: str, layer): + self.graph[layer_id] = layer + self._prev_layer = layer + return self + + def input(self, width: int, height: int, nb_channels=3, dtype='float32', layer_id='input'): + self.graph = {} + return self._build_layer(layer_id, tf.Variable(np.zeros((1, height, width, nb_channels)), dtype=dtype)) + + def avg_pool(self, layer_id: str): + return self._build_layer(layer_id, tf.nn.avg_pool(self._prev_layer, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')) + + def relu_conv_2d(self, layer_id: str, layer_weights): + """A Relu wrapped around a convolutional layer. + + Will use the layer_id to find weight (for W and b matrices) values in + the pretrained model (layer). + + Also uses the layer_id to as dict key to the output graph. + """ + W, b = layer_weights + return self._build_layer(layer_id, tf.nn.relu(self._conv_2d(W, b))) + + def _conv_2d(self, W: NDArray, b: NDArray): + W = tf.constant(W) + b = tf.constant(np.reshape(b, (b.size))) + return tf.compat.v1.nn.conv2d(self._prev_layer, filter=W, strides=[1, 1, 1, 1], padding='SAME') + b + + +@attr.s +class ModelParameters: + params = attr.ib(default=attr.Factory(load_default_model_parameters)) + + +class GraphFactory: + builder = GraphBuilder() + + @classmethod + def weights(cls, layer: NDArray) -> Tuple[NDArray, NDArray]: + """Get the weights and bias for a given layer of the VGG model.""" + # wb = vgg_layers[0][layer][0][0][2] + wb = layer[0][0][2] + W = wb[0][0] + b = wb[0][1] + return W, b + + @classmethod + def create(cls, config: ImageSpecs, model_parameters=None) -> Dict[str, Any]: + """Create a model for the purpose of 'painting'/generating a picture. + + Creates a Deep Learning Neural Network with most layers having weights + (aka model parameters) with values extracted from a pre-trained model + (ie another neural network trained on an image dataset suitably). + + Args: + config ([type]): [description] + model_parameters ([type], optional): [description]. Defaults to None. + + Returns: + Dict[str, Any]: [description] + """ + + vgg_model_parameters = ModelParameters(*list(filter(None, [model_parameters]))) + + vgg_layers = get_layers(vgg_model_parameters.params) + + layer_getter = VggLayersGetter(vgg_layers) + + def relu(layer_id: str): + return cls.builder.relu_conv_2d(layer_id, cls.weights(layer_getter.id_2_layer[layer_id])) + + layer_callbacks = { + 'conv': relu, + 'avgpool': cls.builder.avg_pool + } + + def layer(layer_id: str): + matched_string = re.match(r'(\w+?)[\d_]*$', layer_id).group(1) + return layer_callbacks[matched_string](layer_id) + + ## Build Graph + + # each relu_conv_2d uses pretrained model's layer weights for W and b matrices + # each average pooling layer uses custom weight values + # all weights are guaranteed to remain constant (see GraphBuilder._conv_2d method) + + # cls.builder.input(config.image_width, config.image_height, nb_channels=config.color_channels) + cls.builder.input(config.width, config.height, nb_channels=config.color_channels) + for layer_id in NETWORK_DESIGN: + layer(layer_id) + + return cls.builder.graph diff --git a/src/artificial_artwork/pretrained_model/vgg_architecture.py b/src/artificial_artwork/pretrained_model/vgg_architecture.py new file mode 100644 index 0000000..b829c64 --- /dev/null +++ b/src/artificial_artwork/pretrained_model/vgg_architecture.py @@ -0,0 +1,52 @@ +""" +""" +def vgg_layers(): + """The network's layer structure of the vgg image model.""" + return ( + (0, 'conv1_1') , # (3, 3, 3, 64) + (1, 'relu1_1') , + (2, 'conv1_2') , # (3, 3, 64, 64) + (3, 'relu1_2') , + (4, 'pool1') , + (5, 'conv2_1') , # (3, 3, 64, 128) + (6, 'relu2_1') , + (7, 'conv2_2') , # (3, 3, 128, 128) + (8, 'relu2_2') , + (9, 'pool2') , + (10, 'conv3_1'), # (3, 3, 128, 256) + (11, 'relu3_1'), + (12, 'conv3_2'), # (3, 3, 256, 256) + (13, 'relu3_2'), + (14, 'conv3_3'), # (3, 3, 256, 256) + (15, 'relu3_3'), + (16, 'conv3_4'), # (3, 3, 256, 256) + (17, 'relu3_4'), + (18, 'pool3') , + (19, 'conv4_1'), # (3, 3, 256, 512) + (20, 'relu4_1'), + (21, 'conv4_2'), # (3, 3, 512, 512) + (22, 'relu4_2'), + (23, 'conv4_3'), # (3, 3, 512, 512) + (24, 'relu4_3'), + (25, 'conv4_4'), # (3, 3, 512, 512) + (26, 'relu4_4'), + (27, 'pool4') , + (28, 'conv5_1'), # (3, 3, 512, 512) + (29, 'relu5_1'), + (30, 'conv5_2'), # (3, 3, 512, 512) + (31, 'relu5_2'), + (32, 'conv5_3'), # (3, 3, 512, 512) + (33, 'relu5_3'), + (34, 'conv5_4'), # (3, 3, 512, 512) +# 35 is relu +# 36 is maxpool +# 37 is fullyconnected (7, 7, 512, 4096) +# 38 is relu +# 39 is fullyconnected (1, 1, 4096, 4096) +# 40 is relu +# 41 is fullyconnected (1, 1, 4096, 1000) +# 42 is softmax + ) + + +LAYERS = tuple((layer_id for _, layer_id in vgg_layers())) diff --git a/src/artificial_artwork/progress_interface.py b/src/artificial_artwork/progress_interface.py deleted file mode 100644 index 9464014..0000000 --- a/src/artificial_artwork/progress_interface.py +++ /dev/null @@ -1,12 +0,0 @@ -from abc import ABC - - -class ProgressInterface(ABC): - """The progress made by an iterative algorithm. - - Args: - ABC ([type]): [description] - """ - iterations: int - duration: float - cost_improvement: float diff --git a/src/artificial_artwork/styling_observer.py b/src/artificial_artwork/styling_observer.py index f767b02..cc2fca5 100644 --- a/src/artificial_artwork/styling_observer.py +++ b/src/artificial_artwork/styling_observer.py @@ -9,6 +9,7 @@ @define class StylingObserver(Observer): save_on_disk_callback: Callable[[str, npt.NDArray], None] + convert_to_unit8: Callable[[npt.NDArray], npt.NDArray] """Store a snapshot of the image under construction. Args: @@ -18,42 +19,45 @@ def update(self, *args, **kwargs): output_dir = args[0].state.output_path content_image_path = args[0].state.content_image_path style_image_path = args[0].state.style_image_path - itererations_completed = args[0].state.metrics['iterations'] + iterations_completed = args[0].state.metrics['iterations'] matrix = args[0].state.matrix + # Impelement handling of the request to persist with a chain of responsibility design pattern + # it suit since we do not knw how many checks and/or image transformation will be required before + # saving on disk + output_file_path = os.path.join( output_dir, - f'{os.path.basename(content_image_path)}+{os.path.basename(style_image_path)}-{itererations_completed}.png' + f'{os.path.basename(content_image_path)}+{os.path.basename(style_image_path)}-{iterations_completed}.png' ) - + # if we have shape of form (1, Width, Height, Number_of_Color_Channels) if matrix.ndim == 4 and matrix.shape[0] == 1: - # we have shape of form (1, Width, Height, Number_of_Color_Channels) + # reshape to (Width, Height, Number_of_Color_Channels) matrix = np.reshape(matrix, tuple(matrix.shape[1:])) if str(matrix.dtype) != 'uint8': - matrix = self._convert_to_uint8(matrix) - + matrix = self.convert_to_unit8(matrix) self.save_on_disk_callback(matrix, output_file_path, format='png') - bit_2_data_type = {8: np.uint8} - - def _convert_to_uint8(self, im): - bitdepth = 8 - out_type = type(self).bit_2_data_type[bitdepth] - mi = np.nanmin(im) - ma = np.nanmax(im) - if not np.isfinite(mi): - raise ValueError("Minimum image value is not finite") - if not np.isfinite(ma): - raise ValueError("Maximum image value is not finite") - if ma == mi: - return im.astype(out_type) - - # Make float copy before we scale - im = im.astype("float64") - # Scale the values between 0 and 1 then multiply by the max value - im = (im - mi) / (ma - mi) * (np.power(2.0, bitdepth) - 1) + 0.499999999 - assert np.nanmin(im) >= 0 - assert np.nanmax(im) < np.power(2.0, bitdepth) - im = im.astype(out_type) - return im + # bit_2_data_type = {8: np.uint8} + + # def _convert_to_uint8(self, im): + # bitdepth = 8 + # out_type = type(self).bit_2_data_type[bitdepth] + # mi = np.nanmin(im) + # ma = np.nanmax(im) + # if not np.isfinite(mi): + # raise ValueError("Minimum image value is not finite") + # if not np.isfinite(ma): + # raise ValueError("Maximum image value is not finite") + # if ma == mi: + # return im.astype(out_type) + + # # Make float copy before we scale + # im = im.astype("float64") + # # Scale the values between 0 and 1 then multiply by the max value + # im = (im - mi) / (ma - mi) * (np.power(2.0, bitdepth) - 1) + 0.499999999 + # assert np.nanmin(im) >= 0 + # assert np.nanmax(im) < np.power(2.0, bitdepth) + # im = im.astype(out_type) + # return im diff --git a/src/artificial_artwork/termination_condition_adapter.py b/src/artificial_artwork/termination_condition_adapter.py index d969d10..5372427 100644 --- a/src/artificial_artwork/termination_condition_adapter.py +++ b/src/artificial_artwork/termination_condition_adapter.py @@ -56,7 +56,8 @@ def __new__(mcs, *args, **kwargs): # The (outer) keys are usable by client code to select termination condition # Each (inner) 'key_name' points to the name to use to query the subject dict - # Each initializer is a callback to use to initialize the 'runtime_state' attribute [per termination condition (adapter)] + # Each 'state' key points to a value that should be used to initialize + # the 'runtime_state' attribute [per termination condition (adapter)] termination_condition_adapter_class.mapping = { 'max-iterations': {'key_name': 'iterations', 'state': 0}, 'convergence': {'key_name': 'cost', 'state': float('inf')}, diff --git a/tests/conftest.py b/tests/conftest.py index de591dc..74f8aaa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,10 +53,10 @@ def __exit__(self, type, value, traceback): return MySession -@pytest.fixture -def default_image_processing_config(): - from artificial_artwork.image import ImageProcessingConfig - return ImageProcessingConfig.from_image_dimensions() +# @pytest.fixture +# def default_image_processing_config(): +# from artificial_artwork.image import ImageProcessingConfig +# return ImageProcessingConfig.from_image_dimensions() @pytest.fixture @@ -68,7 +68,7 @@ def image_factory(): Returns: ImageFactory: an instance of the ImageFactory class """ - from artificial_artwork.image import ImageFactory + from artificial_artwork.image.image_factory import ImageFactory from artificial_artwork.disk_operations import Disk return ImageFactory(Disk.load_image) @@ -102,3 +102,28 @@ def subscribe(): def _subscribe(broadcaster, listeners): broadcaster.add(*listeners) return _subscribe + + + +@pytest.fixture +def broadcaster_class(): + class TestSubject: + def __init__(self, subject, done_callback): + self.subject = subject + self.done = done_callback + + def iterate(self): + i = 0 + while not self.done(): + # do something in the current iteration + print('Iteration with index', i) + + # notify when we have completed i+1 iterations + self.subject.state = type('Subject', (), { + 'metrics': {'iterations': i + 1}, # we have completed i+1 iterations + }) + self.subject.notify() + i += 1 + return i + + return TestSubject diff --git a/tests/test_algorithm_params.py b/tests/test_algorithm_params.py new file mode 100644 index 0000000..e035f55 --- /dev/null +++ b/tests/test_algorithm_params.py @@ -0,0 +1,23 @@ +import pytest + +@pytest.fixture +def algorithm_parameters(): + from artificial_artwork.algorithm import AlogirthmParameters + return AlogirthmParameters( + 'content_image', + 'style_image', + [ + ('layer-1', 0.5), + ('layer-2', 0.5), + ], + 'termination_condition', + 'output_path', + ) + + +def test_algorithm_parameters(algorithm_parameters): + assert hasattr(algorithm_parameters, 'content_image') + assert hasattr(algorithm_parameters, 'style_image') + assert hasattr(algorithm_parameters, 'style_layers') + assert hasattr(algorithm_parameters, 'termination_condition') + assert hasattr(algorithm_parameters, 'output_path') diff --git a/tests/test_content_cost.py b/tests/test_content_cost.py index fa497ea..7e9ef5c 100644 --- a/tests/test_content_cost.py +++ b/tests/test_content_cost.py @@ -8,10 +8,6 @@ def compute_cost(): return NSTContentCostComputer.compute -# @pytest.fixture -# def activations(): - - def test_content_cost_computation(session, compute_cost): with session(2) as _test: a_C = tf.compat.v1.random_normal([1, 4, 4, 3], mean=1, stddev=4) diff --git a/tests/test_cv_model.py b/tests/test_cv_model.py index ebee01d..179c6cd 100644 --- a/tests/test_cv_model.py +++ b/tests/test_cv_model.py @@ -1,24 +1,116 @@ import os import pytest +from artificial_artwork.pretrained_model.model_loader import get_vgg_19_model_path, load_default_model_parameters my_dir = os.path.dirname(os.path.realpath(__file__)) -IMAGE_MODEL_FILE_NAME = 'imagenet-vgg-verydeep-19.mat' +# IMAGE_MODEL_FILE_NAME = 'imagenet-vgg-verydeep-19.mat' + + +PRODUCTION_IMAGE_MODEL = os.environ.get('AA_VGG_19', 'PRETRAINED_MODEL_NOT_FOUND') + + +@pytest.fixture +def model_parameters(): + from artificial_artwork.pretrained_model.model_loader import load_default_model_parameters + return load_default_model_parameters() + # return load_default_model_parameters() + # from artificial_artwork.pretrained_model.model_loader import load_vgg_model_parameters + # return load_vgg_model_parameters + + +@pytest.fixture +def vgg_layers(): + """Expected layers structure of the vgg image model.""" + from artificial_artwork.pretrained_model.vgg_architecture import LAYERS + return LAYERS @pytest.fixture -def production_image_model(): - return os.path.join(my_dir, '..', IMAGE_MODEL_FILE_NAME) +def style_network_architecture(): + from artificial_artwork.pretrained_model.image_model import LAYERS + return LAYERS @pytest.fixture -def load_model(default_image_processing_config): - from artificial_artwork.model_loader import load_vgg_model - return lambda model_path: load_vgg_model(model_path, default_image_processing_config) +def graph_factory(): + from artificial_artwork.pretrained_model import graph_factory + return graph_factory -@pytest.mark.xfail(not os.path.isfile(os.path.join(my_dir, '..', IMAGE_MODEL_FILE_NAME)), +@pytest.mark.xfail(not os.path.isfile(PRODUCTION_IMAGE_MODEL), reason="No file found to load the pretrained image (cv) model.") -def test_pretrained_model(load_model, production_image_model): - _ = load_model(production_image_model) +def test_pretrained_model(model_parameters, graph_factory, vgg_layers, style_network_architecture): + layers = model_parameters['layers'] + + image_specs = type('ImageSpecs', (), { + 'width': 400, + 'height': 300, + 'color_channels': 3 + })() + + # verify original/loaded neural network has 43 layers + assert len(layers[0]) == 43 + + for i, name in enumerate(vgg_layers): + assert layers[0][i][0][0][0][0] == name + + graph = graph_factory.create(image_specs, model_parameters=model_parameters) + assert set(graph.keys()) == set(['input'] + list(style_network_architecture)) + + +@pytest.fixture +def graph_builder(): + from artificial_artwork.pretrained_model.model_loader import GraphBuilder + return GraphBuilder() + + +def test_building_layers(graph_builder): + import numpy as np + height = 2 + width = 6 + channels = 2 + expected_input_shape = (1, height, width, channels) + + graph_builder.input(width, height, nb_channels=channels) + # assert previous layer is the 'input' layer we just added/created + assert tuple(graph_builder._prev_layer.shape) == expected_input_shape + for w in range(width): + for h in range(height): + for c in range(channels): + assert graph_builder._prev_layer[0][h][w][c] == graph_builder.graph['input'][0][h][w][c] == 0 + + # create relu(convolution) layer + W = np.array(np.random.rand(*expected_input_shape[1:], channels), dtype=np.float32) + + b_weight = 6.0 + b = np.array([b_weight], dtype=np.float32) + graph_builder.relu_conv_2d('convo1', (W, b)) + + # assert the previous layer is the relu(convolution) layer we just added + assert tuple(graph_builder._prev_layer.shape) == expected_input_shape + for w in range(width): + for h in range(height): + for c in range(channels): + assert graph_builder._prev_layer[0][h][w][c] == graph_builder.graph['convo1'][0][h][w][c] + assert graph_builder._prev_layer[0][h][w][c] == b_weight # W[h][w][c][c] + b[0] + + + # create Average Pooling layer + layer_id = 'avgpool1' + graph_builder.avg_pool(layer_id) + + # assert previous layer is the layer we just added/created + expected_avg_pool_shape = (1, 1, 2, channels) + expected_avg_output = np.array( + [[[[b_weight, b_weight, b_weight], + [b_weight, b_weight, b_weight], + [b_weight, b_weight, b_weight] + ]]] + ,dtype=np.float32) + + for i in range(expected_avg_pool_shape[2]): + for c in range(channels): + assert graph_builder._prev_layer[0][0][i][c] == graph_builder.graph[layer_id][0][0][i][c] + assert graph_builder._prev_layer[0][0][i][c] == expected_avg_output[0][0][i][c] diff --git a/tests/test_image.py b/tests/test_image.py deleted file mode 100644 index b46c093..0000000 --- a/tests/test_image.py +++ /dev/null @@ -1,51 +0,0 @@ -import pytest - - -@pytest.mark.parametrize('image_path', [ - ('canoe_water.jpg'), -]) -def test_image(image_path, image_factory, test_image): - in_memory_image = image_factory.from_disk(test_image_path := test_image(image_path), preprocess=True) - assert in_memory_image.file_path == test_image_path - assert in_memory_image.matrix.shape == (1, 300, 400, 3) - - -def test_image_processing_config_methods(default_image_processing_config): - import numpy as np - assert default_image_processing_config.image_width == 400 - assert default_image_processing_config.image_height == 300 - assert default_image_processing_config.color_channels == 3 - assert default_image_processing_config.noise_ratio == 0.6 - assert np.array_equal(default_image_processing_config.means, np.array([123.68, 116.779, 103.939]).reshape((1,1,1,3))) - - -@pytest.fixture(params=[ - ['canoe_water.jpg', 400, 300, 4], - # ['canoe_water.jpg', 300, 300, 3], -], scope='function') -def data(request, test_image): - import numpy as np - from artificial_artwork.image import ImageProcessor, ImageProcessingConfig, ImageFactory, ConfigMeansShapeMissmatchError - from artificial_artwork.disk_operations import Disk - dummy_means = [100 + i * 10 for i in range(request.param[3])] - return type('WrongConfigurationScenarioData', (), { - 'image_factory': ImageFactory(Disk.load_image, ImageProcessor(ImageProcessingConfig( - request.param[1], - request.param[2], - request.param[3], - 0.6, - np.array(dummy_means).reshape((1, 1, 1, request.param[3])) - ))), - 'test_image': test_image(request.param[0]), - 'expected_exception': ConfigMeansShapeMissmatchError, - }) - - -def test_wrong_configuration(data): - """Test a failed scenario of image preprocessing. - - Tests the behaviour when the input image dimensions and/or color channels do not match the ones configured - and used by components such as the ImageProcessor. - """ - with pytest.raises(data.expected_exception): - data.image_factory.from_disk(data.test_image, preprocess=True) diff --git a/tests/test_image/test_image_processing.py b/tests/test_image/test_image_processing.py new file mode 100644 index 0000000..22a3892 --- /dev/null +++ b/tests/test_image/test_image_processing.py @@ -0,0 +1,137 @@ +import pytest +import numpy as np + + +@pytest.fixture +def image_operations(): + from artificial_artwork.image.image_operations import reshape_image, subtract, noisy, convert_to_uint8 + return type('Ops', (), { + 'reshape': reshape_image, + 'subtract': subtract, + 'noisy': noisy, + 'convert_to_uint8': convert_to_uint8, + }) + + +@pytest.mark.parametrize('test_image', [ + ([[11,12,13], [21, 22, 23]]), +]) +def test_image_reshape(test_image, image_operations): + math_array = np.array(test_image, dtype=np.float32) + image = image_operations.reshape(math_array, (1,) + math_array.shape) + assert image.shape == (1,) + math_array.shape + + +@pytest.mark.parametrize('test_image, array', [ + ([[11,12,13], [21, 22, 23]], [[1, 2, 3], [4, 5, 6]]), +]) +def test_subtract_image(test_image, array, image_operations): + math_array_1 = np.array(test_image, dtype=np.float32) + math_array_2 = np.array(array, dtype=np.float32) + + image = image_operations.subtract(math_array_1, math_array_2) + assert image.shape == math_array_1.shape + assert image.tolist() == [ + [10, 10, 10], + [17, 17, 17] + ] + +@pytest.mark.parametrize('test_image, array', [ + ([[11,12,13], [21, 22, 23]], [[1, 2], [4, 5]]), +]) +def test_wrong_subtract(test_image, array, image_operations): + from artificial_artwork.image.image_operations import ShapeMissmatchError + with pytest.raises(ShapeMissmatchError): + math_array_1 = np.array(test_image, dtype=np.float32) + math_array_2 = np.array(array, dtype=np.float32) + image_operations.subtract(math_array_1, math_array_2) + + + +@pytest.mark.parametrize('test_image, ratio', [ + ([[11,12,13], [21, 22, 23]], 0), + ([[11,12,13], [21, 22, 23]], 0.6), + ([[11,12,13], [21, 22, 23]], 1), +]) +def test_noisy(test_image, ratio, image_operations): + math_array = np.array(test_image, dtype=np.float32) + min_pixel_value = np.min(math_array) + max_pixel_value = np.max(math_array) + image = image_operations.noisy(math_array, ratio) + assert (image <= max(max_pixel_value, 20)).all() + assert (min(min_pixel_value, -20) <= image).all() + + + +@pytest.mark.parametrize('test_image, ratio', [ + ([[11, 12], [21, 22]], 1.1), + ([[12, 13], [21, 23]], -0.2), +]) +def test_wrong_noisy_ratio(test_image, ratio, image_operations): + from artificial_artwork.image.image_operations import InvalidRatioError + math_array = np.array(test_image, dtype=np.float32) + with pytest.raises(InvalidRatioError): + image = image_operations.noisy(math_array, ratio) + + +# UINT8 CONVERTION TESTS +@pytest.mark.parametrize('test_image, expected_image', [ +( + [[1.2, 9.1], + [10, 3]], + + [[0, 229], + [255, 52]] + ), + + ( + [[1, 1], + [3, 5]], + + [[0, 0], + [127, 255]] + ), + + ( + [[1, 1], + [1, 1]], + + [[1, 1], + [1, 1]] + ), + +]) +def test_uint8_convertion(test_image, expected_image, image_operations): + runtime_image = image_operations.convert_to_uint8(np.array(test_image, dtype=np.float32)) + assert runtime_image.dtype == np.uint8 + assert 0 <= np.nanmin(runtime_image) + assert np.nanmax(runtime_image) < np.power(2.0, 8) + assert runtime_image.tolist() == expected_image + + +@pytest.mark.parametrize('test_image', [ + ( + [[np.nan, np.nan], + [np.nan, np.nan]], + ), + + ( + [[1, -float('inf')], + [2, 3]], + ), + +]) +def test_non_finite_minimum_value(test_image, image_operations): + with pytest.raises(ValueError, match=r'Minimum image value is not finite'): + runtime_image = image_operations.convert_to_uint8(np.array(test_image, dtype=np.float32)) + + +@pytest.mark.parametrize('test_image', [ + ( + [[1, float('inf')], + [2, 3]], + ), +]) +def test_non_finite_maximum_value(test_image, image_operations): + with pytest.raises(ValueError, match=r'Maximum image value is not finite'): + runtime_image = image_operations.convert_to_uint8(np.array(test_image, dtype=np.float32)) diff --git a/tests/test_image/test_image_processor.py b/tests/test_image/test_image_processor.py new file mode 100644 index 0000000..3650499 --- /dev/null +++ b/tests/test_image/test_image_processor.py @@ -0,0 +1,17 @@ +import pytest +import numpy as np + + +@pytest.fixture +def image_processor(): + from artificial_artwork.image.image_processor import ImageProcessor + return ImageProcessor() + + +@pytest.mark.parametrize('image, pipeline, output', [ + ([[1,2,3], [4,5,6]], [], [[1,2,3], [4,5,6]]), + ([[1,2,3], [4,5,6]], [lambda array: array + 1], [[2,3,4], [5,6,7]]), +]) +def test_image_processor(image, pipeline, output, image_processor): + runtime_output = image_processor.process(np.array(image), pipeline) + assert (runtime_output - np.array(output) == 0).all() diff --git a/tests/test_math.py b/tests/test_math.py index f051f74..e27a35c 100644 --- a/tests/test_math.py +++ b/tests/test_math.py @@ -4,7 +4,7 @@ @pytest.fixture def gram_matrix(): - from artificial_artwork.math import gram_matrix + from artificial_artwork.nst_math import gram_matrix return gram_matrix diff --git a/tests/test_nst_image.py b/tests/test_nst_image.py new file mode 100644 index 0000000..fbb039d --- /dev/null +++ b/tests/test_nst_image.py @@ -0,0 +1,35 @@ +import pytest + + +@pytest.fixture +def image_manager(): + from artificial_artwork.nst_image import ImageManager + return ImageManager([lambda array: array + 2]) + + +@pytest.fixture +def compatible_images(test_image): + return type('CompatibleImages', (), { + 'content': test_image('canoe_water.jpg'), + 'style': test_image('blue-red-w400-h300.jpg'), + })() + +@pytest.fixture +def incompatible_image(test_image): + return test_image('wikipedia-logo.png') + + +def test_image_manager(image_manager, compatible_images, incompatible_image): + assert image_manager.images_compatible == False + + image_manager.load_from_disk(compatible_images.content, 'content') + assert image_manager.images_compatible == False + + image_manager.load_from_disk(compatible_images.style, 'style') + assert image_manager.images_compatible == True + + image_manager.load_from_disk(incompatible_image, 'content') + assert image_manager.images_compatible == False + + with pytest.raises(ValueError): + image_manager.load_from_disk(compatible_images.content, 'unknown-type') diff --git a/tests/test_nst_observer.py b/tests/test_nst_observer.py new file mode 100644 index 0000000..0688b23 --- /dev/null +++ b/tests/test_nst_observer.py @@ -0,0 +1,60 @@ +import pytest + + +@pytest.fixture +def nst_observer(disk): + """An instance object of StylingObserver class.""" + from artificial_artwork.styling_observer import StylingObserver + from artificial_artwork.image.image_operations import convert_to_uint8 + return StylingObserver(disk.save_image, convert_to_uint8) + + + +@pytest.fixture +def subject_container(tmpdir): + import numpy as np + class TestSubject: + def __init__(self, subject): + self.subject = subject + + def build_state(self, runtime_data): + return { + 'output_path': tmpdir, + 'content_image_path': 'c', + 'style_image_path': 's', + 'metrics': { + 'iterations': runtime_data + }, + 'matrix': np.random.randint(0, high=255, size=(30, 40), dtype=np.uint8) + } + + def notify(self, runtime_data): + self.subject.state = type('Subject', (), self.build_state(runtime_data)) + self.subject.notify() + + return TestSubject + + +@pytest.fixture +def styling_observer_data(subject_container, nst_observer): + from artificial_artwork.utils.notification import Subject + return type('TestData', (), { + 'broadcaster': subject_container(Subject()), + 'observer': nst_observer, + }) + + +def test_styling_observer(styling_observer_data, subscribe, tmpdir): + d = styling_observer_data + # Subscribe observer/listener to subject/broadcaster at runtime + subscribe( + d.broadcaster.subject, + [d.observer] + ) + + import os + + d.broadcaster.notify(1) + assert os.path.isfile(os.path.join(tmpdir, 'c+s-1.png')) + d.broadcaster.notify(2) + assert os.path.isfile(os.path.join(tmpdir, 'c+s-2.png')) diff --git a/tests/test_session_runner.py b/tests/test_session_runner.py index 15d2756..f74c12b 100644 --- a/tests/test_session_runner.py +++ b/tests/test_session_runner.py @@ -13,8 +13,7 @@ def prod_session_runner(): @pytest.fixture def test_content_image(image_factory, test_image): - # 300 x 400 image - return image_factory.from_disk(test_image('canoe_water.jpg'), preprocess=True) + return image_factory.from_disk(test_image('canoe_water.jpg')) def test_session_runner_behaviour(prod_session_runner, test_content_image): diff --git a/tests/test_style_layers_cost.py b/tests/test_style_layers_cost.py deleted file mode 100644 index 0e3022c..0000000 --- a/tests/test_style_layers_cost.py +++ /dev/null @@ -1,19 +0,0 @@ -# import pytest - - -# @pytest.fixture -# def interactive_session(): -# import tensorflow as tf - -# # Reset the graph -# tf.compat.v1.reset_default_graph() - -# # Start interactive session -# sess = tf.compat.v1.InteractiveSession() - -# return sess - - -# def test_total_style_cost_computation(total_style_cost, interactive_session, session): -# with session(1) as test: - \ No newline at end of file diff --git a/tests/test_termination_condition_adapter.py b/tests/test_termination_condition_adapter.py index f708265..a511cba 100644 --- a/tests/test_termination_condition_adapter.py +++ b/tests/test_termination_condition_adapter.py @@ -7,29 +7,6 @@ def termination_condition_adapter(termination_condition): return TerminationConditionAdapterFactory.create('max-iterations', termination_condition_instance) -@pytest.fixture -def broadcaster_class(): - class TestSubject: - def __init__(self, subject, done_callback): - self.subject = subject - self.done = done_callback - - def iterate(self): - i = 0 - while not self.done(): - # do something in the current iteration - print('Iteration with index', i) - - # notify when we have completed i+1 iterations - self.subject.state = type('Subject', (), { - 'metrics': {'iterations': i + 1}, # we have completed i+1 iterations - }) - self.subject.notify() - i += 1 - return i - - return TestSubject - @pytest.fixture def test_objects(broadcaster_class, termination_condition_adapter): diff --git a/tox.ini b/tox.ini index 8c7db1a..7d9bb6f 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = mypy, clean, dev [testenv] +passenv = AA_VGG_19 setenv = PYTHONHASHSEED=2577074909 MYPYPATH={toxinidir}/src/stubs @@ -22,6 +23,13 @@ commands = pytest {posargs} --cov -vv --junitxml={env:TEST_RESULTS_DIR:test-resu [testenv:dev] basepython = {env:TOXPYTHON:python} +; commands = pytest --disable-warnings {posargs} -vv +commands = pytest --disable-warnings {posargs} --cov -vv --junitxml={env:TEST_RESULTS_DIR:test-results}/{env:JUNIT_TEST_RESULTS:junit-test-results.xml} + +[testenv:run] +basepython = {env:TOXPYTHON:python} +; commands = neural-style-transfer {toxinidir}/tests/data/canoe_water.jpg {toxinidir}/tests/data/blue-red-w400-h300.jpg --iterations 103 --location {env:TEST_RESULTS_DIR} +commands = neural-style-transfer {posargs} --iterations 600 --location nst_output [testenv:test38] @@ -183,7 +191,7 @@ deps = pylint==2.7.4 skip_install = false use_develop = true -commands = python -m pylint {posargs:{toxinidir}/src/neural_style_transfer} +commands = python -m pylint {posargs:{toxinidir}/src/artificial_artwork} ## GENERATE ARCHITECTURE GRAPHS @@ -262,7 +270,7 @@ deps = skip_install = false use_develop = true commands_pre = - - python -c 'import os; my_dir = os.getcwd(); os.mkdir(os.path.join(my_dir, "{env:UML_DIAGRAMS}"))' + python -c 'from glob import glob; import os; dir = os.path.join("{toxinidir}", "{env:UML_DIAGRAMS}"); exec("if not os.path.isdir(dir):\n os.mkdir(dir)\nelse:\n _ = [os.remove(x) for x in glob(dir+\"/*\")]")' commands = python -c 'import sys; print(sys.path)'