Last active
June 22, 2021 09:23
-
-
Save nonchris/9a4f1e32e1c801d4779fa2462fcf327f to your computer and use it in GitHub Desktop.
A task for the lecture 'computational itelligence' at the univeristy of bonn. It places three images from the MNIST dataset 'randomized' on a 64x64 image.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import random | |
from typing import Tuple, List | |
import numpy as np | |
import numpy.typing as npt | |
import torch | |
""" | |
This is a sub-task for the lecture 'computational itelligence' at the univeristy of bonn. | |
It places three images from the MNIST dataset 'randomized' on a 64x64 image. | |
""" | |
class ImageGenerator: | |
""" | |
Class to handle the positioning of three sub images and the generation of those label-cards\n | |
\n | |
The the values in __init__() are hardcoded to a base-image of 64x64 and sub-images of 28x28.\n | |
Some functions assume that y and x dimensions are the same and that the shapes are even.\n | |
\n | |
Internal processes are based on the mutability of np.array and torch.tensor\n | |
-> only references to an array will be passed into a function, the "outer" array will also be changed | |
""" | |
def __init__(self): | |
# we need to place the images without colliding. | |
# so imagine we split the whole field in four equal sectors. | |
# the anchorpoint (upper left corner) of our sub-image can only be located in a small field | |
# so that the whole 28x28 image fits into the 32x32 sector it can be placed in. | |
# those two lists represent the coordinates for those corners (see names) | |
self.left_or_upper_offset = [i for i in range(0, 5)] # upper for y axis, left offsite for x axis | |
self.right_or_lower_offset = [i for i in range(32, 37)] # lower for y axis, right for x-axis | |
# tuples that represent the y and x coordinates that describe one of those anchorpoint fields | |
# ([y values], [x values]) | |
self.upper_left = (self.left_or_upper_offset, self.left_or_upper_offset) | |
self.lower_left = (self.right_or_lower_offset, self.left_or_upper_offset) | |
self.upper_right = (self.left_or_upper_offset, self.right_or_lower_offset) | |
self.lower_right = (self.right_or_lower_offset, self.right_or_lower_offset) | |
# the random field selection is done by getting a random integer | |
# using enumeration from cartesian sectors | |
# fields that have been used will be set to None | |
# using integers has also the advantage that we're able to detect whether two left fields are adjacent | |
self.field_map = { | |
0: self.upper_right, | |
1: self.upper_left, | |
2: self.lower_left, | |
3: self.lower_right | |
} | |
def put_sub_images(self, sub_images, base_image=None, shape=(64, 64)): | |
""" | |
Main function to be called by the user.\n | |
Takes up to three sub-images and places them randomized on a - possibly new generated - tensor | |
:param sub_images: List[Tuple[torch.tensor[float, float], int] - the tensors must be 2 dimensional!\n | |
List of three or less Tuples. | |
First entry of the Tuple is sub-image that shall be placed onto the base-image | |
Second entry is the actual value (int) shown on the image that is placed | |
:param base_image: optional base image (2dim torch.tensor) as input, create new (zeroes) if None is given | |
:param shape: shape of the base image (y_axis, x_axis), used to generate a new array if none is given | |
:returns: base_image (2dim torch.tensor) of given shape, label cards as torch.tensor([10, shape[0], shape[1]]) | |
Label cards are in "natural" order - cards[0] label card for value 0, ..., cards[9] card for value 9 | |
------- | |
""" | |
# check if base image was given - generate new one if not | |
if base_image is None: | |
base_image = torch.zeros(shape[0], shape[1]) | |
# generate list of label cards | |
label_cards = self.generate_label_cards(base_image.shape[0], 10) | |
# print(label_cards) | |
# iterate over sub-images to place them random in the base image | |
for sub_image in sub_images: | |
content = sub_image[1] # extract value shown on image, just for readability :) | |
# print(f"{len(sub_image)=}") | |
# print(sub_image[1]) | |
# print(f"{label_cards[content]=}") | |
# place sub-image in base-image, mark on label-card that belongs to this value | |
self.__put_sub_image(base_image, sub_image[0], label_cards[content]) | |
# print(base_image) | |
# resetting fields for next round, since we've set some to the value of None | |
# not the best solution, but it does the job and this is the only function that shall be called by the user | |
self._reset_fields() | |
# print(torch.from_numpy(np.array(label_cards)).shape) | |
return base_image, torch.from_numpy(np.array(label_cards)) | |
def _select_field(self) -> Tuple[int, int]: | |
""" | |
Selects a random field that's still free.\n | |
Chooses a random value from the set of possible anchor points for that field.\n | |
If only tow fields left: concatenate those fields if adjacent | |
:return: y and x anchorpoint to place the sub-image | |
""" | |
# get fields that are still available - fields that have the value None will be ignored | |
fields = [x for x in self.field_map.keys() if self.field_map[x]] | |
# only relevant when only two entries are left | |
# needed to decide whether those fields are adjacent | |
mx: int = max(fields) | |
mn: int = min(fields) | |
# check if only two fields are left - concat and return if possible | |
# subtracting the greater from the smaller | |
# if the delta value is 1 or 3 this means the fields are adjacent (2 means that they are diagonal) | |
if len(fields) == 2 and mx - mn in [1, 3]: | |
y_options, x_options = self.__concat_fields(mn, mx) | |
return random.choice(y_options), random.choice(x_options) | |
# merge not possible select single available field | |
choice = random.choice(fields) | |
# print(f"{choice=}") | |
# choose random coordinate from from possible anchor points | |
y_val = random.choice(self.field_map[choice][0]) | |
x_val = random.choice(self.field_map[choice][1]) | |
# set fields value to None so it won't be selected again | |
self.field_map[choice] = None | |
return y_val, x_val | |
def __concat_fields(self, key1: int, key2: int) -> Tuple[List[int], List[int]]: | |
""" | |
Used to concatenate two fields.\n | |
This is used when only tow (adjacent) fields are left in the dict.\n | |
Enables a more random pattern if we merge the last two empty sectors | |
:param key1: first field left | |
:param key2: second field to merge | |
:return: Tuple: all possible y-anchorpoints, possible x-anchorpoints | |
""" | |
# get anchor fields coordinates | |
y_1, x_1 = self.field_map[key1] | |
y_2, x_2 = self.field_map[key2] | |
# merge field coords to set | |
y_joined = {*y_1, *y_2} | |
x_joined = {*x_1, *x_2} | |
# print(f"{y_joined=}") | |
# print(f"{x_joined=}") | |
# generate all possible coords by generating all values between lowest and greatest possible coordinate | |
# plus one since end of range is an open interval and we want to include the last coordinate as well | |
y_options = [i for i in range(min(y_joined), max(y_joined) + 1)] | |
x_options = [i for i in range(min(x_joined), max(x_joined) + 1)] | |
# print(f"{y_options=}") | |
# print(f"{x_options=}") | |
return y_options, x_options | |
def __put_sub_image(self, base_image: npt.ArrayLike, sub_image: npt.ArrayLike, label_card: np.ndarray): | |
""" | |
Internal function to place a single sub-image on a yet to determine place in the base-image.\n | |
Marks position of the sub_image on the label card | |
:param base_image: ArrayLike with 2 dims - Image to place the sub_image in | |
:param sub_image: ArrayLike with 2 dims - Image that shall be placed onto base_image | |
:param label_card: NumpyArray with 2 dims - Label card of the number that is shown on the sub_image | |
""" | |
# get y and x positions to place the array at | |
y_pos, x_pos = self._select_field() | |
# print(sub_image) | |
# print(type(sub_image)) | |
# get size of sub_image - needed to determine end of slice to place sub-image in | |
y_size_img = sub_image.shape[0] | |
x_size_img = sub_image.shape[1] | |
# print(f"{y_pos=}, {x_pos=}") | |
# print(f"{y_size_img} {x_size_img}") | |
# place sub-image based on y and x anchor-points | |
base_image[y_pos:y_pos + y_size_img, x_pos:x_pos + x_size_img] = sub_image | |
# mark position on label card | |
self._mark_position(label_card, y_pos, x_pos, placed_image_size=sub_image.shape[0]) | |
@staticmethod | |
def _mark_position(label_card: np.ndarray, y_pos: int, x_pos: int, placed_image_size=28, label_size=4, smb_number=1): | |
""" | |
Mark center of placed sum image with centered filed of ones\ņ | |
function can fail if placed_image_size minus label_size results in an odd value! | |
:param label_card: card to mark the position on | |
:param y_pos: anchor-point of placed image on y axis | |
:param x_pos: anchor-point of placed image on x axis | |
:param placed_image_size: size of the sub-image that was placed, needed to determine center of image | |
:param label_size: size of the square that shall mark the middle of the sub-image | |
:param smb_number: symbol number that shall be placed in the middle as mark of the center | |
""" | |
# generate array to position | |
label = np.full((label_size, label_size), smb_number) | |
# print() | |
# print(f"{placed_image_size=}") | |
# print(f"{y_pos=}") | |
# determine value to shift y_pos and x_pos to get the anchor point of the sub-images center | |
# the place odd values can cause some uncool errors, but we've only got even numbers in this task - ignored | |
shift = (placed_image_size - label_size) // 2 | |
# print(f"{shift=}") | |
# determine anchor-points of the center field | |
y_anchor_point = y_pos + shift | |
x_anchor_point = x_pos + shift | |
# print(f"{y_anchor_point=}") | |
# print(f"{x_anchor_point=}") | |
# place label array in at the determined position | |
label_card[y_anchor_point:y_anchor_point+label_size, x_anchor_point:x_anchor_point+label_size] = label | |
# print(label_card) | |
@staticmethod | |
def generate_label_cards(size: int, amount: int): | |
""" | |
Generate given amount of label cards of wanted size | |
:return: List[np.ndarray, ...] list of as many numpy arrays as wished - typehints for numpy suck | |
""" | |
# generate zeroed arrays of given size | |
return [np.zeros((size, size)) for i in range(0, amount)] | |
def _reset_fields(self): | |
""" | |
Just resetting the internal dict, to be called when we're done with placing all sub-images | |
""" | |
self.field_map = { | |
0: self.upper_right, | |
1: self.upper_left, | |
2: self.lower_left, | |
3: self.lower_right | |
} | |
# if __name__ == '__main__': | |
# image = np.arange(0, 100).reshape((10, 10)) | |
# # image = np.zeros((64, 64)) | |
# | |
# sub_img1 = np.array([ | |
# [1, 1, 1, 0], | |
# [0, 0, 1, 0], | |
# [0, 1, 1, 1], | |
# [0, 0, 1, 0], | |
# | |
# ], dtype=np.float64) | |
# | |
# print(image) | |
# print(sub_img1) | |
# | |
# # place_image(image, sub_img1) | |
# | |
# generator = ImageGenerator() | |
# | |
# subimages = [(sub_img1, 1), (sub_img1, 2), (sub_img1, 3)] | |
# base_image, labels = generator.put_sub_images(subimages, base_image=image) | |
# | |
# print() | |
# print(base_image) | |
# print(labels) | |
# generator.mark_position() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment