2
\$\begingroup\$

This post is a continuation of post here: Create mosaic task.

  • I improved code readability, hopefully it'll be reviewable now. Once again, I'm asking for tips to write better code.

Task: The point of this task is to create a service, which will generate a mosaic for given images downloaded from provided URLs.

  • mosaic.py takes a list of images in cv2 format (for example jpg) and creates a mosaic from them. server.py allows to run a server on your computer from command line, so by entering localhost:8080 in your web browser you can provide a link with urls. The server downloads all images and passes it to the mosaic function, so the mosaic is displayed in the web browser.

Example with 3 images: When this URL is provided, one of possible outcomes: http://localhost:8080/mozaika?losowo=1&rozdzielczosc=512x512&zdjecia=https://www.humanesociety.org/sites/default/files/styles/768x326/public/2018/08/kitten-440379.jpg?h=f6a7b1af&itok=vU0J0uZR,https://cdn.britannica.com/67/197567-131-1645A26E.jpg,https://images.unsplash.com/photo-1518791841217-8f162f1e1131?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&w=1000&q=80 enter image description here

To run:

mozaika.py:

import cv2
import numpy as np
import random
from functools import reduce


class Image:
    def __init__(self, image):
        self._image = image
        self.height, self.width = image.shape[:2]

    @property
    def ratio(self):
        return max(self.height, self.width) / min(self.height, self.width)

    def square(self):
        if self.height > self.width:
            cut = int((self.height - self.width) / 2)
            return Image(self._image[cut : -cut, :self.width])
        else:
            cut = int((self.width - self.height) / 2)
            return Image(self._image[:self.height, cut : -cut])

    def make_rectangle1x2(self, vertical=True): #check later if it works for images with high ratio ver/hor
        ratio = self.ratio
        if vertical:
            if ratio < 2:
                cut = int((self.width - ratio * self.width / 2) / 2)
                return Image(self._image[: self.height, cut : -cut])
            elif ratio > 2:
                cut = int((self.width - 2 * self.width / ratio) / 2)
                return Image(self._image[cut : -cut, : self.width])
            return self
        else:
            if ratio < 2:
                cut = int((self.height - ratio * self.height / 2) / 2)
                return Image(self._image[cut : -cut, : self.width])
            elif ratio > 2:
                if self.width > self.height:
                    cut = int((self.height - 2 * self.height / ratio) / 2)
                    return Image(self._image[: self.height, cut : -cut])
            return self        

    def resize(self, dimensions, final=False):
        if final: # returns numpy array
            return cv2.resize(self._image, dimensions)
        else: # returns Image class object
            return Image(cv2.resize(self._image, dimensions)) 

    def split(self, vertical=True):
        if vertical:
            return Image(self._image[: int(self.height/2), :]), Image(self._image[int(self.height/2) : self.height, :])
        else:
            return Image(self._image[: , : int(self.width/2)]), Image(self._image[: , int(self.width/2) : self.width])

    def merge(self, other, horizontally=True):
        axis = 0 if horizontally else 1
        return Image(np.concatenate([self._image, other._image], axis=axis))


