Skip to main content


Create Droste Effect Using Python for NFT

Incease you wonder what is Droste Effect. This is what a Droste Effect looks like.

Droste Effect Using Python for NFT

History:- Droste Effect was one of the options in Pixel bender which was an addon of Photoshop CS5 which was fun to use, but later it was discontinued for unknown reasons in later Photoshop versions. So I thought there could be a posibility if the same could be achieved using Python. Polishing the code is still in progress but the end result is quite good enough for now.


Let me show you how I made Droste Effect using python with the help of Google Colab. You can use it for creating NFTs. Or try it out from my github repositories. (Link Below)


I will explain the code here.
This Code will remove the sample_data folder, we do not need it. By default when ever you create a new google colab it will automatically generate one for you to use and testing out excel data with python and other programs.

#@title Remove sample_data
!rm -rf sample_data

Add input of your desired Image URL and the Code will download it for you and rename it as test.jpg automatially. JPG or PNG won't make a difference it still works the same.

#@markdown Download image from web
import os
Download_Image = "https://m.media-amazon.com/images/I/812L5zyAmpL._AC_SX522_.jpg" #@param {type:"string"}
os.popen(f"""wget {Download_Image} -O 'test.jpg'""").read()
print("Downloaded as test.jpg!!!")

And this is the Main Code which makes the magic happen

#@title ***Droste Effect Input***
import numpy as np
import cv2
import random
#import sys
import os.path
from IPython.display import Image
# Math explained here: http://www.josleys.com/article_show.php?id=82

