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\$
4
  • \$\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\$ Commented Jun 22, 2019 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\$ Commented Jun 22, 2019 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\$ Commented Jun 27, 2019 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\$ Commented Jun 30, 2019 at 18:57

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.