class Mozaika:
    def __init__(self, image_list, losowo=1, w=2048, h=2048):
        self.losowo = losowo # defines whether image position is random
        self.w = int(w) # width of output image
        self.h = int(h) # height of output image
        self.output_image = 0

        self.images = [Image(i) for i in image_list]
        if self.losowo == 1:
            random.shuffle(self.images)

        self.how_many_images()

    @property
    def big_image(self):
        return int(self.w*2/3), int(self.h*2/3)

    @property
    def medium_image(self):
        return int(self.w/2), int(self.h/2)

    @property
    def small_image(self):
        return int(self.w/3), int(self.h/3)

    def big_rectangle_image(self, vertical=True):
        if vertical:
            return int(self.w/2), self.h
        else:
            return self.w, int(self.h/2)

    def small_rectangle_image(self, vertical=True):
        if vertical:
            return int(self.w/3), int(self.h*2/3)
        else:
            return int(self.w*2/3), int(self.h/3)

    def how_many_images(self):
        number_of_images = len(self.images) # checks how many images is given
        if number_of_images == 1:
            self.output_image = self.images[0].square().resize(dimensions=(self.w, self.h), final=True)
        elif 2 <= number_of_images <= 4:
            self.output_image = self.merge2x2().resize(dimensions=(self.w, self.h), final=True)
        elif number_of_images > 4:
            self.output_image = self.merge3x3().resize(dimensions=(self.w, self.h), final=True)

    def merge2x2(self):
        placement = self.put_image2x2()
        row1 = placement[0].merge(placement[1], horizontally=False)
        row2 = placement[2].merge(placement[3], horizontally=False)
        return row1.merge(row2, horizontally=True)

    def merge3x3(self):
        placement = self.put_image3x3()
        row1 = reduce(lambda x, y: x.merge(y, horizontally=False), placement[:3])
        row2 = reduce(lambda x, y: x.merge(y, horizontally=False), placement[3:6])
        row3 = reduce(lambda x, y: x.merge(y, horizontally=False), placement[6:9])
        rows = row1.merge(row2, horizontally=True)
        rows = rows.merge(row3, horizontally=True)
        return rows

    def put_image2x2(self):
        placement = [0]*4 # four possible image positions
        # 2 images
        if len(self.images) == 2:
            image1, vertical, num = self.find_rectangle_image() # finds image with greatest ratio and shapes it 1x2
            # vertical is boolean value, num is an index of image with highest ratio
            image2 = [e for e in self.images if self.images.index(e) != num][0]
            image2 = image2.make_rectangle1x2(vertical=vertical) # shaping second image
            image1 = image1.resize(self.big_rectangle_image(vertical=vertical))
            image2 = image2.resize(self.big_rectangle_image(vertical=vertical))

            if self.losowo == 1: # find_rectangle fixes image position, it shuffles them again
                shuffle = random.randrange(0,2)
                if shuffle:
                    image1, image2 = image2, image1

            if vertical:
                placement[0], placement[2] = image1.split(vertical=True)
                placement[1], placement[3] = image2.split(vertical=True)
            else:
                placement[0], placement[1] = image1.split(vertical=False)
                placement[2], placement[3] = image2.split(vertical=False)

        # 3 images
        elif len(self.images) == 3:
            rect_image, vertical, num = self.find_rectangle_image() # finds image with greatest ratio and shapes it 1x2
            other_images = [e for e in self.images if self.images.index(e) != num]
            rect_image = rect_image.resize(self.big_rectangle_image(vertical=vertical))
            all_positions = [e for e in range(4)]

            if vertical:
                if self.losowo == 1:
                    position = random.randrange(0,2) # choose random position for image
                else:
                    position = 0
                all_positions.remove(position) # rectangle image takes 2 places
                all_positions.remove(position + 2)
                placement[position], placement[position + 2] = rect_image.split(vertical=True)
            else:
                if self.losowo == 1:
                    position = random.randrange(0, 3, 2)
                else:
                    position = 0
                all_positions.remove(position)
                all_positions.remove(position + 1)
                placement[position], placement[position + 1] = rect_image.split(vertical=False)

            var = 0
            for i in all_positions:
                placement[i] = other_images[var].square().resize(self.medium_image)
                var += 1

        # 4 images
        elif len(self.images) == 4:
            placement = [e.square().resize(self.medium_image) for e in self.images]

        return placement

    def put_image3x3(self):
        placement = [0]*9
        img2x = [] # list of rectangle images
        img4x = [] # list of big square images 2x2
        num_img = len(self.images)
        while num_img < 9:
            if 9 - num_img < 3: # big image can't fit, increase number of images by making rectangles
                rect_image, vertical, num = self.find_rectangle_image() # finds most rectangle image and shapes it
                img2x.append([rect_image, vertical])
                del self.images[num]
                num_img += 1
            else:
                square_img = min(enumerate(self.images), key=lambda i: abs(i[1].ratio) - 1) # get image with 1:1 ratio
                img4x.append(square_img[1].square())
                del self.images[square_img[0]]
                num_img += 3

        all_positions = [e for e in range(9)]
        for img in img4x:
            img = img.resize(self.big_image)
            hor_img1, hor_img2 = img.split(vertical=False) # making 2 rectanles and then 4 small squares
            img1, img2 = hor_img1.split(vertical=True)
            img3, img4 = hor_img2.split(vertical=True)
            all_positions, position = self.find_big_position(avaiable_pos=all_positions)
            placement[position] = img1.resize(self.small_image)
            placement[position + 1] = img3.resize(self.small_image)
            placement[position + 3] = img2.resize(self.small_image)
            placement[position + 4] = img4.resize(self.small_image)

        for img in img2x: # takes rectangles and tries to fit them
            rect_image, vertical = img
            if vertical:
                rect_image = rect_image.resize(self.small_rectangle_image(vertical=True))
                img1, img2 = rect_image.split(vertical=True)
                all_positions, position = self.find_vertical_position(avaiable_pos=all_positions) # checks for vertical possibilities
                placement[position] = img1.resize(self.small_image)
                placement[position + 3] = img2.resize(self.small_image)
            else:
                rect_image = rect_image.resize(self.small_rectangle_image(vertical=False))
                img1, img2 = rect_image.split(vertical=False)
                all_positions, position = self.find_horizontal_position(avaiable_pos=all_positions) # checks for horizontal possibilities
                placement[position] = img1.resize(self.small_image)
                placement[position + 1] = img2.resize(self.small_image)

        num = 0
        for i in all_positions: # after puting bigger image fill other places with smaller images
            placement[i] = self.images[num].square().resize(self.small_image)
            num += 1

        return placement

    def find_rectangle_image(self):
        enum_largest = max(enumerate(self.images), key=lambda i: i[1].ratio)
        largest = enum_largest[1]
        maxratio = largest.ratio

        if largest.width > largest.height:
            return largest.make_rectangle1x2(vertical=False), False, enum_largest[0]
        else:
            return largest.make_rectangle1x2(vertical=True), True, enum_largest[0]

    def find_big_position(self, avaiable_pos):
        # find position for 2/3 width/height image
        myList = avaiable_pos
        mylistshifted=[x-1 for x in myList]
        possible_position = [0,1,3,4] # only possible possisions for big image
        intersection_set = list(set(myList) & set(mylistshifted) & set(possible_position))
        if self.losowo == 1:
            position = random.choice(intersection_set)
        else:
            position = intersection_set[0]
        myList = [e for e in myList if e not in (position, position + 1, position + 3, position + 4)]
        return myList, position

    def find_vertical_position(self, avaiable_pos):
        # find position vertical rectangle image
        myList = avaiable_pos
        mylistshifted=[x-3 for x in myList]
        possible_position = [e for e in range(6)] # positions where image is not cut in half
        intersection_set = list(set(myList) & set(mylistshifted) & set(possible_position))
        if self.losowo == 1:
            position = random.choice(intersection_set)
        else:
            position = intersection_set[0]
        myList.remove(position) # removes places from other_position, so no other image can take these places
        myList.remove(position + 3)
        return myList, position

    def find_horizontal_position(self, avaiable_pos):
        # find position for horizontal rectangle image
        myList = avaiable_pos
        mylistshifted=[x-1 for x in myList]
        possible_position = [0,1,3,4,6,7] # positions where image is not cut in half
        intersection_set = list(set(myList) & set(mylistshifted) & set(possible_position))
        if self.losowo == 1:
            position = random.choice(intersection_set)
        else:
            position = intersection_set[0]
        myList.remove(position) # removes places from other_position, so no other image can take these places
        myList.remove(position + 1)
        return myList, position