class Droste:
    def __init__(self, fPath, repeatsX, repeatsY, r1, r2, xRange, yRange, resultFolderAddress):
        self.repeatsX = repeatsX
        self.repeatsY = repeatsY
        self.r1 = min(r1, r2)
        self.r2 = max(r1, r2)
        self.inputImg = cv2.imread(fPath, cv2.IMREAD_UNCHANGED) #returns a matrix-like object
        self.xRange = xRange
        self.yRange = yRange
        self.resultFolderAddress = resultFolderAddress

    def Transform(self, method):
        # Builds  new plane
        img, r, c, Z = self.CreateComplexPlane()
        if method == 1:
            # 1: Ln -> Tile   -> Rotate -> exponent
            self.Calculate1(Z, r, c)  # takes in the complex plane, outputs the transformed plane
        else:
            # 0: Ln -> Rotate -> Tile   -> exponent
            self.Calculate(Z, r, c)  # takes in the complex plane, outputs the transformed plane


    def CreateComplexPlane(self):
        '''
        :param self: loads the input image
        :return: the image as matrix, nbr of rows, nbr of cols, complex plane
        '''
        img = self.inputImg
        r = img.shape[0] #nbr of rows
        c = img.shape[1] #nbr of cols
        x = np.linspace(-self.xRange, self.xRange, num=r, endpoint=True, dtype=np.float32) #Returns an array of evenly spaced numbers over a specified interval.
        y = np.linspace(-self.yRange, self.yRange, num=c, endpoint=True, dtype=np.float32)
        X, Y = np.meshgrid(x, y, indexing='ij') #creates rect grid off arrays, here it uses Matrix indexing. Returns X, Y coordinates
        Z = X + (1j * Y); # Transf to the Complex Plane (then we will transform back to Cartesian) 1J is the imaginary part.
        return img, r, c, Z



    def TILE(self, W, r, c, repeatX, repeatY):
        X = W.real
        Y = W.imag

        xOffset = np.log(self.r2 / self.r1)
        repeatYHalf = int((repeatY - 1)/2)
        repeatXHalf = int((repeatX - 1)/2)
        for i in range(-repeatYHalf, repeatYHalf + 1):
            W2 = None
            for j in range(-repeatXHalf, repeatXHalf + 1):
                if 0 == repeatXHalf + j:
                    W2 = X + j * xOffset + (Y + 2 * np.pi * i) * 1j
                else:
                    W2 = np.concatenate([W2, X + j * xOffset + (Y + np.pi*2*i) * 1j], axis=0)
            if 0 == repeatYHalf + i:
                W1 = W2
            else:
                W1 = np.concatenate([W1, W2], axis=1)
        return W1


    def TILE2(self, W, X, Y, repeatX, repeatY):
        X = W.real
        Y = W.imag

        repeatYHalf = int((repeatY - 1)/2)
        repeatXHalf = int((repeatX - 1)/2)

        alpha = np.arctan2(np.log(max(self.r2,self.r1) / min(self.r1, self.r2)), 2 * np.pi) # we need alpha to calculate the offsets
        cosine = np.cos(alpha)
        sine = np.sin(alpha)
        yOffset = 2 * np.pi * sine * sine
        xOffset = 2 * np.pi * sine * cosine

        for i in range(-repeatYHalf, repeatYHalf + 1):
            W2 = None
            for j in range(-repeatXHalf, repeatXHalf + 1):
                if 0 == repeatXHalf + j:
                    W2 = X + j * xOffset + ((Y + (2 * np.pi * i + yOffset * j)) * 1j)
                else:
                    W2 = np.concatenate([W2, X + j * xOffset + ((Y + (2 * np.pi * i + yOffset * j)) * 1j)], axis=0)
            if 0 == repeatYHalf + i:
                W1 = W2
            else:
                W1 = np.concatenate([W1, W2], axis=1)

        return W1



    def LogTransform(self, Z, r, c):
        '''
        :param Z: Complex plane
        :param r: nbr of rows
        :param c: nbr of cols
        :return: np.log(z / min(r1, r2))
        '''
        Z1 = np.zeros([r, c], dtype=complex)

        ZAbs = np.absolute(Z)
        elemetsToApplyTransformation = (self.r1 <= ZAbs) & (ZAbs <= self.r2)

        Z1[elemetsToApplyTransformation] = np.log(Z[elemetsToApplyTransformation])

        return Z1


    def ExponentTransform(self, Z, r, c, repeatX, repeatY):
        Z1 = Z[:r * repeatX, :c * repeatY].copy()
        Z1 = np.exp(Z)
        return Z1


    def RotationTransform(self, Z, r, c, repeatX, repeatY):
        Z1 = Z[:r * repeatX, :c * repeatY].copy()
        alpha = np.arctan2(np.log(self.r2 / self.r1), 2 * np.pi)
        f = np.cos(alpha)

        Z1 = Z * f * np.exp(1j * alpha)
        return Z1


    def Calculate(self, Z, r, c):
        print(Z.shape)
        W1 = self.LogTransform(Z, r, c)
        # self.ReCreateImageAndSave(W1, "log-{}.{}-{}.{}-{}.jpg".format(0, self.r1, self.r2, 1, 1), r, c, 1, 1)

        W1 = self.RotationTransform(W1, r, c, 1, 1)
        # self.ReCreateImageAndSave(W1, "rot-{}.{}-{}.{}-{}.jpg".format(0, self.r1, self.r2, 1, 1), r, c, 1, 1)

        repeatX = self.repeatsX
        repeatY = self.repeatsY
        W1 = self.TILE2(W1, r, c, repeatX, repeatY)
        print(W1.shape)
        # self.ReCreateImageAndSave(W1, "tile-{}.{}-{}.{}-{}.jpg".format(0, self.r1, self.r2, repeatX, repeatY), r, c, repeatX, repeatY)

        W1 = self.ExponentTransform(W1, r, c, repeatX, repeatY)
        self.ReCreateImageAndSave(W1, "final-{}.{}-{}.{}-{}.jpg".format(0, self.r1, self.r2, repeatX, repeatY), r, c, repeatX, repeatY)
        return W1
        
    def Calculate1(self, Z, r, c):
        print(Z.shape)
        # apply Ln(z/r1)
        W1 = self.LogTransform(Z, r, c)
        # self.ReCreateImageAndSave(W1, "log-{}.{}-{}.{}-{}.jpg".format(1, self.r1, self.r2, 1, 1), r, c, 1, 1)

        # choose how many tiles we want to add
        repeatX = self.repeatsX
        repeatY = self.repeatsY
        # apply Tile
        W1 = self.TILE(W1, r, c, repeatX, repeatY)
        print(W1.shape)
        # self.ReCreateImageAndSave(W1, "rot-{}.{}-{}.{}-{}.jpg".format(1, self.r1, self.r2, repeatX, repeatY), r, c, repeatX, repeatY)

        # apply Rotation
        W1 = self.RotationTransform(W1, r, c, repeatX, repeatY)
        # self.ReCreateImageAndSave(W1, "tile-{}.{}-{}.{}-{}.jpg".format(1, self.r1, self.r2, repeatX, repeatY), r, c, repeatX, repeatY)

        # apply exponent
        W1 = self.ExponentTransform(W1, r, c, repeatX, repeatY)
        self.ReCreateImageAndSave(W1, "final-{}.{}-{}.{}-{}.jpg".format(1, self.r1, self.r2, repeatX, repeatY), r, c, repeatX, repeatY)
        return W1
        Image(open(W1, "final-{}.{}-{}.{}-{}.jpg".format(0, self.r1, self.r2, repeatX, repeatY), r, c, repeatX, repeatY,'rb').read())


    def makeNewXY(self, W, wmax, x):
        return np.multiply(np.add(np.divide(W, wmax), 1), x / 2)

    def GetNewXY(self, W, r, c, repeatX, repeatY):
        Wx = np.real(W)
        Wy = np.imag(W)
        wxmax = np.absolute(Wx).max()
        wymax = np.absolute(Wy).max()
        XNew = self.makeNewXY(Wx, wxmax, c * repeatY)
        YNew = self.makeNewXY(Wy, wymax, r * repeatX)
        return XNew, YNew

    def ReCreateImage(self, Xnew, Ynew, img, r, c, repeatX, repeatY):
        '''
        :param Xnew:
        :param Ynew:
        :param img:
        :param r:
        :param c:
        :param repeat:
        :return: A matrix representing the pic in the new coordinates
        '''
        # newImg = np.zeros([r * repeatX, c * repeatY, 3])
        imgList = img.tolist()
        XnewList = Xnew.tolist()
        YnewList = Ynew.tolist()
        newImgList = [[[0, 0, 0] for j in range(c * repeatY)] for i in range(r * repeatX)]
        for i in range(r * repeatX):
            for j in range(c * repeatY):
                if int(XnewList[i][j]) == c * repeatY:
                    XnewList[i][j] = c * repeatY - 1
                if int(YnewList[i][j]) == r * repeatX:
                    YnewList[i][j] = r * repeatX - 1
                newImgList[int(YnewList[i][j])][int(XnewList[i][j])] = imgList[i % r][j % c][:3] # extract RGB from inputImage
        newImg = np.array(newImgList)
        return newImg

    # I know this is overkill
    # need to find a opencv equivalent
    # if you know tell me plz :)
    def FixBlackPixels(self, img, r, c, repeatX, repeatY):
        global image, limitR, limitC
        black = [0,0,0]
        image = img
        limitR = r * repeatX - 1
        limitC = c * repeatY - 1
        imgList = img.tolist()
        for i in range(limitR):
            for j in range(limitC):
                if imgList[i][j] == black:
                    n = 0
                    temp = [0,0,0]
                    # diagonal screws the result
                    for u, v in [(1, 0), (-1, 0), (2, 0), (-2, 0), (0, 1), (0, -1), (0, 2), (0, -2)]:
                        if not (u == 0 and v == 0):
                            if 0 <= i + u and i + u <= limitR and 0<= j + v and j + v <= limitC:
                                if imgList[i + u][j + v] != black:
                                    n += 1
                                    temp = [imgList[i + u][j + v][k] + temp[k] for k in range(3)]
                    if n != 0:
                        imgList[i][j] = [temp[k] / n for k in range(3)]

        img = np.array(imgList)
        return img



    def ReCreateImageAndSave(self, W, imgTitle, r, c, repeatX, repeatY):
        xNew, yNew = self.GetNewXY(W, r, c, repeatX, repeatY)
        img = self.ReCreateImage(xNew, yNew, self.inputImg, r, c, repeatX, repeatY)
        img = self.FixBlackPixels(img, r, c, repeatX, repeatY)

        # img[blackPixels] = cv2.blur(img.astype(np.float32), (5, 5))
        # img = cv2.medianBlur(img.astype(np.float32), 5)
        cv2.imwrite(self.resultFolderAddress + '/' + imgTitle, img)


