├── .gitignore ├── LICENSE ├── Manhattanhenge.png ├── README.md ├── animate_frames.py ├── beach.gif ├── beach.jpg ├── manhattanhenge.gif ├── pydroste.py ├── pydroste_multiple.py ├── requirements.in ├── requirements.txt └── zoom_transparent.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Douwe Osinga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Manhattanhenge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DOsinga/pydroste/d8d25fa07a2ced3889f9690e1d9fd7fa49c3c399/Manhattanhenge.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyDroste 2 | 3 | Small python project to generate movies with a droste effect or to create 4 | zoomable movies using DALLE 5 | 6 | ## Installation 7 | 8 | To setup a virtual environment with python3 and the dependencies installed, 9 | execute in a shell: 10 | 11 | python3 -m venv venv3 12 | source venv3/bin/activate 13 | pip install -r requirements.txt 14 | 15 | You can now run the main script: 16 | 17 | python pydroste.py --input=beach.jpg \ 18 | --output=beach.mp4 \ 19 | --center=1904,1940 \ 20 | --scale=125 21 | 22 | And it should create a zooming in movie: 23 | 24 | ![Beach Movie](beach.gif) 25 | 26 | Beside the input and output parameters, the important flags are 27 | `center` and `scale`. `center` determines the point in the input picture 28 | to zoom in on. `Scale` is the scale of the sub-image compared to the main 29 | image. In other words, this is how far you need to zoom in to have the 30 | main image be replaced by the sub-image completely and which would 31 | complete one loop. 32 | 33 | The two other flags you can pass are `frames`, which determines how 34 | many frames the output image will have and `fps`, which determines 35 | how many frames per seconds the output movie will play with. 36 | 37 | The `pydroste_multiple.py` script can render loops with multiple zoom 38 | points. The basic pattern is the same, except that a parameter `zoom_points` 39 | needs to be supplied that contains a list of `;` separated tuples 40 | specifying the various zoom point as `center_x,center_y,scale` 41 | 42 | ![Manhattanhenge](manhattanhenge.gif) 43 | 44 | ## DALLE use 45 | 46 | This repository also contains some scripts that can be used to create 47 | zoomable movies using DALLE. 48 | 49 | Put your images say in the vangogh directory, create a first frame 50 | and put it in the say vangogh directory. You can then use: 51 | 52 | python zoom_transparent.py \ 53 | --input=vangogh/frame1.png \ 54 | --output=vangogh/zoomed.png 55 | 56 | To create a zoomed out version of frame1. Use that as a basis for your 57 | next image and repeat this until you have a longish list of zoomed 58 | out frames. You can put them together into a movie using: 59 | 60 | 61 | python animate_frames.py \ 62 | --frame_count=6 --frames=60 \ 63 | --input_dir=vangogh 64 | 65 | This will produce a movie with 6 frames and with 60 frames per step 66 | valled vangogh.mp4 in the root directory. -------------------------------------------------------------------------------- /animate_frames.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import math 3 | 4 | import argument 5 | import imageio 6 | import numpy as np 7 | import tqdm 8 | from PIL import Image 9 | 10 | 11 | def zoom_at(org_img, zoom, x, y): 12 | w, h = org_img.size 13 | 14 | xz = x / zoom 15 | yz = y / zoom 16 | box = (int(x - xz), int(y - yz), int(x - xz + w / zoom), int(y - yz + h / zoom)) 17 | 18 | img = org_img.crop(box) 19 | img = img.resize((w, h), Image.LANCZOS) 20 | return img 21 | 22 | 23 | @argument.entrypoint 24 | def main(*, 25 | frame_count: int = 38, 26 | target: str = 'dalle', 27 | frames: int = 40, 28 | fps: float = 30): 29 | log_scale = math.log(1.82) 30 | with imageio.get_writer(target + ".mp4", mode='I', fps=fps) as writer: 31 | for i in range(frame_count, 0, -1): 32 | img = Image.open(target + '/frame' + str(i) + ".png") 33 | w, h = img.size 34 | for frame in tqdm.tqdm(range(frames)): 35 | if i == 1 and frame == frames // 2: 36 | break 37 | zoom = math.exp(log_scale * frame / (frames)) 38 | zoomed = zoom_at(img, zoom, w // 2 + 64, h // 2 + 64) 39 | w, h = zoomed.size 40 | zoomed = zoomed.crop((16, 16, w - 32, h - 32)) 41 | writer.append_data(np.array(zoomed)) 42 | 43 | 44 | if __name__ == '__main__': 45 | main() 46 | -------------------------------------------------------------------------------- /beach.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DOsinga/pydroste/d8d25fa07a2ced3889f9690e1d9fd7fa49c3c399/beach.gif -------------------------------------------------------------------------------- /beach.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DOsinga/pydroste/d8d25fa07a2ced3889f9690e1d9fd7fa49c3c399/beach.jpg -------------------------------------------------------------------------------- /manhattanhenge.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DOsinga/pydroste/d8d25fa07a2ced3889f9690e1d9fd7fa49c3c399/manhattanhenge.gif -------------------------------------------------------------------------------- /pydroste.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import math 3 | 4 | import argument 5 | import imageio 6 | import numpy as np 7 | import tqdm 8 | from PIL import Image 9 | 10 | 11 | def zoom_at(org_img, zoom, scale, x, y): 12 | w, h = org_img.size 13 | 14 | xz = x / zoom 15 | yz = y / zoom 16 | box = (int(x - xz), int(y - yz), int(x - xz + w / zoom), int(y - yz + h / zoom)) 17 | 18 | img = org_img.crop(box) 19 | img = img.resize((w, h), Image.LANCZOS) 20 | w2 = int(w * zoom / scale) 21 | h2 = int(h * zoom / scale) 22 | x2 = int(x - w2 * x / w) 23 | y2 = int(y - h2 * y / h) 24 | img.paste(org_img.resize((w2, h2), Image.LANCZOS), (x2, y2)) 25 | return img 26 | 27 | 28 | @argument.entrypoint 29 | def main(*, 30 | input: str = 'beach.jpg', 31 | output: str = 'beach.mp4', 32 | center: str = '1904,1940', 33 | scale: float = 125, 34 | frames: int = 90, 35 | fps: float = 30): 36 | """Create a Droste type of movie from a still by zooming in and replacing part of it by itself.""" 37 | center_x, center_y = map(int, center.split(',')) 38 | log_scale = math.log(scale) 39 | img = Image.open(input) 40 | with imageio.get_writer(output, mode='I', fps=fps) as writer: 41 | for frame in tqdm.tqdm(range(frames)): 42 | zoom = math.exp(log_scale * frame / frames) 43 | zoomed = zoom_at(img, zoom, scale, center_x, center_y) 44 | if frame == 0: 45 | # frame in frame: 46 | img = zoomed 47 | writer.append_data(np.array(zoomed)) 48 | 49 | 50 | if __name__ == '__main__': 51 | main() 52 | -------------------------------------------------------------------------------- /pydroste_multiple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import math 3 | 4 | import argument 5 | import imageio 6 | import numpy as np 7 | import tqdm 8 | from PIL import Image 9 | 10 | from pydroste import zoom_at 11 | 12 | 13 | @argument.entrypoint 14 | def main(*, 15 | input: str = 'Manhattanhenge.png', 16 | output: str = 'manhattanhenge.mp4', 17 | zoom_points: str = '665,1440,27.8;1410,1395,35;380,1295,65;1193,1460,35;108,1320,41.6;1230,1322,76', 18 | frames: int = 75, 19 | fps: float = 30): 20 | """Create a Droste type of movie from a still by zooming in and replacing part of it by itself.""" 21 | zoom_points = [[*map(float, zoom_point.split(','))] for zoom_point in zoom_points.split(';')] 22 | base_img = Image.open(input) 23 | with imageio.get_writer(output, mode='I', fps=fps) as writer: 24 | for idx, (center_x, center_y, scale) in enumerate(zoom_points): 25 | log_scale = math.log(scale) 26 | next_idx = (idx + 1) % len(zoom_points) 27 | next_center_x, next_center_y, next_scale = zoom_points[next_idx] 28 | img = zoom_at(base_img, 1.0, next_scale, next_center_x, next_center_y) 29 | for frame in tqdm.tqdm(range(frames)): 30 | zoom = math.exp(log_scale * frame / frames) 31 | zoomed = zoom_at(img, zoom, scale, center_x, center_y) 32 | writer.append_data(np.array(zoomed)) 33 | 34 | 35 | if __name__ == '__main__': 36 | main() 37 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | imageio 2 | imageio-ffmpeg 3 | Pillow 4 | tqdm 5 | argument-clinic 6 | pip-tools 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with python 3.9 3 | # To update, run: 4 | # 5 | # pip-compile 6 | # 7 | argument-clinic==0.12 8 | # via -r requirements.in 9 | build==0.8.0 10 | # via pip-tools 11 | click==8.1.3 12 | # via pip-tools 13 | docstring-parser==0.14.1 14 | # via argument-clinic 15 | imageio==2.19.3 16 | # via -r requirements.in 17 | imageio-ffmpeg==0.4.7 18 | # via -r requirements.in 19 | numpy==1.23.0 20 | # via imageio 21 | packaging==21.3 22 | # via build 23 | pep517==0.12.0 24 | # via build 25 | pillow==9.2.0 26 | # via 27 | # -r requirements.in 28 | # imageio 29 | pip-tools==6.8.0 30 | # via -r requirements.in 31 | pyparsing==3.0.9 32 | # via packaging 33 | tomli==2.0.1 34 | # via 35 | # build 36 | # pep517 37 | tqdm==4.64.0 38 | # via -r requirements.in 39 | wheel==0.37.1 40 | # via pip-tools 41 | 42 | # The following packages are considered to be unsafe in a requirements file: 43 | # pip 44 | # setuptools 45 | -------------------------------------------------------------------------------- /zoom_transparent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argument 4 | from PIL import Image 5 | 6 | 7 | @argument.entrypoint 8 | def main(*, 9 | input: str = 'frame_01.png', 10 | output: str = 'frame_02.png', 11 | ): 12 | img = Image.open(input) 13 | w, h = img.size 14 | img = img.crop((16, 16, w - 32, h - 32)) 15 | img_out = Image.new("RGBA", img.size, (255, 255, 255, 0)) 16 | img = img.resize((w // 2, h // 2)) 17 | img_out.paste(img, (w // 4, h // 4)) 18 | img_out.save(output) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | --------------------------------------------------------------------------------