if __name__ == "__main__": # check if it's working with local files
    image_names = ["img5.jpg", "img7.jpg"] # enter image names here
    image_list = [cv2.imread(e) for e in image_names]
    mozaika = Mozaika(image_list)
    cv2.imshow("i", mozaika.output_image)
    cv2.waitKey(0)

server.py

from http.server import HTTPServer, BaseHTTPRequestHandler
import re
from urllib.request import urlopen
import cv2
import numpy as np
from mozaika import Mozaika


class Serv(BaseHTTPRequestHandler):
    def do_GET(self):
        w = 2048 # default width
        h = 2048 # default height
        losowo = 1 # random image placement = true
        urls = [] # images URLs
        if self.path.startswith("/mozaika?"): # keyword for getting mosaic, URL should be put in format:
            parameters = self.path.split("&") # http://localhost:8080/mozaika?losowo=Z&rozdzielczosc=XxY&zdjecia=URL1,URL2,URL3..
            for par in parameters:
                if par.find("losowo") == -1:
                    pass
                else:
                    losowo_index = par.find("losowo")
                    try:
                        losowo = int(par[losowo_index + 7])
                    except:
                        pass

                if par.find("rozdzielczosc") == -1:
                    pass
                else:
                    try:
                        w, h = re.findall('\d+', par)
                    except:
                        pass

                if par.find("zdjecia=") == -1:
                    pass
                else:
                    urls = self.path[self.path.find("zdjecia=") + 8 :]
                    urls = urls.split(",")

            try:
                image_list = create_images_list(urls)   
                # call mosaic creator
                # 1 required attribute: list of images in cv2 format,
                # 3 optional attributes: random image positioning, width of output image, height of output image
                mozaika = Mozaika(image_list, losowo, w, h)
                img = mozaika.output_image # store output image

                f = cv2.imencode('.jpg', img)[1].tostring() # encode to binary format
                self.send_response(200)
                self.send_header('Content-type', 'image/jpg')
            except:
                self.send_response(404)
            self.end_headers()
            self.wfile.write(f) # send output image
                #return


def url_to_image(url):
    # gets image from URL and converts it to cv2 color image format
    resp = urlopen(url)
    image = np.asarray(bytearray(resp.read()), dtype="uint8")
    image = cv2.imdecode(image, cv2.IMREAD_COLOR)
    return image

def create_images_list(urls):
    # takes URLs list and creates list of images
    image_list = []
    for url in urls:
        image = url_to_image(url)
        if image is not None:
            image_list.append(image)
    return image_list

httpd = HTTPServer(("localhost", 8080), Serv)
httpd.serve_forever()
\$\endgroup\$
  • \$\begingroup\$ Wouldn't it be easier to give those two variables a descriptive English name instead of describing them in great detail in the question? \$\endgroup\$ – AlexV Jun 22 at 8:12
  • \$\begingroup\$ No. As you can see server.py has to accept polish words. Here is the question too, if I should change variable name as fast as possible or be consistent with names. \$\endgroup\$ – Plajerity Jun 22 at 13:24
  • \$\begingroup\$ Wow, I'm slacking. I only just saw this question! It looks like you've followed the points I outlined in my previous question pretty well, good job! \$\endgroup\$ – Peilonrayz Jun 27 at 9:10
  • \$\begingroup\$ Thank you :) I need to update the code so it can fit in 80 char/line, but I'm not sure if I can do this without losing some clarity. Or maybe I shoudn't care and just try to fit in in 100 lines? Any other sugestion? \$\endgroup\$ – Plajerity Jun 30 at 18:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.