def only_odd():
  lst = []
  for i in range(0,100):
    ele = int(i)
    lst.append(ele)
  return [num for num in lst if num % 2 == 1]


def GetCommandLineArgs():
    '''
    Gets command line argumets, does some error checking.
    :return: command line arguments
             fPath must be valid
             method must be 0 or 1
             repeats must be positive and odd
    '''  
    #@markdown Address of input picture
    fPath = '/content/test.jpg' #@param {type:"string"}
    #@markdown Method number is either 0 or 1
    method_ = "0" #@param [0, 1] 
    #@markdown RepeatsX must be positive and odd
    repeatsX_ = "3" #@param [1, 3, 5, 7, 9]
    #@markdown RepeatsY must be positive and odd
    repeatsY_ = "5" #@param [1, 3, 5, 7, 9]
    method = int(method_)
    repeatsX = int(repeatsX_)
    repeatsY = int(repeatsY_)
    if not os.path.isfile(fPath):
      print("Input picture does not exist")
    if not (method == 0 or method == 1):
      print("Method must be 0 or 1")
    if repeatsX <= 0 or repeatsX & 1 == 0:
      print("Repeats must be positive and odd")
    if repeatsY <= 0 or repeatsY & 1 == 0:
      print("Repeats must be positive and odd")
    return fPath, method, repeatsX, repeatsY
  

def CreateOutputFolder(inputImagePath, repeatsX, repeatsY, method, r1, r2):
    '''
    :param inputImagePath: address of our input image
    :param repeats:        number of repeats
    :return: address of the result folder
    '''
    inputImageName = inputImagePath.split('/')[-1] # extract input image name
    outputFolder = "OutputImages/"        # pictures will be stored

    resultFolderAddress = outputFolder + inputImageName + "-{}.{}-{}.{}-{}.jpg".format(method, r1, r2, repeatsX, repeatsY) # the folder that our code is stored in
    if not os.path.exists(resultFolderAddress): # create the result folder
        os.makedirs(resultFolderAddress)
    return resultFolderAddress

def main():
    inputImagePath, method, repeatsX, repeatsY = GetCommandLineArgs()
    # We have two methods of calculating the result
    # 0: Ln -> Rotate -> Tile   -> exponent
    # 1: Ln -> Tile   -> Rotate -> exponent
    #@markdown inner circle radius 0.2 
    r1_ = "0.2" #@param[0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1]
    r1 = float(r1_)
    #@markdown outer circle radius 0.9     
    r2_ = "0.9" #@param[0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1]   
    r2 = float(r2_)

    resultFolderAddress = CreateOutputFolder(inputImagePath, repeatsX, repeatsY, method, r1, r2)

    d = Droste(inputImagePath, repeatsX, repeatsY, r1, r2, -1, 1, resultFolderAddress)
    print('be patient...')
    d.Transform(method)


if __name__ == "__main__":
    main()

This code will resize the generated image according to input. Let me tell you that this Code does a preety neat job resizing image from small to big or big to small and keeps the image crisp.

#@title Resize Image *Copy paste path here
from PIL import Image
from IPython.display import Image as Image_

Image_to_Correct = "/content/OutputImages/test.jpg-0.0.2-0.9.3-5.jpg/final-0.0.2-0.9.3-5.jpg"#@param {type:"string"}
image = Image.open(Image_to_Correct)
width, height = image.size

#@markdown Size:- SD 480p || HD 720p || HD 1080p || 4K 2160p
add_size_ = "640" #@param [640,1280,1920,3840]
add_size = int(add_size_)

remain_adw = add_size - width
remain_adh = add_size - height

new_image = image.resize((width+remain_adw,height+remain_adh))
if add_size == 640:
  sd = 'newsize_SD_480p.png'
  new_image.save(sd)
  fl = sd 
if add_size == 1280:
  HD = 'newsize_HD_720p.png'
  new_image.save(HD)
  fl = HD
if add_size == 1920:
  HDD = 'newsize_HD_1080p.png'
  new_image.save(HDD)
  fl = HDD
if add_size == 3840:
  _4K = 'newsize_4K_2160p.png'
  new_image.save(_4K)
  fl = _4K

print("File Saved!!!")
Image_(filename = fl, width = 512, height = 512) 

And this Code will download your Enlarged Image from google colab

# @markdown # Download Your File
from google.colab import files
files.download(fl)

:::Dev Info:::

My Name:- Joel D'costa
Git:- DrosteEffect
Google Colab:-


Comments



🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍

Popular Posts

Python underline string, Python underline text, Underline python print

Python pip - Installing modules from IDLE (Pyton GUI) for python 3.7

Top 40 Python - String Processing in Python | Working with String in Python

Python Program - When was I born? / Date of Birth / MY BIRTHDAY (using Python3+)

Top 11 Essential Python Tips and Tricks





Subscribe to our Channel


Follow us on Facebook Page

Join our python facebook groups



Join us on Telegram