├── .gitignore ├── .pre-commit-config.yaml ├── .vscode └── settings.json ├── Dockerfile ├── FEATURES.md ├── README.md ├── ascii-art-matrix-effect-color.py ├── ascii-art-matrix-effect.py ├── community-version.py ├── dog.gif ├── example ├── makeArtPython3.py ├── make_art.py ├── ztm-logo.png └── ztm-logo_ascii_art.txt ├── fast-api-main.py ├── fastapi_source ├── application │ └── ascii │ │ └── ascii_service.py ├── core │ └── config.py └── host │ └── ascii_art_routes.py ├── geometric-art.py ├── giphy.webp ├── gui.py ├── invert-color-art.py ├── mosaic-art.py ├── pointillism-art.py ├── pyproject.toml ├── requirements.txt ├── texture-art-source ├── README.md ├── p1.jpg ├── p2.jpg ├── p3.jpg ├── p4.jpg └── p5.jpg ├── texture-art.py ├── ztm-icon.ico ├── ztm_ascii.txt └── ztm_ascii_colored.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # DS_Store 7 | **/.DS_Store 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | #poetry.lock 106 | 107 | # pdm 108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 109 | #pdm.lock 110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 111 | # in version control. 112 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 113 | .pdm.toml 114 | .pdm-python 115 | .pdm-build/ 116 | poetry.lock 117 | 118 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 119 | __pypackages__/ 120 | 121 | # Celery stuff 122 | celerybeat-schedule 123 | celerybeat.pid 124 | 125 | # SageMath parsed files 126 | *.sage.py 127 | 128 | # Environments 129 | .env 130 | .venv 131 | env/ 132 | venv/ 133 | ENV/ 134 | env.bak/ 135 | venv.bak/ 136 | 137 | # Spyder project settings 138 | .spyderproject 139 | .spyproject 140 | 141 | # Rope project settings 142 | .ropeproject 143 | 144 | # mkdocs documentation 145 | /site 146 | 147 | # mypy 148 | .mypy_cache/ 149 | .dmypy.json 150 | dmypy.json 151 | 152 | # Pyre type checker 153 | .pyre/ 154 | 155 | # pytype static type analyzer 156 | .pytype/ 157 | 158 | # Cython debug symbols 159 | cython_debug/ 160 | 161 | # PyCharm 162 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 163 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 164 | # and can be added to the global gitignore or merged into this file. For a more nuclear 165 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 166 | #.idea/ 167 | 168 | .ruff_cache/ 169 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: check-added-large-files 7 | - id: check-ast 8 | - id: check-case-conflict 9 | - id: check-json 10 | - id: check-toml 11 | - id: check-yaml 12 | - id: end-of-file-fixer 13 | # exclude: migrations/ 14 | - repo: https://github.com/astral-sh/ruff-pre-commit 15 | rev: v0.6.9 16 | hooks: 17 | - id: ruff 18 | args: 19 | - --fix 20 | - id: ruff-format 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll": "explicit", 6 | "source.organizeImports": "explicit" 7 | }, 8 | "editor.defaultFormatter": "charliermarsh.ruff" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | WORKDIR /app 4 | 5 | COPY . /app 6 | 7 | RUN pip install --no-cache-dir -r requirements.txt 8 | 9 | CMD ["bash"] 10 | # CMD ["python", "community-version.py", "example/ztm-logo.png"] 11 | 12 | 13 | # docker build -t ascii-art . 14 | 15 | # Linux 16 | # docker run -it --rm -v $(pwd):/app ascii-art 17 | # Windows 18 | # docker run -it --rm -v path:/app ascii-art 19 | -------------------------------------------------------------------------------- /FEATURES.md: -------------------------------------------------------------------------------- 1 | # ASCII Art Generator 2 | 3 | ### Overview 4 | This ASCII Art Generator uses customizable patterns and filters to convert images into ASCII art. The script supports options for resizing, brightness/contrast adjustments, and even color themes to enhance your ASCII art. 5 | 6 | --- 7 | 8 | ### Installation Requirements 9 | 10 | #### Prerequisites 11 | - Python 3.x installed on your system 12 | - Install necessary packages using the following command: 13 | ```bash 14 | pip install typer pillow numpy rich 15 | ``` 16 | 17 | --- 18 | 19 | ### Command Line Options 20 | 21 | | Option | Description | Example | 22 | |------------------|----------------------------------------------------------------|-------------------------------------------------------------------------------------------| 23 | | `--width` | Sets the width of the ASCII art. Default is 100. | `--width 150` | 24 | | `--pattern` | Selects ASCII pattern type. Options: `basic`, `complex`, `emoji`, `numeric` | `--pattern emoji` | 25 | | `--theme` | Color theme for colorized ASCII. Options: `neon`, `pastel`, `grayscale` | `--theme neon` | 26 | | `--brightness` | Adjusts image brightness. Default is 1.0 (no change). | `--brightness 1.2` | 27 | | `--contrast` | Adjusts image contrast. Default is 1.0 (no change). | `--contrast 1.3` | 28 | | `--blur` | Applies blur effect to the image before ASCII conversion. | `--blur` | 29 | | `--sharpen` | Sharpens the image before ASCII conversion. | `--sharpen` | 30 | | `--contours` | Adds contour effect to enhance edges in ASCII output. | `--contours` | 31 | | `--invert` | Inverts image colors before conversion. | `--invert` | 32 | | `--output` | Saves ASCII art to specified file. | `--output output.txt` | 33 | 34 | 1. Basic Usage: 35 | 36 | ```plaintext 37 | python community-version.py 38 | ``` 39 | 40 | Example: `python community-version.py example/ztm-logo.png` 41 | 42 | 43 | 2. Width Option: 44 | 45 | ```plaintext 46 | python community-version.py --width or -w 47 | ``` 48 | 49 | Example: `python community-version.py example/ztm-logo.png --width 150` 50 | 51 | 52 | 3. Output File Option: 53 | 54 | ```plaintext 55 | python community-version.py --output or -o 56 | ``` 57 | 58 | Example: `python community-version.py example/ztm-logo.png --output ztm_ascii.txt` 59 | 60 | 61 | 4. ASCII Pattern Option: 62 | 63 | ```plaintext 64 | python community-version.py --pattern or -p 65 | ``` 66 | 67 | Available patterns: 'basic', 'complex', 'emoji', 'numeric' 68 | Example: `python community-version.py example/ztm-logo.png --pattern complex` 69 | 70 | 71 | 5. Brightness Adjustment: 72 | 73 | ```plaintext 74 | python community-version.py --brightness or -b 75 | ``` 76 | 77 | Example: `python community-version.py example/ztm-logo.png --brightness 1.2` 78 | 79 | 80 | 6. Contrast Adjustment: 81 | 82 | ```plaintext 83 | python community-version.py --contrast or -c 84 | ``` 85 | 86 | Example: `python community-version.py example/ztm-logo.png --contrast 1.1` 87 | 88 | 89 | 7. Blur Effect: 90 | 91 | ```plaintext 92 | python community-version.py --blur 93 | ``` 94 | 95 | Example: `python community-version.py example/ztm-logo.png --blur` 96 | 97 | 98 | 8. Sharpen Effect: 99 | 100 | ```plaintext 101 | python community-version.py --sharpen 102 | ``` 103 | 104 | Example: `python community-version.py example/ztm-logo.png --sharpen` 105 | 106 | 107 | 9. Contour Effect: 108 | 109 | ```plaintext 110 | python community-version.py --contours 111 | ``` 112 | 113 | Example: `python community-version.py example/ztm-logo.png --contours` 114 | 115 | 116 | 10. Help Command: 117 | 118 | ```plaintext 119 | python community-version.py --help 120 | ``` 121 | 122 | This displays all available options with their descriptions. 123 | 124 | 125 | 126 | 127 | You can combine multiple options in a single command. For example: 128 | 129 | ```plaintext 130 | python community-version.py example/ztm-logo.png --width 120 --pattern complex --brightness 1.1 --contrast 1.2 --colorize --theme pastel --output ztm_ascii_colored.txt 131 | ``` 132 | 133 | This command will generate a colorized ASCII art of the ZTM logo with a width of 120 characters, using the complex pattern, adjusted brightness and contrast, and the pastel color theme, saving the output to a file named 'ztm_ascii_colored.txt'. 134 | 135 | Remember to replace `example/ztm-logo.png` with the path to the image you want to convert to ASCII art. 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # [![ASCII ART](https://img.shields.io/badge/PYTHON%20PROJECT-ASCII%20ART-blue?style=for-the-badge&logo=Python)](https://github.com/zero-to-mastery/ascii-art) 4 | 5 | Welcome to this years Python challenge for Hacktoberfest, a project with beginners and aspiring developers in mind, utilizing Python to convert images into ASCII Art. 6 | 7 | ## ❇️ Getting Started with Hacktoberfest 8 | 9 | Hacktoberfest is a month-long celebration of open source, organised by Digital Ocean. ([More details here](https://github.com/zero-to-mastery/Hacktoberfest-2024/blob/master/README.md)) 10 | 11 | If you've never made a pull request before, or participated in an open-source project, we recommend taking a look at: 12 | 13 | - Our [Start Here Guidelines](https://github.com/zero-to-mastery/start-here-guidelines) 14 | - Our [Youtube Video](https://www.youtube.com/watch?v=uQLNFRviB6Q). 15 | 16 | These two resources have everything you need to learn about open-source, with a step-by-step guide to making your very first PR. Once you've got your feet wet, you're ready to come back and dive into Hacktoberfest fun! 17 | 18 | --- 19 | 20 | 21 | 22 | --- 23 | 24 | ## ❇️ But what is ASCII Art? 25 | 26 | > ASCII art is a graphic design technique that uses computers for presentation and consists of pictures pieced together from the 95 printable characters defined by the ASCII Standard from 1963 and ASCII compliant character sets with proprietary extended characters. 27 | > ~ [Wikipedia](https://en.wikipedia.org/wiki/ASCII_art) 28 | 29 | ## ❇️ How to get started: 30 | 31 | In order to get started on this project, it is recommended that you watch the section on **Scripting** in the [Python course](https://zerotomastery.io/courses/learn-python/?utm_source=github&utm_campaign=ascii-art-hf24). We talk about `sys.argv` and `Pillow` library (Image processing) in that section which would help you! 32 | 33 | ### Step 1: Setup the project environment 34 | 35 | 1. First up you need to fork (make a copy) of this repo to your Github account. 36 | 37 | 2. Clone (download) your fork to your computer. 38 | 39 | ```bash 40 | git clone https://github.com/{your-username}/ascii-art.git 41 | ``` 42 | 43 | 3. Navigate to your project directory. 44 | 45 | ```bash 46 | cd ascii-art 47 | ``` 48 | 49 | 4. Set your streams so you can sync your clone with the original repo (get the latest updates) 50 | 51 | - `git remote add upstream https://github.com/zero-to-mastery/ascii-art.git` 52 | - `git pull upstream master` 53 | - The above 2 commands will synchronize your forked version of the project with the actual repository. 54 | 55 | 5. Create a virtual environment. 56 | 57 | ```bash 58 | python3 -m venv venv 59 | ``` 60 | 61 | 6. Activate the virtual environment: 62 | 63 | - On Windows: 64 | 65 | ```bash 66 | venv\Scripts\activate 67 | ``` 68 | 69 | - On Mac/Linux: 70 | 71 | ```bash 72 | source venv/bin/activate 73 | ``` 74 | 75 | 7. Install the required packages using the `requirements.txt` file. 76 | 77 | ```bash 78 | pip install -r requirements.txt 79 | ``` 80 | 81 | ### Step 2: Running the example code 82 | 83 | 1. Run the example code with the command. 84 | 85 | ```bash 86 | python3 community-version.py example/ztm-logo.png 87 | ``` 88 | 89 | 2. Stare with amazement 😮 90 | 91 | ### Step 3: Contribute and Collaborate 92 | 93 | Start chatting with other ZTM students in the #hacktoberfest-2024 channel on our Discord to get help, work together, and share your contributions! 94 | 95 | # **IMPORTANT: DO NOT MODIFY THE make_art.py FILE. ONLY THE community_version.py FILE SHOULD BE MODIFIED.** 96 | 97 | 4. Make sure you have Python 3 installed on your machine 98 | 5. Run the command cd example 99 | 6. Run the example code with the command: `python3 community-version.py example/ztm-logo.png` 100 | 7. Stare with amazement 😮 101 | 8. Start chatting with other ZTM students in the #hacktoberfest-2024 channel on our Discord to get help, work together, and share your contributions! 102 | 9. **IMPORTANT: DO NOT MODIFY THE make_art.py FILE. ONLY THE community_version.py FILE SHOULD BE MODIFIED.** 103 | 104 | ## ❇️ How to contribute? 105 | 106 | Now that you see how this command line tool works, let's see how we can evolve it with our ZTM community help!! Maybe we want to display this on an HTML web page where users can submit images and we convert it to ASCII art? Maybe we want to improve how the Command Line Tool works/make it more customizeable? Or maybe modify the script to do many other types of art beyond ASCII. 107 | 108 | The options are endless and there is no wrong answer. This is all for fun, so try to customize the tool whichever way you think would be best and let's see what we get at the end of the month! Enjoy! 109 | 110 | > ⚠ Please do **not** make changes to the files in the example directory, These files should remain intact for future contributors to examine and compare with the community version! Pull requests with changes to these files will be closed. 111 | 112 | 1. Examine the code in `community-version.py`, figure out what improvements your fellow community members have made (check out `FEATURES.md` for a list of existing features you can add to or improve). 113 | 2. Make an improvement, it doesnt have to be elaborate 114 | 3. Create a pull request 115 | 4. [Tweet about making your first Hacktoberfest pull request](https://ctt.ac/36L1C) 116 | 117 | > Congratulations! You are now one pull request closer. Repeat these steps until you have made at least 4 qualifying pull requests. You can check how many qualifying pull requests you have made at Have Fun and Happy Coding! 118 | 119 | ### Bonus Task 120 | 121 | Looking for a challenge? 122 | We have left the original code which was written in Python 2 under the `example/make_art_python2.py` file. See what happens when you run it with Python 3. See all of the errors? Can you fix it so it works with python 3? The answer is with the `example/make_art.py` file which is written in Python 3. 123 | 124 | **All discussions around this event can now be had in our dedicated Hacktoberfest channel on Discord!** 125 | 126 | ## My Contribution 127 | 128 | ### Simple Python Script 129 | 130 | ```python 131 | def greet(name): 132 | return f"Hello, {name}!" 133 | 134 | print(greet("Hacktoberfest")) 135 | ### New ASCII Art Example 136 | 137 | _______ 138 | 139 | / 140 | / O O 141 | | ^ | | ‘-’ | _________/ 142 | ``` 143 | 144 | ## Code Quality and Linting 145 | 146 | This project uses [Ruff](https://github.com/charliermarsh/ruff), a fast Python linter, to ensure code quality and consistency. Ruff checks for common Python code issues, ensures consistent code style, and enforces best practices. It is integrated as part of the development workflow to maintain a clean codebase. 147 | 148 | ### Installation 149 | 150 | To install `pre-commit` and `Ruff`, simply install the dependencies listed in `requirements.txt`: 151 | 152 | ```bash 153 | pip install -r requirements.txt 154 | ``` 155 | 156 | Then, install the pre-commit hooks: 157 | 158 | ```bash 159 | pre-commit install 160 | ``` 161 | 162 | This will set up `Ruff` to automatically check your code before each commit. 163 | 164 | ### Running Ruff 165 | 166 | Ruff will automatically run when you make a commit, but you can also manually check your code with the following command: 167 | 168 | ```bash 169 | ruff check . 170 | ``` 171 | 172 | To automatically fix issues detected by Ruff, you can use: 173 | 174 | ```bash 175 | ruff check . --fix 176 | ``` 177 | 178 | ### Configuration 179 | 180 | Ruff is configured through the `pyproject.toml` file. You can modify the linter rules by adjusting the following section: 181 | 182 | ```toml 183 | [tool.ruff] 184 | line-length = 100 # Adjust as necessary 185 | select = ["E", "F", "I", "UP"] # Add or remove specific rules here 186 | ``` 187 | 188 | ### Pre-commit Hook 189 | 190 | We have configured Ruff as a pre-commit hook, so it will run automatically before every commit. If you'd like to manually run it on all files, you can use: 191 | 192 | ```bash 193 | pre-commit run --all-files 194 | ``` 195 | 196 | ### Ignoring Rules 197 | 198 | If you want to ignore specific files or directories, or disable certain rules, you can do so by editing the `.ruffignore` file or configuring rule exceptions in `pyproject.toml`. 199 | -------------------------------------------------------------------------------- /ascii-art-matrix-effect-color.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | import cv2 4 | import numpy as np 5 | from PIL import Image 6 | 7 | ASCII_CHARS: List[str] = [".", ":", ">", "&", "%", "#", "N", "M", "W", "R", "B"] 8 | 9 | 10 | def scale_image(image: Image.Image, new_width: int = 100) -> Image.Image: 11 | (original_width, original_height) = image.size 12 | aspect_ratio: float = original_height / float(original_width) 13 | new_height: int = int(aspect_ratio * new_width) 14 | new_image: Image.Image = image.resize((new_width, new_height)) 15 | return new_image 16 | 17 | 18 | def convert_to_grayscale(image: Image.Image) -> Image.Image: 19 | return image.convert("L") 20 | 21 | 22 | def map_pixels_to_ascii_chars(image: Image.Image, range_width: int = 25) -> str: 23 | pixels_in_image: List[int] = list(image.getdata()) 24 | pixels_to_chars: List[int] = [ASCII_CHARS[int(pixel_value / range_width)] for pixel_value in pixels_in_image] 25 | return "".join(pixels_to_chars) 26 | 27 | 28 | def convert_image_to_ascii(image: Image.Image, new_width: int = 100) -> Tuple[str, List[Tuple[int, int, int, int]]]: 29 | image = scale_image(image, new_width) 30 | grayscale_image = convert_to_grayscale(image) 31 | pixels_to_chars: str = map_pixels_to_ascii_chars(grayscale_image) 32 | len_pixels_to_chars: int = len(pixels_to_chars) 33 | image_ascii: List[str] = [pixels_to_chars[index : index + new_width] for index in range(0, len_pixels_to_chars, new_width)] 34 | color_data: List[Tuple[int, int, int, int]] = list(image.convert("RGBA").getdata()) 35 | return "\n".join(image_ascii), color_data 36 | 37 | 38 | def get_char_for_position(x, y, lines, column_states, column_lengths): 39 | if x >= len(lines[y]): 40 | return " " # Pad shorter lines with spaces 41 | if column_states[x] == -1 or column_states[x] < y: 42 | return lines[y][x] 43 | if column_states[x] - column_lengths[x] <= y: 44 | if column_states[x] == y: 45 | return " " 46 | else: 47 | return chr(np.random.choice(list(range(33, 127)))) # Random ASCII characters excluding space 48 | return lines[y][x] 49 | 50 | 51 | def generate_new_frame(lines, column_states, column_lengths, height, width): 52 | new_frame = [] 53 | for y in range(height): 54 | new_line = "".join(get_char_for_position(x, y, lines, column_states, column_lengths) for x in range(width)) 55 | new_frame.append(new_line) 56 | return new_frame 57 | 58 | 59 | def update_column_states(column_states, column_lengths, columns_covered, height, width): 60 | # Move the flow down faster 61 | column_states = [state + 2 if state != -1 else state for state in column_states] 62 | # Reset the flow if it reaches the bottom and mark columns as covered 63 | for i in range(width): 64 | if column_states[i] >= height + column_lengths[i]: 65 | column_states[i] = -1 66 | columns_covered[i] = True # Mark this column as covered 67 | return column_states, columns_covered 68 | 69 | 70 | def start_new_flows(column_states, column_lengths, columns_covered, height, width, t, skip_frames): 71 | if t >= skip_frames: 72 | active_flows = sum(1 for state in column_states if state != -1) 73 | if active_flows < width: 74 | # Ensure all columns are eventually covered 75 | uncovered_columns = [i for i, covered in enumerate(columns_covered) if not covered] 76 | if uncovered_columns: 77 | i = np.random.choice(uncovered_columns) 78 | else: 79 | i = np.random.randint(0, width) 80 | if column_states[i] == -1: 81 | column_states[i] = 0 82 | column_lengths[i] = np.random.randint(int(0.2 * height), int(1.2 * height)) # Random length between 20-120% of height 83 | return column_states, column_lengths 84 | 85 | 86 | def generate_matrix_effect(image_ascii: str) -> List[str]: 87 | lines = image_ascii.split("\n") 88 | height = len(lines) 89 | width = max(len(line) for line in lines) # Ensure all lines have the same width 90 | frames = [] 91 | 92 | column_states = [-1] * width # -1 means no flow 93 | column_lengths = [0] * width 94 | columns_covered = [False] * width # Track which columns have been covered by the flow 95 | 96 | # Skip the first second (assuming 30 FPS, skip the first 30 frames) 97 | skip_frames = 30 98 | t = 0 99 | 100 | while not all(columns_covered): 101 | new_frame = generate_new_frame(lines, column_states, column_lengths, height, width) 102 | frames.append("\n".join(new_frame)) 103 | column_states, columns_covered = update_column_states(column_states, column_lengths, columns_covered, height, width) 104 | column_states, column_lengths = start_new_flows(column_states, column_lengths, columns_covered, height, width, t, skip_frames) 105 | t += 1 106 | 107 | # Add 1 second of additional frames after all columns are covered 108 | additional_frames = 30 109 | frames.extend([frames[-1]] * additional_frames) 110 | 111 | return frames 112 | 113 | 114 | def create_video_from_frames(frames: List[str], color_data: List[Tuple[int, int, int, int]], width: int, output_path: str): 115 | height = len(frames[0].split("\n")) 116 | fourcc = cv2.VideoWriter_fourcc(*"mp4v") 117 | frame_rate = 30 118 | video = cv2.VideoWriter(output_path, fourcc, frame_rate, (width * 10, height * 10)) 119 | 120 | # Track which characters have been part of the flow 121 | flow_passed = [[False] * width for _ in range(height)] 122 | 123 | for frame in frames: 124 | img = np.zeros((height * 10, width * 10, 3), dtype=np.uint8) 125 | for y, line in enumerate(frame.split("\n")): 126 | for x, char in enumerate(line): 127 | color = color_data[y * width + x] if y * width + x < len(color_data) else (0, 255, 0, 255) 128 | if color[3] == 0: # Handle transparency 129 | color = (50, 50, 50, 255) # Convert transparent pixels to dark gray 130 | 131 | if char in ASCII_CHARS: 132 | color = (color[2], color[1], color[0]) # Convert RGBA to BGR 133 | elif char == " ": # Head of the flow 134 | char = chr(np.random.choice(list(range(33, 127)))) # Random ASCII characters excluding space 135 | color = (200, 255, 200) # Lighter color for the first character of the flow 136 | else: 137 | color = (0, 255, 0) # Green color for the flow 138 | flow_passed[y][x] = True # Mark this character as part of the flow 139 | 140 | # If the flow has passed, turn the character green 141 | if flow_passed[y][x]: 142 | color = (0, 255, 0) 143 | 144 | cv2.putText(img, char, (x * 10, y * 10 + 10), cv2.FONT_HERSHEY_PLAIN, 1, color, 1) 145 | video.write(img) 146 | 147 | video.release() 148 | 149 | 150 | if __name__ == "__main__": 151 | import sys 152 | 153 | image_file_path: str = sys.argv[1] 154 | new_width = 100 155 | 156 | image = Image.open(image_file_path) 157 | image_ascii, color_data = convert_image_to_ascii(image, new_width) 158 | frames = generate_matrix_effect(image_ascii) 159 | create_video_from_frames(frames, color_data, new_width, "ascii-art-matrix-effect-color.mp4") 160 | """ 161 | Feature: 162 | Generate a MP4 video with matrix effect from ascii-art of an image file. 163 | Gradually turning the coloured characters green as the "flow" passes through them. 164 | 165 | Usage: 166 | python3 ascii-art-matrix-effect-color.py 167 | 168 | Args: 169 | image_file_path: str - Path to the image file. 170 | 171 | Example: 172 | python3 ascii-art-matrix-effect-color.py example/ztm-logo.png 173 | 174 | Output file: 175 | ascii-art-matrix-effect-color.mp4 176 | """ 177 | -------------------------------------------------------------------------------- /ascii-art-matrix-effect.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import List 3 | 4 | import cv2 5 | import numpy as np 6 | from PIL import Image 7 | 8 | ASCII_CHARS: List[str] = ["#", "?", "%", ".", "S", "+", ".", "*", ":", ",", "@"] 9 | 10 | 11 | def scale_image(image: Image.Image, new_width: int = 100) -> Image.Image: 12 | (original_width, original_height) = image.size 13 | aspect_ratio: float = original_height / float(original_width) 14 | new_height: int = int(aspect_ratio * new_width) 15 | return image.resize((new_width, new_height)) 16 | 17 | 18 | def convert_to_grayscale(image: Image.Image) -> Image.Image: 19 | return image.convert("L") 20 | 21 | 22 | def map_pixels_to_ascii_chars(image: Image.Image, range_width: int = 25) -> str: 23 | pixels_in_image: List[int] = list(image.getdata()) 24 | pixels_to_chars: List[int] = [ASCII_CHARS[int(pixel_value / range_width)] for pixel_value in pixels_in_image] 25 | return "".join(pixels_to_chars) 26 | 27 | 28 | def convert_image_to_ascii(image: Image.Image, new_width: int = 100) -> str: 29 | image = scale_image(image, new_width) 30 | image = convert_to_grayscale(image) 31 | pixels_to_chars: str = map_pixels_to_ascii_chars(image) 32 | len_pixels_to_chars: int = len(pixels_to_chars) 33 | return "\n".join([pixels_to_chars[index : index + new_width] for index in range(0, len_pixels_to_chars, new_width)]) 34 | 35 | 36 | def generate_new_frame(image_ascii: str, column_states: List[int], height: int, width: int) -> str: 37 | """Generates a new frame based on the current column states.""" 38 | new_frame = [] 39 | lines = image_ascii.split("\n") 40 | 41 | for y in range(height): 42 | new_line = "" 43 | for x in range(width): 44 | if x >= len(lines[y]): 45 | new_line += " " # Pad shorter lines with spaces 46 | elif column_states[x] == -1: 47 | new_line += lines[y][x] 48 | elif column_states[x] < y: 49 | new_line += lines[y][x] 50 | else: 51 | new_line += chr(np.random.choice(list(range(33, 127)))) # Random ASCII character 52 | new_frame.append(new_line) 53 | 54 | return "\n".join(new_frame) 55 | 56 | 57 | def update_column_states(column_states: List[int], height: int, column_lengths: List[int]): 58 | """Updates the column states and resets them if they reach the bottom.""" 59 | for i in range(len(column_states)): 60 | if column_states[i] >= height + column_lengths[i]: 61 | column_states[i] = -1 62 | return column_states 63 | 64 | 65 | def generate_matrix_effect(image_ascii: str, frame_count: int) -> List[str]: 66 | """Generates frames for the matrix effect.""" 67 | lines = image_ascii.split("\n") 68 | height = len(lines) 69 | width = max(len(line) for line in lines) 70 | frames = [] 71 | flow_count = width 72 | 73 | column_states = [-1] * width # -1 means no flow 74 | column_lengths = [0] * width 75 | 76 | # Skip the first second (assuming 30 FPS, skip the first 30 frames) 77 | skip_frames = 30 78 | 79 | for t in range(frame_count): 80 | new_frame = generate_new_frame(image_ascii, column_states, height, width) 81 | frames.append(new_frame) 82 | 83 | column_states = [state + 2 if state != -1 else state for state in column_states] # Move the flow down faster 84 | column_states = update_column_states(column_states, height, column_lengths) 85 | 86 | # Randomly select columns to start new flows if the number of active flows is less than flow_count 87 | if t >= skip_frames and sum(state != -1 for state in column_states) < flow_count and np.random.rand() < 0.8: 88 | i = np.random.randint(0, width) 89 | if column_states[i] == -1: 90 | column_states[i] = 0 91 | column_lengths[i] = np.random.randint(int(0.2 * height), int(1.2 * height)) # Random length 92 | 93 | return frames 94 | 95 | 96 | def create_video_from_frames(frames: List[str], output_path: str): 97 | """Creates a video from ASCII frames.""" 98 | height = len(frames[0].split("\n")) 99 | width = len(frames[0].split("\n")[0]) 100 | fourcc = cv2.VideoWriter_fourcc(*"mp4v") 101 | frame_rate = 30 102 | video = cv2.VideoWriter(output_path, fourcc, frame_rate, (width * 10, height * 10)) 103 | 104 | for frame in frames: 105 | img = np.zeros((height * 10, width * 10, 3), dtype=np.uint8) 106 | for y, line in enumerate(frame.split("\n")): 107 | for x, char in enumerate(line): 108 | color = (0, 255, 0) if char != " " else (200, 255, 200) # Color for the character 109 | cv2.putText(img, char, (x * 10, y * 10 + 10), cv2.FONT_HERSHEY_PLAIN, 1, color, 1) 110 | 111 | video.write(img) 112 | 113 | video.release() 114 | 115 | 116 | if __name__ == "__main__": 117 | image_file_path: str = sys.argv[1] 118 | frame_count: int = int(sys.argv[2]) if len(sys.argv) > 2 else 500 119 | new_width = 100 120 | 121 | image = Image.open(image_file_path) 122 | image_ascii = convert_image_to_ascii(image, new_width) 123 | frames = generate_matrix_effect(image_ascii, frame_count) 124 | create_video_from_frames(frames, "ascii-art-matrix-effect.mp4") 125 | """ 126 | Feature: 127 | Generate a MP4 video with matrix effect from ascii-art of an image file. 128 | 129 | Args: 130 | image_file_path: str - Path to the image file. 131 | frame_count: int - Number of frames to generate (optional, default: 500). 132 | 133 | Usage: 134 | python3 ascii-art-matrix-effect.py 135 | 136 | Example: 137 | python3 ascii-art-matrix-effect.py example/ztm-logo.png 138 | python3 ascii-art-matrix-effect.py example/ztm-logo.png 1000 139 | 140 | Output file: 141 | ascii-art-matrix-effect.mp4 142 | """ 143 | -------------------------------------------------------------------------------- /community-version.py: -------------------------------------------------------------------------------- 1 | import typer 2 | from PIL import Image, ImageEnhance, ImageFilter, ImageOps, ImageDraw, ImageFont 3 | import streamlit as st 4 | from streamlit_webrtc import webrtc_streamer, WebRtcMode 5 | import numpy as np 6 | from rich.console import Console 7 | from rich.panel import Panel 8 | from rich.progress import Progress 9 | import sys 10 | import random 11 | 12 | app = typer.Typer() 13 | console = Console() 14 | 15 | ASCII_PATTERNS = { 16 | 'basic': ['@', '#', 'S', '%', '?', '*', '+', ';', ':', ',', '.'], 17 | 'complex': ['▓', '▒', '░', '█', '▄', '▀', '▌', '▐', '▆', '▇', '▅', '▃', '▂'], 18 | 'emoji': ['😁', '😎', '🤔', '😱', '🤩', '😏', '😴', '😬', '😵', '😃'], 19 | 'numeric': ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], 20 | } 21 | 22 | COLOR_THEMES = { 23 | 'neon': [(57, 255, 20), (255, 20, 147), (0, 255, 255)], 24 | 'pastel': [(255, 179, 186), (255, 223, 186), (186, 255, 201), (186, 225, 255)], 25 | 'grayscale': [(i, i, i) for i in range(0, 255, 25)], 26 | } 27 | 28 | 29 | def resize_image(image, new_width=100): 30 | width, height = image.size 31 | aspect_ratio = height / width 32 | new_height = int(aspect_ratio * new_width * 0.55) 33 | return image.resize((new_width, new_height)) 34 | 35 | 36 | def apply_image_filters(image, brightness, contrast, blur, sharpen): 37 | if brightness != 1.0: 38 | image = ImageEnhance.Brightness(image).enhance(brightness) 39 | if contrast != 1.0: 40 | image = ImageEnhance.Contrast(image).enhance(contrast) 41 | if blur: 42 | image = image.filter(ImageFilter.BLUR) 43 | if sharpen: 44 | image = image.filter(ImageFilter.SHARPEN) 45 | return image 46 | 47 | def text_to_image(text, canvas_width, canvas_height): 48 | image = Image.new('RGB', (canvas_width, canvas_height), color='white') 49 | draw = ImageDraw.Draw(image) 50 | font = ImageFont.load_default() 51 | text_width, text_height = draw.textsize(text, font=font) 52 | position = ((canvas_width - text_width) / 2, 53 | (canvas_height - text_height) / 2) 54 | draw.text(position, text, fill='black', font=font) 55 | return image 56 | 57 | # Function to flip image 58 | def flip_image(image: Image.Image, flip_horizontal: bool, flip_vertical: bool) -> Image.Image: 59 | if flip_horizontal: 60 | image = ImageOps.mirror(image) # Flip horizontally 61 | if flip_vertical: 62 | image = ImageOps.flip(image) # Flip vertically 63 | return image 64 | 65 | def create_ascii_art(image, pattern, colorize=False, theme='grayscale'): 66 | ascii_chars = ASCII_PATTERNS[pattern] 67 | ascii_art = [] 68 | pixels = np.array(image) 69 | 70 | for y in range(image.height): 71 | line = [] 72 | for x in range(image.width): 73 | pixel = pixels[y, x] 74 | char_index = int(np.mean(pixel) / 255 * (len(ascii_chars) - 1)) 75 | char = ascii_chars[char_index] 76 | if colorize: 77 | color = random.choice(COLOR_THEMES[theme]) 78 | line.append(f"[color rgb({color[0]},{color[1]},{color[2]})]" + char + "[/color]") 79 | else: 80 | line.append(char) 81 | ascii_art.append("".join(line)) 82 | 83 | return "\n".join(ascii_art) 84 | 85 | def map_pixels_to_ascii(image: Image.Image, pattern: list) -> str: 86 | grayscale_image = image.convert('L') 87 | pixels = np.array(grayscale_image) 88 | ascii_chars = np.vectorize(lambda pixel: pattern[min(pixel // (256 // len(pattern)), len(pattern) - 1)])(pixels) 89 | ascii_image = "\n".join(["".join(row) for row in ascii_chars]) 90 | return ascii_image 91 | 92 | # Function to create colorized ASCII art in HTML format 93 | def create_colorized_ascii_html(image: Image.Image, pattern: list, theme: str) -> str: 94 | image = resize_image(image, 80, 'basic') 95 | pixels = np.array(image) 96 | 97 | ascii_image_html = """ 98 |
99 | """ 100 | 101 | color_palette = COLOR_THEMES.get(theme, COLOR_THEMES['grayscale']) 102 | 103 | for row in pixels: 104 | for pixel in row: 105 | ascii_char = pattern[int(np.mean(pixel) / 255 * (len(pattern) - 1))] 106 | color = random.choice(color_palette) 107 | ascii_image_html += f"{ascii_char}" 108 | ascii_image_html += "
" 109 | 110 | ascii_image_html += "
" 111 | return ascii_image_html 112 | 113 | def create_contours(image): 114 | return image.filter(ImageFilter.FIND_EDGES) 115 | 116 | # Streamlit app for the ASCII art generator 117 | def run_streamlit_app(): 118 | if not st.runtime.exists(): 119 | console.print(f"[yellow]Make sure you run this streamlit app from the streamlit command not python") 120 | return False 121 | 122 | st.title("🌟 Customizable ASCII Art Generator") 123 | page = st.sidebar.selectbox('ASCII Art', ['Image', 'Live']) 124 | if page == "Image": 125 | # Sidebar for options and settings 126 | st.sidebar.title("Settings") 127 | pattern_type = st.sidebar.selectbox("Choose ASCII Pattern", options=[ 128 | 'basic', 'complex', 'emoji']) 129 | colorize = st.sidebar.checkbox("Enable Colorized ASCII Art") 130 | color_theme = st.sidebar.selectbox( 131 | "Choose Color Theme", options=list(COLOR_THEMES.keys())) 132 | width = st.sidebar.slider("Set ASCII Art Width", 50, 150, 100) 133 | # New Flip Image Feature 134 | flip_horizontal = st.sidebar.checkbox("Flip Image Horizontally") 135 | flip_vertical = st.sidebar.checkbox("Flip Image Vertically") 136 | # Image filters 137 | brightness = st.sidebar.slider("Brightness", 0.5, 2.0, 1.0) 138 | contrast = st.sidebar.slider("Contrast", 0.5, 2.0, 1.0) 139 | apply_blur = st.sidebar.checkbox("Apply Blur") 140 | apply_sharpen = st.sidebar.checkbox("Apply Sharpen") 141 | # New Contour Feature 142 | apply_contours = st.sidebar.checkbox("Apply Contours") 143 | # Upload image 144 | uploaded_file = st.file_uploader( 145 | "Upload an image (JPEG/PNG)", type=["jpg", "jpeg", "png"]) 146 | if uploaded_file: 147 | image = Image.open(uploaded_file) 148 | # Apply filters to the image 149 | image = apply_image_filters( 150 | image, brightness, contrast, apply_blur, apply_sharpen) 151 | # Apply contour effect if selected 152 | if apply_contours: 153 | image = create_contours(image) 154 | # Flip the image if requested 155 | image = flip_image(image, flip_horizontal, flip_vertical) 156 | # Display the original processed image 157 | st.image(image, caption="Processed Image", use_column_width=True) 158 | # Resize the image based on the pattern type's aspect ratio 159 | image_resized = resize_image(image, width, pattern_type) 160 | # Generate ASCII art 161 | ascii_pattern = ASCII_PATTERNS[pattern_type] 162 | if colorize: 163 | st.subheader("Colorized ASCII Art Preview:") 164 | ascii_html = create_colorized_ascii_html( 165 | image_resized, ascii_pattern, color_theme) 166 | st.markdown(ascii_html, unsafe_allow_html=True) 167 | else: 168 | st.subheader("Grayscale ASCII Art Preview:") 169 | ascii_art = map_pixels_to_ascii(image_resized, ascii_pattern) 170 | st.text(ascii_art) 171 | # Download options 172 | if colorize: 173 | st.download_button("Download ASCII Art as HTML", ascii_html, 174 | file_name="ascii_art.html", mime="text/html") 175 | else: 176 | st.download_button("Download ASCII Art as Text", ascii_art, 177 | file_name="ascii_art.txt", mime="text/plain") 178 | # Instructions for the user 179 | st.markdown(""" 180 | - 🎨 Use the **Settings** panel to customize your ASCII art with patterns, colors, and image filters. 181 | - 📤 Upload an image in JPEG or PNG format to start generating your ASCII art. 182 | - 💾 Download your creation as a **text file** or **HTML** for colorized output. 183 | """) 184 | elif page == "Live": 185 | st.markdown(""" 186 | **Note**: 187 | Click the **`START`** button and allow the camera permissions. 188 | """) 189 | webrtc_ctx = webrtc_streamer( 190 | key="video-sendonly", 191 | mode=WebRtcMode.SENDONLY, 192 | media_stream_constraints={"video": True}, 193 | ) 194 | image_place = st.empty() 195 | while True: 196 | if webrtc_ctx.video_receiver: 197 | try: 198 | video_frame = webrtc_ctx.video_receiver.get_frame( 199 | timeout=1) 200 | except Exception as e: 201 | print(e) 202 | break 203 | img_rgb = video_frame.to_ndarray(format="rgb24") 204 | image = Image.fromarray(img_rgb) 205 | image_resized = resize_image(image, 100, "basic") 206 | ascii_art = map_pixels_to_ascii( 207 | image_resized, ASCII_PATTERNS["basic"]) 208 | image_place.text(ascii_art) 209 | 210 | 211 | @app.command() 212 | def generate( 213 | image_path: str = typer.Argument(..., help="Path to the input image"), 214 | width: int = typer.Option(100, help="Width of the ASCII art"), 215 | pattern: str = typer.Option("basic", help="ASCII pattern to use"), 216 | colorize: bool = typer.Option(False, help="Generate colorized ASCII art"), 217 | theme: str = typer.Option("grayscale", help="Color theme for colorized output"), 218 | brightness: float = typer.Option(1.0, help="Brightness adjustment"), 219 | contrast: float = typer.Option(1.0, help="Contrast adjustment"), 220 | blur: bool = typer.Option(False, help="Apply blur effect"), 221 | sharpen: bool = typer.Option(False, help="Apply sharpen effect"), 222 | contours: bool = typer.Option(False, help="Apply contour effect"), 223 | invert: bool = typer.Option(False, help="Invert the image"), 224 | output: str = typer.Option(None, help="Output file path") 225 | ): 226 | """Generate ASCII art from an image with various customization options.""" 227 | 228 | with Progress() as progress: 229 | task = progress.add_task("[green]Processing image...", total=100) 230 | 231 | # Load and process the image 232 | image = Image.open(image_path) 233 | progress.update(task, advance=20) 234 | 235 | image = resize_image(image, width) 236 | progress.update(task, advance=20) 237 | 238 | image = apply_image_filters(image, brightness, contrast, blur, sharpen) 239 | progress.update(task, advance=20) 240 | 241 | if contours: 242 | image = create_contours(image) 243 | 244 | if invert: 245 | image = ImageOps.invert(image.convert('RGB')) 246 | progress.update(task, advance=20) 247 | 248 | ascii_art = create_ascii_art(image, pattern, colorize, theme) 249 | progress.update(task, advance=20) 250 | 251 | # Display or save the result 252 | if output: 253 | with open(output, 'w', encoding='utf-8') as f: 254 | f.write(ascii_art) 255 | console.print(f"ASCII art saved to {output}") 256 | else: 257 | console.print(Panel(ascii_art, title="ASCII Art", expand=False)) 258 | 259 | if __name__ == "__main__": 260 | if (len(sys.argv) > 1): 261 | app() 262 | else: 263 | run_streamlit_app() -------------------------------------------------------------------------------- /dog.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-to-mastery/ascii-art/f0ca8de3e0c9c841182682bd9c41a7593414e1a2/dog.gif -------------------------------------------------------------------------------- /example/makeArtPython3.py: -------------------------------------------------------------------------------- 1 | # this project requires Pillow installation: https://pillow.readthedocs.io/en/stable/installation.html 2 | 3 | # code credit goes to: https://www.hackerearth.com/practice/notes/beautiful-python-a-simple-ascii-art-generator-from-images/ 4 | # Extra Task: Make this code work with Python3... you will notice you will have a few errors to fix! If you get stuck, see the answer @ make_art.py 5 | from PIL import Image 6 | 7 | ASCII_CHARS = ["#", "?", "%", ".", "S", "+", ".", "*", ":", ",", "@"] 8 | 9 | 10 | def scale_image(image, new_width=100): 11 | """Resizes an image preserving the aspect ratio.""" 12 | (original_width, original_height) = image.size 13 | aspect_ratio = original_height / float(original_width) 14 | new_height = int(aspect_ratio * new_width) 15 | 16 | new_image = image.resize((new_width, new_height)) 17 | return new_image 18 | 19 | 20 | def convert_to_grayscale(image): 21 | return image.convert("L") 22 | 23 | 24 | def map_pixels_to_ascii_chars(image, range_width=25): 25 | """Maps each pixel to an ascii char based on the range 26 | in which it lies. 27 | 28 | 0-255 is divided into 11 ranges of 25 pixels each. 29 | """ 30 | 31 | pixels_in_image = list(image.getdata()) 32 | pixels_to_chars = [ASCII_CHARS[pixel_value // range_width] for pixel_value in pixels_in_image] 33 | 34 | return "".join(pixels_to_chars) 35 | 36 | 37 | def convert_image_to_ascii(image, new_width=100): 38 | image = scale_image(image) 39 | image = convert_to_grayscale(image) 40 | 41 | pixels_to_chars = map_pixels_to_ascii_chars(image) 42 | len_pixels_to_chars = len(pixels_to_chars) 43 | 44 | image_ascii = [pixels_to_chars[index : index + new_width] for index in range(0, len_pixels_to_chars, new_width)] 45 | 46 | return "\n".join(image_ascii) 47 | 48 | 49 | def handle_image_conversion(image_filepath): 50 | image = None 51 | try: 52 | image = Image.open(image_filepath) 53 | 54 | except Exception as e: 55 | print("Unable to open image file {image_filepath}.".format(image_filepath=image_filepath)) 56 | print(f"Error: {e}") 57 | return 58 | 59 | image_ascii = convert_image_to_ascii(image) 60 | print(image_ascii) 61 | 62 | 63 | if __name__ == "__main__": 64 | import sys 65 | 66 | image_file_path = sys.argv[1] 67 | handle_image_conversion(image_file_path) 68 | -------------------------------------------------------------------------------- /example/make_art.py: -------------------------------------------------------------------------------- 1 | # this project requires Pillow installation: https://pillow.readthedocs.io/en/stable/installation.html 2 | 3 | # code credit goes to: https://www.hackerearth.com/practice/notes/beautiful-python-a-simple-ascii-art-generator-from-images/ 4 | # code modified to work with Python 3 by @aneagoie 5 | from PIL import Image 6 | 7 | ASCII_CHARS = ["#", "?", "%", ".", "S", "+", ".", "*", ":", ",", "@"] 8 | 9 | 10 | def scale_image(image, new_width=100): 11 | """Resizes an image preserving the aspect ratio.""" 12 | (original_width, original_height) = image.size 13 | aspect_ratio = original_height / float(original_width) 14 | new_height = int(aspect_ratio * new_width) 15 | 16 | new_image = image.resize((new_width, new_height)) 17 | return new_image 18 | 19 | 20 | def convert_to_grayscale(image): 21 | return image.convert("L") 22 | 23 | 24 | def map_pixels_to_ascii_chars(image, range_width=25): 25 | """Maps each pixel to an ascii char based on the range 26 | in which it lies. 27 | 28 | 0-255 is divided into 11 ranges of 25 pixels each. 29 | """ 30 | 31 | pixels_in_image = list(image.getdata()) 32 | pixels_to_chars = [ASCII_CHARS[int(pixel_value / range_width)] for pixel_value in pixels_in_image] 33 | 34 | return "".join(pixels_to_chars) 35 | 36 | 37 | def convert_image_to_ascii(image, new_width=100): 38 | image = scale_image(image) 39 | image = convert_to_grayscale(image) 40 | 41 | pixels_to_chars = map_pixels_to_ascii_chars(image) 42 | len_pixels_to_chars = len(pixels_to_chars) 43 | 44 | image_ascii = [pixels_to_chars[index : index + new_width] for index in range(0, len_pixels_to_chars, new_width)] 45 | 46 | return "\n".join(image_ascii) 47 | 48 | 49 | def handle_image_conversion(image_filepath): 50 | image = None 51 | try: 52 | image = Image.open(image_filepath) 53 | except Exception as e: 54 | print(f"Unable to open image file {image_filepath}.") 55 | print(e) 56 | return 57 | 58 | image_ascii = convert_image_to_ascii(image) 59 | print(image_ascii) 60 | 61 | 62 | if __name__ == "__main__": 63 | import sys 64 | 65 | image_file_path = sys.argv[1] 66 | print(image_file_path) 67 | handle_image_conversion(image_file_path) 68 | -------------------------------------------------------------------------------- /example/ztm-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-to-mastery/ascii-art/f0ca8de3e0c9c841182682bd9c41a7593414e1a2/example/ztm-logo.png -------------------------------------------------------------------------------- /example/ztm-logo_ascii_art.txt: -------------------------------------------------------------------------------- 1 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@;+++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 2 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+++++*****@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 3 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@++++++++*++****+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 4 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+++++++++++****+++****@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 5 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@++++++++++++++*******+++****@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 6 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*+++++++++++++++++***********++***+*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 7 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@*++++++++++++++++++++**************++*****@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 8 | @@@@@@@@@@@@@@@@@@@@@@@@@@++++++++++++++++++++++++*****************++**++*@@@@@@@@@@@@@@@@@@@@@@@@@@ 9 | @@@@@@@@@@@@@@@@@@@@@@@+++++++++++++++++++++++++++++******************+++****@@@@@@@@@@@@@@@@@@@@@@@ 10 | @@@@@@@@@@@@@@@@@@@@++++++++++++++++++++++++++++++***++******************+++***+@@@@@@@@@@@@@@@@@@@@ 11 | @@@@@@@@@@@@@@@@?+++++++++++++++++++++++++++++++@@@@++**+++*****************+++**+*?@@@@@@@@@@@@@@@@ 12 | @@@@@@@@@@@@@++++++++++++++++++++++++++++++++@@@@@@@@@@+***+++******************++***++@@@@@@@@@@@@@ 13 | @@@@@@@@@@++++++++++++++++++++++++++++++++@@@@@@@@@@@@@@@@****+++******************++*****@@@@@@@@@@ 14 | @@@@@@@++++++++++++++++++++++++++++++++@@@@@@@@@@@@@@@@@@@@@@****+++******************+++***+@@@@@@@ 15 | @@@@++++++++++++++++++++++++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@+****++******************+++***+@@@@ 16 | @+++++++++++++++++++++++++++++++*@@@@@@@@@@@@@@@?%%%@@@@@@@@@@@@@@@*+***++******************+++****@ 17 | @++++++++++++++++++++++++++++@@@@@@@@@@@@@@@@%%%%%%%%%%@@@@@@@@@@@@@@@;****++******************++++@ 18 | @+++++++++++++++++++++++++@@@@@@@@@@@@@@@@%??%???%%%%%%%%%@@@@@@@@@@@@@@@@****+++**********++++++++@ 19 | @++++++++++++++++++++++@@@@@@@@@@@@@@@@%?%%???%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@@++**+++****+++++++++++@ 20 | @+++++++++++++++++++@@@@@@@@@@@@@@@??%%%???%%%%%%%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@*****++++++++++++++@ 21 | @+++++++++++++++****@@@@@@@@@@@@????%??%%%%%%%%%%%%%%%%%%%%%%%%%%%%S@@@@@@@@@@@@@++++++++++++++++++@ 22 | @+++++++++++****++****+@@@@@@%%%%%??%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@@@@@@@+++++++++++++++++++++@ 23 | @++++++++**********++***++%%%%???%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%+++++++++++++++++++++++++@ 24 | @+++++***************++**?%???%%%%%%%%%%%%%%%%%%??%%%%%%%%%%%%%%%%%%%%%%?*+++++++++++++++++++++++++@ 25 | @++**++***********+***??%%?%%%%%%%%%%%%%%%%%???%%%%%%%%%%%%%%%%%%%%%%?**++++++++++++++++++++++++++;@ 26 | @@@*****++*****+**???%%%%%%%%%%%%%%%%%%%%???%%?%@@@@%%%%%%%%%%%%%%?*+++++++++++++++++++++++++++++@@@ 27 | @@@@@@;+***++**??%%%%%%%%%%%%%%%%%%%%%%%%%%??@@@@@@@@@@%%%%%%%%?*+++++++++++++++++++++++++++++@@@@@@ 28 | @@@@@@@@@@**??%%%%%%%%%%%%%%%%%%%%%%%%????@@@@@@@@@@@@@@@@%??*+++++++++++++++++++++++++***@@@@@@@@@@ 29 | @@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%??**++*+**@@@@@@@@@@*++++++++++++++++++++++++++++*?%%%%%%%@@@@@@@ 30 | @@@@%%%%???%%%%%%%%%%%%%%%%%%%%???**+****++****+@@@@*++++++++++++++++++++++++++++*?%%%%%%%%%%%%%@@@@ 31 | @%%%%???%%%%%%%%%%%%%%%%%?%%??***+**********+++***+++++++++++++++++++++++++++**?%%%%%%%%%%%%%%%%%%%@ 32 | @???%%%%%%%%%%%%%%%%%%???%?**++****************+++++++++++++++++++++++++++**?%%%%%%%%%%%%%%%%%%%%??@ 33 | @?%%%%%%%%%%%%%%%%%???%?%?+++*+++*****************++++++++++++++++++++++++%%%%%%%%%%%%%%%%%%%%%%%%?@ 34 | @?%%%%%%%%%%%%%%??%%??%@@@@@@****+++**************+++++++++++++++++++++@@@@@@%%%%%%%%%%%%%%%%%%%%%?@ 35 | @?%%%%%%%%%%%%%%%???@@@@@@@@@@@@*****++***********++++++++++++++++++@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%?@ 36 | @?%%%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@+****++********+++++++++++++++@@@@@@@@@@@@@@@@??%%%%%%%%%%%%%%%?@ 37 | @?%%%%%%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@+****++*****++++++++++++@@@@@@@@@@@@@@@%??%%??%%%%%%%%%%%%%%?@ 38 | @?%%%%%%%%%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@+*+**+++*+++++++++@@@@@@@@@@@@@@@?%%%%??%%%%%%%%%%%%%%%%%?@ 39 | @??%%%%%%%%%%%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@@****++++++@@@@@@@@@@@@@@@@?%?%???%%%%%%%%%%%%%%%%%%???@ 40 | @%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@@++++@@@@@@@@@@@@@@@@?%%%???%%%%%%%%%%%%%%%%%%??%%%?@ 41 | @@@S%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%?@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%???%%%%%%%%%%%%%%%%%???%%%??@@@ 42 | @@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%S@@@@@@@@@@@@@@@@@@@@@@%?%%%???%%%%%%%%%%%%%%%%%???%?%?%@@@@@@ 43 | @@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@@?%%%%??%%%%%%%%%%%%%%%%%%???%%%?@@@@@@@@@@ 44 | @@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@@@@@@@@@@%%%%%??%%%%%%%%%%%%%%%%%%???%%%%@@@@@@@@@@@@@ 45 | @@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@@@@??%%???%%%%%%%%%%%%%%%%%%??%%%%%@@@@@@@@@@@@@@@@ 46 | @@@@@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%???%%%%%%%%%%%%%%%%%%??%%%%?@@@@@@@@@@@@@@@@@@@ 47 | @@@@@@@@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%???%%%%%%%%%%%%%%%%%???%%%?%@@@@@@@@@@@@@@@@@@@@@@ 48 | @@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%???%%??@@@@@@@@@@@@@@@@@@@@@@@@@@ 49 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%???%??%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 50 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%??%%%?%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 51 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%??%%%%?@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 52 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%???%%?%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 53 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%%%???%%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 54 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%%%?@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 55 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%?@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 56 | -------------------------------------------------------------------------------- /fast-api-main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | import re 3 | import webbrowser 4 | import threading 5 | import time 6 | from fastapi import FastAPI 7 | from fastapi_source.host.ascii_art_routes import router as ascii_router 8 | from starlette.routing import Route 9 | from fastapi_source.core.config import settings 10 | 11 | # Initialize FastAPI app with project metadata 12 | app = FastAPI( 13 | title=settings.PROJECT_NAME_EN_US, 14 | version=settings.VERSION, 15 | openapi_tags=[ 16 | { 17 | "name": settings.ROUTER_NAME_Object_Detection, 18 | } 19 | ] 20 | ) 21 | 22 | # Register the ASCII art router 23 | app.include_router(ascii_router) 24 | 25 | # Convert all API routes to be case-insensitive 26 | for route in app.router.routes: 27 | if isinstance(route, Route): 28 | route.path_regex = re.compile(route.path_regex.pattern, re.IGNORECASE) 29 | 30 | # Function to open the browser to the API documentation 31 | def open_browser(): 32 | """Open the API documentation in the default browser after a delay.""" 33 | time.sleep(2) # wait for server to start 34 | webbrowser.open("http://127.0.0.1:8000/docs") 35 | 36 | # Start the API with live reloading and auto browser open 37 | if __name__ == "__main__": 38 | print("Starting server and waiting to open the browser...") 39 | threading.Thread(target=open_browser).start() 40 | uvicorn.run("fast-api-main:app", host="0.0.0.0", port=8000, reload=True) 41 | 42 | # Example usage: 43 | # python3 fast-api-main.py 44 | -------------------------------------------------------------------------------- /fastapi_source/application/ascii/ascii_service.py: -------------------------------------------------------------------------------- 1 | import io, numpy as np 2 | from typing import List 3 | from PIL import Image, ImageDraw 4 | 5 | def __get_image_from_bytes(byte_contents: bytes) -> Image: 6 | """_summary_ 7 | Args: 8 | byte_contents (bytes): Input image bytes. 9 | Returns: 10 | Image: Output image. 11 | """ 12 | return Image.open(io.BytesIO(byte_contents)).convert('RGBA') 13 | 14 | def get_mosaic_image(contents: bytes, block_size:int=10) -> Image: 15 | """_summary_ 16 | Args: 17 | contents (bytes): Input image bytes. 18 | block_size (int, optional): Sidelength of a mosaic block. Defaults to 10. 19 | Returns: 20 | Image: Output image. 21 | """ 22 | image = __get_image_from_bytes(contents) 23 | 24 | img = np.array(image) 25 | 26 | #get height and width of image 27 | height, width, _ = img.shape 28 | 29 | #create an empty image 30 | result = Image.new('RGBA', (width, height), (0, 0, 0, 0)) 31 | draw = ImageDraw.Draw(result) 32 | 33 | #split image "blocks" 34 | for y in range(0, height, block_size): 35 | for x in range(0, width, block_size): 36 | #get a block 37 | block = img[y:y+block_size, x:x+block_size] 38 | 39 | #get block color alpha channel 40 | alpha_channel = block[:, :, 3] 41 | 42 | #calculate the average of alpha value: avg_alpha 43 | avg_alpha = np.mean(alpha_channel) 44 | 45 | if avg_alpha < 50: 46 | continue #nearly transparent 47 | 48 | #calculate the avg_color of this block(only RGB channels) 49 | avg_color = np.mean(block[:, :, :3], axis=(0, 1)).astype(int) 50 | color = tuple(avg_color) + (255,) #set alpha to 255 51 | 52 | #draw the block 53 | draw.rectangle([x, y, x+block_size, y+block_size], fill=color) 54 | 55 | return result 56 | 57 | -------------------------------------------------------------------------------- /fastapi_source/core/config.py: -------------------------------------------------------------------------------- 1 | class Settings: 2 | PROJECT_NAME_EN_US: str = "Ascii-Art Api" 3 | VERSION: str = "1.0.0" 4 | 5 | #router setting 6 | ROUTER_NAME_Object_Detection, ROUTER_Description_Object_Detection = ("AsciiArt", "Enjoy 😎😎😎") 7 | 8 | settings = Settings() -------------------------------------------------------------------------------- /fastapi_source/host/ascii_art_routes.py: -------------------------------------------------------------------------------- 1 | import io 2 | from fastapi.responses import StreamingResponse 3 | from fastapi import APIRouter, UploadFile, File, Query 4 | from fastapi_source.core.config import settings 5 | from fastapi_source.application.ascii.ascii_service import get_mosaic_image 6 | 7 | router = APIRouter(prefix=f'/{settings.ROUTER_NAME_Object_Detection}', 8 | tags=[settings.ROUTER_NAME_Object_Detection]) 9 | 10 | @router.put("/Mosaic", summary = "Make your image mosaic! 😁", 11 | description = 'Upload your image file, and make it mosaic. 😃', 12 | response_class = StreamingResponse, 13 | responses = {200: {"content": {"image/png": {}}}}) 14 | async def detect(image_file: UploadFile = File(..., description="upload image file"), 15 | block_size: int=Query(description="Sidelength of a mosaic block. Default value=10", default=10)): 16 | 17 | contents = await image_file.read() 18 | result_image = get_mosaic_image(contents, block_size) 19 | 20 | #save images to bytes 21 | img_byte_arr = io.BytesIO() 22 | result_image.save(img_byte_arr, format="PNG") 23 | img_byte_arr.seek(0) 24 | 25 | return StreamingResponse(img_byte_arr, media_type = "image/png") -------------------------------------------------------------------------------- /geometric-art.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | from PIL import Image 6 | from scipy.spatial import Delaunay 7 | 8 | 9 | # Load and preprocess the image 10 | def load_image(image_path, resize_factor=1): 11 | img = Image.open(image_path) 12 | img = img.resize((int(img.width * resize_factor), int(img.height * resize_factor))) 13 | return np.array(img) 14 | 15 | 16 | # Generate random points over the image 17 | def generate_points(image, num_points=500): 18 | height, width, _ = image.shape 19 | # Random points across the image dimensions 20 | points = np.vstack((np.random.randint(0, width, num_points), np.random.randint(0, height, num_points))).T 21 | # Add corners to ensure triangulation covers the entire image 22 | points = np.vstack([points, [[0, 0], [0, height], [width, 0], [width, height]]]) 23 | return points 24 | 25 | 26 | # Create Delaunay triangulation from points 27 | def create_delaunay_triangulation(points): 28 | return Delaunay(points) 29 | 30 | 31 | # Draw triangles on the image using the Delaunay triangulation 32 | def draw_geometric_art(image, points, triangulation, output_path=None): 33 | fig, ax = plt.subplots() 34 | ax.set_aspect("equal") 35 | ax.imshow(image) 36 | 37 | for triangle in triangulation.simplices: 38 | vertices = points[triangle] 39 | # Get the color of the center of the triangle 40 | center = np.mean(vertices, axis=0).astype(int) 41 | color = image[center[1], center[0], :] / 255 42 | 43 | # Draw the triangle with the calculated color 44 | triangle_shape = plt.Polygon(vertices, color=color) 45 | ax.add_patch(triangle_shape) 46 | 47 | ax.set_axis_off() 48 | if output_path: 49 | plt.savefig(output_path, bbox_inches="tight", pad_inches=0, facecolor=(0.5, 0.5, 0.5, 0.0)) 50 | else: 51 | plt.show() 52 | 53 | 54 | # Main function to convert image to geometric art 55 | def create_geometric_art(image_path, output_path=None, num_points=500, resize_factor=1): 56 | # Load image and generate points 57 | image = load_image(image_path, resize_factor) 58 | points = generate_points(image, num_points) 59 | 60 | # Create triangulation and draw art 61 | triangulation = create_delaunay_triangulation(points) 62 | draw_geometric_art(image, points, triangulation, output_path) 63 | 64 | 65 | if __name__ == "__main__": 66 | import sys 67 | 68 | if len(sys.argv) < 2: 69 | print("Usage: python3 geometric-art.py [output_image] [num_points]") 70 | sys.exit(1) 71 | 72 | image_file_path = sys.argv[1] 73 | output_file_path = sys.argv[2] if len(sys.argv) > 2 else None 74 | num_points = int(sys.argv[3]) if len(sys.argv) > 3 else 500 75 | 76 | create_geometric_art(image_file_path, output_file_path, num_points) 77 | 78 | # Example usage: 79 | # python3 geometric-art.py example/ztm-logo.png 80 | # python3 geometric-art.py example/ztm-logo.png geometric-art.png 81 | # python3 geometric-art.py example/ztm-logo.png geometric-art.png 1000 82 | -------------------------------------------------------------------------------- /giphy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-to-mastery/ascii-art/f0ca8de3e0c9c841182682bd9c41a7593414e1a2/giphy.webp -------------------------------------------------------------------------------- /gui.py: -------------------------------------------------------------------------------- 1 | import importlib.util 2 | from tkinter import Menu, Scrollbar, StringVar, Text, Tk, Toplevel, filedialog, ttk 3 | from tkinter.constants import END, FALSE, NORMAL, E, N, S, W 4 | 5 | # Workaroung because of hyphen in filename 6 | module_name = "community-version" 7 | file_path = "./community-version.py" 8 | spec = importlib.util.spec_from_file_location(module_name, file_path) 9 | community_version = importlib.util.module_from_spec(spec) 10 | spec.loader.exec_module(community_version) 11 | 12 | 13 | def browse_image(): 14 | file_path = filedialog.askopenfilename(filetypes=[("JPEG", "*.jpg *.jpeg"), ("PNG", "*.png"), ("BMP", "*.bmp"), ("TIFF", "*.tiff")]) 15 | if file_path: 16 | image_path.set(file_path) 17 | 18 | 19 | def convert_image(): 20 | image = image_path.get() 21 | if image: 22 | ascii_art = community_version.handle_image_conversion(image) 23 | display_ascii_art(ascii_art) 24 | 25 | 26 | def save_file(ascii_art): 27 | file_path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text file", "*.txt")]) 28 | if file_path: 29 | with open(file_path, "w") as file: 30 | file.write(ascii_art) 31 | 32 | 33 | def display_ascii_art(ascii_art): 34 | # Create ascii art preview window 35 | preview_window = Toplevel(root) 36 | preview_window.title("ZTM - Ascii Preview") 37 | preview_window.iconbitmap("ztm-icon.ico") 38 | preview_window.columnconfigure(0, weight=1) 39 | preview_window.rowconfigure(0, weight=1) 40 | 41 | # Create Preview Window Frame 42 | preview_frame = ttk.Frame(preview_window, padding="3 3 12 12") 43 | preview_frame.grid(column=0, row=0, sticky=(N, W, E, S)) 44 | preview_frame.columnconfigure(0, weight=1) 45 | preview_frame.rowconfigure(0, weight=1) 46 | 47 | # Create text widget to display the ascii art 48 | text_widget = Text(preview_frame, wrap="none") # Set wrap to none 49 | text_widget.insert(END, ascii_art) 50 | text_widget.config(state=NORMAL) 51 | text_widget.grid(column=0, row=0, sticky=(N, W, E, S)) 52 | 53 | # Add vertical scrollbar 54 | vert_scroll_bar = Scrollbar(preview_frame, orient="vertical", command=text_widget.yview) 55 | text_widget.config(yscrollcommand=vert_scroll_bar.set) 56 | vert_scroll_bar.grid(column=1, row=0, sticky=(N, S)) 57 | 58 | # Add horizontal scrollbar 59 | horz_scroll_bar = Scrollbar(preview_frame, orient="horizontal", command=text_widget.xview) 60 | text_widget.config(xscrollcommand=horz_scroll_bar.set) 61 | horz_scroll_bar.grid(column=0, row=1, sticky=(W, E)) 62 | 63 | # Add File Menu 64 | preview_window.option_add("*tearOff", FALSE) 65 | menubar = Menu(preview_window) 66 | preview_window["menu"] = menubar 67 | menu_file = Menu(menubar) 68 | menubar.add_cascade(menu=menu_file, label="File") 69 | 70 | # Add Save functionality to File Menu 71 | menu_file.add_command(label="Save As...", command=lambda: save_file(ascii_art)) 72 | 73 | 74 | # Create the root window 75 | root = Tk() 76 | root.title("ZTM - Ascii Art") 77 | root.iconbitmap("ztm-icon.ico") 78 | 79 | # Main Window Frame 80 | mainframe = ttk.Frame(root, padding="3 3 12 12") 81 | mainframe.grid(column=0, row=0, sticky=(N, W, E, S)) 82 | root.columnconfigure(0, weight=1) 83 | root.rowconfigure(0, weight=1) 84 | 85 | # Labels 86 | ttk.Label(mainframe, text="Select an image:").grid(column=0, row=0, sticky=(W)) 87 | 88 | # Image path field 89 | image_path = StringVar() 90 | image_entry = ttk.Entry(mainframe, width=50, textvariable=image_path).grid(column=1, row=1, sticky=(W, E), padx=0) 91 | 92 | # Buttons 93 | ttk.Button(mainframe, text="Browse", command=browse_image).grid(column=0, row=1, sticky=(W)) 94 | ttk.Button(mainframe, text="Convert", command=convert_image).grid(column=0, row=2, sticky=(W)) 95 | 96 | # Add padding 97 | for child in mainframe.winfo_children(): 98 | child.grid_configure(padx=0, pady=5) 99 | 100 | # Run mainloop 101 | root.mainloop() 102 | -------------------------------------------------------------------------------- /invert-color-art.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import io 3 | import os 4 | import random 5 | 6 | import matplotlib.pyplot as plt 7 | import numpy as np 8 | from PIL import Image, ImageOps 9 | 10 | 11 | def get_image_from_bytes(byte_contents: bytes) -> Image: 12 | """_summary_ 13 | 14 | Args: 15 | byte_contents (bytes): Input image bytes. 16 | 17 | Returns: 18 | Image: Output image. 19 | """ 20 | return Image.open(io.BytesIO(byte_contents)).convert('RGBA') 21 | 22 | def get_image_from_path(image_path: str) -> Image: 23 | """_summary_ 24 | 25 | Args: 26 | image_path (str): Input image path. 27 | 28 | Returns: 29 | Image: Output image. 30 | """ 31 | return Image.open(image_path).convert('RGBA') 32 | 33 | 34 | def get_invert_colors_image(input_image: Image) -> Image: 35 | """_summary_ 36 | 37 | Args: 38 | input_image (Image): Input image. 39 | 40 | Returns: 41 | Image: Output image. 42 | """ 43 | 44 | # Apply invert effect 45 | inverted_img = ImageOps.invert(input_image.convert("RGB")) # Invert RGB channels 46 | inverted_img = Image.merge("RGBA", (*inverted_img.split()[:3], input_image.split()[3])) # Restore the alpha channel 47 | 48 | return inverted_img 49 | 50 | if __name__ == "__main__": 51 | parser = argparse.ArgumentParser(description="Create a color-inverted image from an input image.") 52 | parser.add_argument("--input", "-i", type=str, help="Path to the input image.") 53 | parser.add_argument("--output", "-o", type=str, help="Path to save the output color-inverted image. (default value: None, and it will 'Show')", default=None) 54 | 55 | args = parser.parse_args() 56 | 57 | # check 'input' argument 58 | if not args.input: 59 | import sys 60 | print("No input file provided!") 61 | sys.exit(1) 62 | 63 | original_img = get_image_from_path(args.input) 64 | inverted_img = get_invert_colors_image(original_img) 65 | 66 | if args.output: 67 | inverted_img.save(args.output, 'PNG') 68 | else: 69 | plt.imshow(inverted_img) 70 | plt.axis('off') 71 | plt.show() 72 | 73 | # Example: 74 | # 1. python3 invert-color-art.py -i example/ztm-logo.png 75 | # 3. python3 invert-color-art.py -i example/ztm-logo.png -o inverted-color-art.png -------------------------------------------------------------------------------- /mosaic-art.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import io 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | from PIL import Image, ImageDraw 7 | 8 | 9 | def get_image_from_bytes(byte_contents: bytes) -> Image: 10 | """_summary_ 11 | 12 | Args: 13 | byte_contents (bytes): Input image bytes. 14 | 15 | Returns: 16 | Image: Output image. 17 | """ 18 | return Image.open(io.BytesIO(byte_contents)).convert("RGBA") 19 | 20 | 21 | def get_image_from_path(image_path: str) -> Image: 22 | """_summary_ 23 | 24 | Args: 25 | image_path (str): Input image path. 26 | 27 | Returns: 28 | Image: Output image. 29 | """ 30 | return Image.open(image_path).convert("RGBA") 31 | 32 | 33 | def get_mosaic_image(input_image: Image, block_size: int = 5) -> Image: 34 | """_summary_ 35 | 36 | Args: 37 | input_image (Image): Input image. 38 | block_size (int, optional): Sidelength of a mosaic block. Defaults to 5. 39 | 40 | Returns: 41 | Image: Output image. 42 | """ 43 | # read image in RGBA 44 | img = np.array(input_image) 45 | 46 | # get height and width of image 47 | height, width, _ = img.shape 48 | 49 | # create an empty image 50 | result_img = Image.new("RGBA", (width, height), (0, 0, 0, 0)) # 初始化為完全透明 51 | draw = ImageDraw.Draw(result_img) 52 | 53 | # split image "blocks" 54 | for y in range(0, height, block_size): 55 | for x in range(0, width, block_size): 56 | # get a block 57 | block = img[y : y + block_size, x : x + block_size] 58 | 59 | # get block color alpha channel 60 | alpha_channel = block[:, :, 3] 61 | 62 | # calculate the average of alpha value: avg_alpha 63 | avg_alpha = np.mean(alpha_channel) 64 | 65 | if avg_alpha < 50: 66 | continue # nearly transparent 67 | 68 | # calculate the avg_color of this block(only RGB channels) 69 | avg_color = np.mean(block[:, :, :3], axis=(0, 1)).astype(int) 70 | color = tuple(avg_color) + (255,) # set alpha to 255 71 | 72 | # draw the block 73 | draw.rectangle([x, y, x + block_size, y + block_size], fill=color) 74 | 75 | return result_img 76 | 77 | 78 | if __name__ == "__main__": 79 | parser = argparse.ArgumentParser(description="Create a mosaic image from an input image.") 80 | 81 | parser.add_argument("--input", "-i", type=str, help="Path to the input image.") 82 | parser.add_argument("--output", "-o", type=str, help="Path to save the output mosaic image. (default value: None, and it will 'Show')", default=None) 83 | parser.add_argument("--block_size", "-b", type=int, help="Block size for the mosaic effect. (default value: 5)", default=5) 84 | 85 | args = parser.parse_args() 86 | 87 | # check 'input' argument 88 | if not args.input: 89 | import sys 90 | 91 | print("No input file provided!") 92 | sys.exit(1) 93 | 94 | original_img = get_image_from_path(args.input) 95 | result_img = get_mosaic_image(original_img, args.block_size) 96 | 97 | if args.output: 98 | result_img.save(args.output, "PNG") 99 | else: 100 | plt.imshow(result_img) 101 | plt.axis("off") 102 | plt.show() 103 | 104 | # Example: 105 | # 1. python3 mosaic-art.py -i example/ztm-logo.png 106 | # 2. python3 mosaic-art.py -i example/ztm-logo.png -b 20 107 | # 3. python3 mosaic-art.py -i example/ztm-logo.png -b 15 -o mosaic-art.png 108 | -------------------------------------------------------------------------------- /pointillism-art.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from PIL import Image 4 | from skimage import color, filters 5 | 6 | 7 | # Load and resize image 8 | def load_image(image_path, size=(400, 400)): 9 | img = Image.open(image_path).resize(size) # Resize for faster processing 10 | img = img.convert("RGB") # Ensure it's RGB format 11 | return np.array(img) 12 | 13 | 14 | # Generate random points across the image 15 | def generate_random_points(image, num_points): 16 | height, width, _ = image.shape 17 | points = np.random.rand(num_points, 2) 18 | points[:, 0] *= width # Scale x-coordinates 19 | points[:, 1] *= height # Scale y-coordinates 20 | return points 21 | 22 | 23 | # Sample color from the image at each point with random perturbation and subtle sepia tone 24 | def sample_colors(image, points): 25 | height, width, _ = image.shape 26 | colors = [] 27 | for point in points: 28 | x, y = int(point[0]), int(point[1]) 29 | x = min(x, width - 1) # Ensure index within bounds 30 | y = min(y, height - 1) 31 | color = image[y, x] # Note that the y-axis comes first in image indexing 32 | 33 | # Add random perturbation to the color 34 | perturbation = np.random.randint(-20, 21, size=3) # Random values between -20 and 20 35 | perturbed_color = np.clip(color + perturbation, 0, 255) # Ensure values are within [0, 255] 36 | 37 | # Apply subtle sepia tone effect 38 | tr = int(0.393 * perturbed_color[0] + 0.769 * perturbed_color[1] + 0.189 * perturbed_color[2]) 39 | tg = int(0.349 * perturbed_color[0] + 0.686 * perturbed_color[1] + 0.168 * perturbed_color[2]) 40 | tb = int(0.272 * perturbed_color[0] + 0.534 * perturbed_color[1] + 0.131 * perturbed_color[2]) 41 | 42 | sepia_color = np.clip([tr, tg, tb], 0, 255) 43 | 44 | # Blend the original perturbed color with the sepia color 45 | blend_ratio = 0.5 # Adjust this value to control the intensity of the sepia effect 46 | blended_color = np.clip((1 - blend_ratio) * perturbed_color + blend_ratio * sepia_color, 0, 255) 47 | 48 | colors.append(blended_color) 49 | return np.array(colors) 50 | 51 | 52 | # Detect edges in the image using Sobel filter 53 | def detect_edges(image): 54 | grayscale_image = color.rgb2gray(image) 55 | edges = filters.sobel(grayscale_image) 56 | return edges 57 | 58 | 59 | # Compute point sizes based on edge proximity 60 | def compute_point_sizes(points, edges, min_size=1, max_size=50): 61 | height, width = edges.shape 62 | point_sizes = [] 63 | edge_values = [] # List to collect edge values 64 | 65 | for point in points: 66 | x, y = int(point[0]), int(point[1]) 67 | x = min(x, width - 1) # Ensure index within bounds 68 | y = min(y, height - 1) 69 | 70 | # Higher edge value means the point is near an edge (smaller point size) 71 | edge_value = edges[y, x] 72 | edge_values.append(edge_value) # Collect edge value 73 | 74 | # Normalize edge values 75 | min_edge_value = min(edge_values) 76 | max_edge_value = max(edge_values) 77 | if max_edge_value != min_edge_value: 78 | normalized_edge_values = [(ev - min_edge_value) / (max_edge_value - min_edge_value) for ev in edge_values] 79 | else: 80 | normalized_edge_values = [0 for _ in edge_values] # All values are the same 81 | 82 | for i, _ in enumerate(points): 83 | normalized_edge_value = normalized_edge_values[i] 84 | 85 | # Linearly interpolate between min and max size based on normalized edge value 86 | point_size = min_size + (1 - normalized_edge_value) * (max_size - min_size) 87 | point_sizes.append(point_size) 88 | 89 | return np.array(point_sizes) 90 | 91 | 92 | # Plot the pointillism image with edge-aware point sizes 93 | def create_pointillism_art(image_path, output_path=None, num_points=50000, min_size=1, max_size=50): 94 | # Load image 95 | img = load_image(image_path) 96 | 97 | # Generate random points 98 | points = generate_random_points(img, num_points) 99 | 100 | # Sample colors from the image based on points 101 | colors = sample_colors(img, points) 102 | 103 | # Detect edges in the image 104 | edges = detect_edges(img) 105 | 106 | # Compute point sizes based on proximity to edges 107 | point_sizes = compute_point_sizes(points, edges, min_size, max_size) 108 | 109 | # Plot the points with sampled colors and dynamic sizes 110 | plt.figure(figsize=(8, 8)) 111 | plt.scatter(points[:, 0], points[:, 1], c=colors / 255, s=point_sizes, edgecolor="none") 112 | plt.gca().invert_yaxis() # Match the image's coordinate system 113 | plt.axis("off") # Hide axis 114 | 115 | if output_path: 116 | # Save the plot as an image file 117 | plt.savefig(output_path, bbox_inches="tight", pad_inches=0, facecolor=(0.5, 0.5, 0.5, 0.0)) 118 | plt.close() 119 | else: 120 | # Show the plot 121 | plt.show() 122 | 123 | 124 | if __name__ == "__main__": 125 | import sys 126 | 127 | image_file_path: str = sys.argv[1] 128 | print(image_file_path) 129 | 130 | if len(sys.argv) == 2: 131 | create_pointillism_art(image_file_path) 132 | elif len(sys.argv) == 3: 133 | output_file_path = sys.argv[2] 134 | create_pointillism_art(image_file_path, output_file_path) 135 | print("Pointillism image file path: ", output_file_path) 136 | 137 | # Example usage: 138 | # python3 pointillism-art.py example/ztm-logo.png 139 | # python3 pointillism-art.py example/ztm-logo.png pointillism-art.png 140 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | authors = ["zero-to-mastery"] 3 | description = "" 4 | name = "ascii-art" 5 | package-mode = false 6 | readme = "README.md" 7 | version = "0.1.0" 8 | 9 | [tool.poetry.dependencies] 10 | GitPython = "3.1.43" 11 | Jinja2 = "3.1.4" 12 | MarkupSafe = "2.1.5" 13 | Pygments = "2.18.0" 14 | altair = "5.4.1" 15 | attrs = "24.2.0" 16 | blinker = "1.8.2" 17 | cachetools = "5.5.0" 18 | certifi = "2024.8.30" 19 | charset-normalizer = "3.3.2" 20 | click = "8.1.7" 21 | colorama = "0.4.6" 22 | contourpy = "1.3.0" 23 | cycler = "0.12.1" 24 | fonttools = "4.54.1" 25 | gitdb = "4.0.11" 26 | idna = "3.10" 27 | imageio = "2.35.1" 28 | jsonschema = "4.23.0" 29 | jsonschema-specifications = "2023.12.1" 30 | kiwisolver = "1.4.7" 31 | lazy_loader = "0.4" 32 | markdown-it-py = "2.2.0" 33 | matplotlib = "3.9.2" 34 | mdurl = "0.1.2" 35 | narwhals = "1.9.1" 36 | networkx = "3.3" 37 | numpy = "2.1.2" 38 | packaging = "24.1" 39 | pandas = "2.2.3" 40 | pillow = "10.4.0" 41 | pre-commit = "4.0.1" 42 | protobuf = "5.28.2" 43 | pyarrow = "17.0.0" 44 | pydeck = "0.9.1" 45 | pyparsing = "3.1.4" 46 | python = "^3.11" 47 | python-dateutil = "2.9.0.post0" 48 | pytz = "2024.2" 49 | referencing = "0.35.1" 50 | requests = "2.32.3" 51 | rich = "13.9.2" 52 | rpds-py = "0.20.0" 53 | ruff = "0.6.9" 54 | scikit-image = "0.24.0" 55 | scipy = "1.14.1" 56 | six = "1.16.0" 57 | smmap = "5.0.1" 58 | streamlit = "1.39.0" 59 | tenacity = "9.0.0" 60 | tifffile = "2024.9.20" 61 | toml = "0.10.2" 62 | torch = "2.0.0" 63 | tornado = "6.4.1" 64 | transformers = "4.37.0" 65 | typing_extensions = "4.12.2" 66 | tzdata = "2024.2" 67 | urllib3 = "2.2.3" 68 | watchdog = "5.0.3" 69 | 70 | [build-system] 71 | build-backend = "poetry.core.masonry.api" 72 | requires = ["poetry-core"] 73 | 74 | # https://docs.astral.sh/ruff/configuration/ 75 | 76 | [tool.ruff] 77 | # Exclude a variety of commonly ignored directories. 78 | exclude = [ 79 | ".bzr", 80 | ".direnv", 81 | ".eggs", 82 | ".git", 83 | ".git-rewrite", 84 | ".hg", 85 | ".ipynb_checkpoints", 86 | ".mypy_cache", 87 | ".nox", 88 | ".pants.d", 89 | ".pyenv", 90 | ".pytest_cache", 91 | ".pytype", 92 | ".ruff_cache", 93 | ".svn", 94 | ".tox", 95 | ".venv", 96 | ".vscode", 97 | "__pypackages__", 98 | "_build", 99 | "buck-out", 100 | "build", 101 | "dist", 102 | "node_modules", 103 | "site-packages", 104 | "venv", 105 | ] 106 | 107 | # Same as Black. 108 | indent-width = 4 109 | line-length = 200 110 | # line-length = 88 111 | 112 | # Assume Python 3.8 113 | # target-version = "py38" 114 | 115 | [tool.ruff.lint] 116 | ignore = [] 117 | select = [ 118 | "E", # pycodestyle errors 119 | "W", # pycodestyle warnings 120 | "F", # pyflakes 121 | "I", # isort 122 | "C", # flake8-comprehensions 123 | "B", # flake8-bugbear # "UP", # pyupgrade 124 | ] 125 | 126 | # Allow fix for all enabled rules (when `--fix`) is provided. 127 | fixable = ["ALL"] 128 | unfixable = [] 129 | 130 | # Allow unused variables when underscore-prefixed. 131 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 132 | 133 | [tool.ruff.format] 134 | # Like Black, use double quotes for strings. 135 | quote-style = "double" 136 | 137 | # Like Black, indent with spaces, rather than tabs. 138 | indent-style = "space" 139 | 140 | # Like Black, respect magic trailing commas. 141 | skip-magic-trailing-comma = false 142 | 143 | # Like Black, automatically detect the appropriate line ending. 144 | line-ending = "auto" 145 | 146 | # Enable auto-formatting of code examples in docstrings. Markdown, 147 | # reStructuredText code/literal blocks and doctests are all supported. 148 | # 149 | # This is currently disabled by default, but it is planned for this 150 | # to be opt-out in the future. 151 | docstring-code-format = false 152 | 153 | # Set the line length limit used when formatting code snippets in 154 | # docstrings. 155 | # 156 | # This only has an effect when the `docstring-code-format` setting is 157 | # enabled. 158 | docstring-code-line-length = "dynamic" 159 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | altair==5.4.1 2 | attrs==24.2.0 3 | blinker==1.8.2 4 | cachetools==5.5.0 5 | certifi==2024.8.30 6 | charset-normalizer==3.3.2 7 | click==8.1.7 8 | colorama==0.4.6 9 | contourpy==1.3.0 10 | cycler==0.12.1 11 | fastapi==0.115.2 12 | fonttools==4.54.1 13 | GitPython==3.1.43 14 | gitdb==4.0.11 15 | idna==3.10 16 | imageio==2.35.1 17 | Jinja2==3.1.4 18 | jsonschema==4.23.0 19 | jsonschema-specifications==2023.12.1 20 | kiwisolver==1.4.7 21 | lazy_loader==0.4 22 | markdown-it-py==3.0.0 23 | MarkupSafe==2.1.5 24 | matplotlib==3.9.2 25 | mdurl==0.1.2 26 | narwhals==1.9.1 27 | networkx==3.3 28 | numpy==2.1.2 29 | packaging==24.1 30 | pandas==2.2.3 31 | pillow==10.4.0 32 | pre-commit==4.0.1 33 | protobuf==5.28.2 34 | pyarrow==17.0.0 35 | pydeck==0.9.1 36 | Pygments==2.18.0 37 | python-dateutil==2.9.0.post0 38 | python-multipart==0.0.12 39 | pytz==2024.2 40 | referencing==0.35.1 41 | requests==2.32.3 42 | rich==13.9.2 43 | rpds-py==0.20.0 44 | ruff==0.6.9 45 | scikit-image==0.24.0 46 | scipy==1.14.1 47 | six==1.16.0 48 | smmap==5.0.1 49 | streamlit==1.39.0 50 | tenacity==9.0.0 51 | tifffile==2024.9.20 52 | toml==0.10.2 53 | torch==2.0.0 54 | tornado==6.4.1 55 | transformers==4.37.0 56 | typing_extensions==4.12.2 57 | tzdata==2024.2 58 | urllib3==2.2.3 59 | uvicorn==0.32.0 60 | watchdog==5.0.3 61 | -------------------------------------------------------------------------------- /texture-art-source/README.md: -------------------------------------------------------------------------------- 1 | - All textures are from [Pexels](https://www.pexels.com/) 2 | - License: https://www.pexels.com/license/ 3 | -------------------------------------------------------------------------------- /texture-art-source/p1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-to-mastery/ascii-art/f0ca8de3e0c9c841182682bd9c41a7593414e1a2/texture-art-source/p1.jpg -------------------------------------------------------------------------------- /texture-art-source/p2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-to-mastery/ascii-art/f0ca8de3e0c9c841182682bd9c41a7593414e1a2/texture-art-source/p2.jpg -------------------------------------------------------------------------------- /texture-art-source/p3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-to-mastery/ascii-art/f0ca8de3e0c9c841182682bd9c41a7593414e1a2/texture-art-source/p3.jpg -------------------------------------------------------------------------------- /texture-art-source/p4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-to-mastery/ascii-art/f0ca8de3e0c9c841182682bd9c41a7593414e1a2/texture-art-source/p4.jpg -------------------------------------------------------------------------------- /texture-art-source/p5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-to-mastery/ascii-art/f0ca8de3e0c9c841182682bd9c41a7593414e1a2/texture-art-source/p5.jpg -------------------------------------------------------------------------------- /texture-art.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import io 3 | import os 4 | import random 5 | 6 | import matplotlib.pyplot as plt 7 | from PIL import Image 8 | 9 | 10 | def get_image_from_bytes(byte_contents: bytes) -> Image: 11 | """_summary_ 12 | 13 | Args: 14 | byte_contents (bytes): Input image bytes. 15 | 16 | Returns: 17 | Image: Output image. 18 | """ 19 | return Image.open(io.BytesIO(byte_contents)).convert("RGBA") 20 | 21 | 22 | def get_image_from_path(image_path: str) -> Image: 23 | """_summary_ 24 | 25 | Args: 26 | image_path (str): Input image path. 27 | 28 | Returns: 29 | Image: Output image. 30 | """ 31 | return Image.open(image_path).convert("RGBA") 32 | 33 | 34 | def get_random_texture_path(folder_path: str) -> str: 35 | """_summary_ 36 | 37 | Args: 38 | folder_path (str): The folder where to find images(only jpg) 39 | 40 | Returns: 41 | str: Full path of a selected image 42 | """ 43 | # get all .jpg files in the folder 44 | jpg_files = [f for f in os.listdir(folder_path) if f.endswith(".jpg")] 45 | 46 | if not jpg_files: 47 | return None 48 | 49 | # randomly select a .jpg file 50 | file = random.choice(jpg_files) 51 | 52 | # return full path 53 | return os.path.join(folder_path, file) 54 | 55 | 56 | def apply_texture_with_fit_cover(input_image: Image, texture_image: Image, alpha_threshold=128) -> Image: 57 | """_summary_ 58 | 59 | Args: 60 | input_image (Image): Input image 61 | texture_image (Image): Texture image 62 | alpha_threshold (int, optional): The alpha value threshold (0-255). 63 | Only areas with an alpha value higher than this threshold 64 | will receive the texture. Default is 128. 65 | - 0 means fully transparent areas are affected. 66 | - 255 means only fully opaque areas are affected 67 | - Defaults to 128. 68 | 69 | Returns: 70 | Image: Output image. 71 | """ 72 | # get width and height of the input and texture images 73 | input_width, input_height = input_image.size 74 | texture_width, texture_height = texture_image.size 75 | 76 | # calculate the scaling factor to ensure the texture covers the input image (object-fit: cover effect) 77 | scale = max(input_width / texture_width, input_height / texture_height) 78 | new_texture_size = (int(texture_width * scale), int(texture_height * scale)) 79 | 80 | # resize the texture image, maintaining the aspect ratio to cover the input image 81 | texture_img_resized = texture_image.resize(new_texture_size, Image.Resampling.LANCZOS) 82 | 83 | # calculate the area to crop from the texture to center it 84 | crop_x = (new_texture_size[0] - input_width) // 2 85 | crop_y = (new_texture_size[1] - input_height) // 2 86 | texture_img_cropped = texture_img_resized.crop((crop_x, crop_y, crop_x + input_width, crop_y + input_height)) 87 | 88 | # extract the alpha channel from the input image 89 | input_alpha = input_image.split()[3] 90 | 91 | # create an empty image 92 | result_img = Image.new("RGBA", (input_width, input_height), (0, 0, 0, 0)) 93 | 94 | # iterate over each pixel, applying the texture based on the alpha channel 95 | for y in range(input_height): 96 | for x in range(input_width): 97 | # get current pixel's alpha value 98 | alpha_value = input_alpha.getpixel((x, y)) 99 | 100 | # apply the texture if the alpha value exceeds the threshold 101 | if alpha_value > alpha_threshold: 102 | # use the pixel from the texture image 103 | texture_pixel = texture_img_cropped.getpixel((x, y)) 104 | result_img.putpixel((x, y), texture_pixel) 105 | else: 106 | # retain the original transparency or make it fully transparent 107 | result_img.putpixel((x, y), (0, 0, 0, 0)) 108 | return result_img 109 | 110 | 111 | if __name__ == "__main__": 112 | parser = argparse.ArgumentParser(description="Apply a texture image to an input image.") 113 | 114 | parser.add_argument("--input_image", "-i", type=str, help="Path to the input image.") 115 | parser.add_argument("--texture_image", "-t", type=str, help="Path to the texture image.", default=None) 116 | parser.add_argument("--output", "-o", type=str, help="Path to save the output image. (default value: None, and it will 'Show')", default=None) 117 | # parser.add_argument("--alpha_threshold", "-a", type=int, help="The alpha value threshold (0-255)", default=128) 118 | 119 | args = parser.parse_args() 120 | 121 | # check 'input_image' argument 122 | if not args.input_image: 123 | import sys 124 | 125 | print("No input file provided!") 126 | sys.exit(1) 127 | 128 | original_img = get_image_from_path(args.input_image) 129 | 130 | # check 'texture_image' argument 131 | # if texture_image is None, select a random texture from somewhere 132 | if args.texture_image is None: 133 | random_texture_path = get_random_texture_path("texture-art-source") 134 | texture_img = get_image_from_path(random_texture_path) 135 | else: 136 | texture_img = get_image_from_path(args.texture_image) 137 | 138 | result_img = apply_texture_with_fit_cover(original_img, texture_img) 139 | 140 | if args.output: 141 | result_img.save(args.output, "PNG") 142 | else: 143 | plt.imshow(result_img) 144 | plt.axis("off") 145 | plt.show() 146 | 147 | # Example: 148 | # 1. python3 texture-art.py -i example/ztm-logo.png 149 | # 2. python3 texture-art.py -i example/ztm-logo.png -t your/own/texture/path 150 | # 3. python3 texture-art.py -i example/ztm-logo.png -o texture-art.png 151 | -------------------------------------------------------------------------------- /ztm-icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-to-mastery/ascii-art/f0ca8de3e0c9c841182682bd9c41a7593414e1a2/ztm-icon.ico -------------------------------------------------------------------------------- /ztm_ascii.txt: -------------------------------------------------------------------------------- 1 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@;+++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 2 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+++++*****@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 3 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@++++++++*++****+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 4 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+++++++++++****+++****@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 5 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@++++++++++++++*******+++****@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 6 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*+++++++++++++++++***********++***+*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 7 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@*++++++++++++++++++++**************++*****@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 8 | @@@@@@@@@@@@@@@@@@@@@@@@@@++++++++++++++++++++++++*****************++**++*@@@@@@@@@@@@@@@@@@@@@@@@@@ 9 | @@@@@@@@@@@@@@@@@@@@@@@+++++++++++++++++++++++++++++******************+++****@@@@@@@@@@@@@@@@@@@@@@@ 10 | @@@@@@@@@@@@@@@@@@@@++++++++++++++++++++++++++++++***++******************+++***+@@@@@@@@@@@@@@@@@@@@ 11 | @@@@@@@@@@@@@@@@?+++++++++++++++++++++++++++++++@@@@++**+++*****************+++**+*?@@@@@@@@@@@@@@@@ 12 | @@@@@@@@@@@@@++++++++++++++++++++++++++++++++@@@@@@@@@@+***+++******************++***++@@@@@@@@@@@@@ 13 | @@@@@@@@@@++++++++++++++++++++++++++++++++@@@@@@@@@@@@@@@@****+++******************++*****@@@@@@@@@@ 14 | @@@@@@@++++++++++++++++++++++++++++++++@@@@@@@@@@@@@@@@@@@@@@****+++******************+++***+@@@@@@@ 15 | @@@@++++++++++++++++++++++++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@+****++******************+++***+@@@@ 16 | @+++++++++++++++++++++++++++++++*@@@@@@@@@@@@@@@?%%%@@@@@@@@@@@@@@@*+***++******************+++****@ 17 | @++++++++++++++++++++++++++++@@@@@@@@@@@@@@@@%%%%%%%%%%@@@@@@@@@@@@@@@;****++******************++++@ 18 | @+++++++++++++++++++++++++@@@@@@@@@@@@@@@@%??%???%%%%%%%%%@@@@@@@@@@@@@@@@****+++**********++++++++@ 19 | @++++++++++++++++++++++@@@@@@@@@@@@@@@@%?%%???%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@@++**+++****+++++++++++@ 20 | @+++++++++++++++++++@@@@@@@@@@@@@@@??%%%???%%%%%%%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@*****++++++++++++++@ 21 | @+++++++++++++++****@@@@@@@@@@@@????%??%%%%%%%%%%%%%%%%%%%%%%%%%%%%S@@@@@@@@@@@@@++++++++++++++++++@ 22 | @+++++++++++****++****+@@@@@@%%%%%??%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@@@@@@@+++++++++++++++++++++@ 23 | @++++++++**********++***++%%%%???%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%+++++++++++++++++++++++++@ 24 | @+++++***************++**?%???%%%%%%%%%%%%%%%%%%??%%%%%%%%%%%%%%%%%%%%%%?*+++++++++++++++++++++++++@ 25 | @++**++***********+***??%%?%%%%%%%%%%%%%%%%%???%%%%%%%%%%%%%%%%%%%%%%?**++++++++++++++++++++++++++;@ 26 | @@@*****++*****+**???%%%%%%%%%%%%%%%%%%%%???%%?%@@@@%%%%%%%%%%%%%%?*+++++++++++++++++++++++++++++@@@ 27 | @@@@@@;+***++**??%%%%%%%%%%%%%%%%%%%%%%%%%%??@@@@@@@@@@%%%%%%%%?*+++++++++++++++++++++++++++++@@@@@@ 28 | @@@@@@@@@@**??%%%%%%%%%%%%%%%%%%%%%%%%????@@@@@@@@@@@@@@@@%??*+++++++++++++++++++++++++***@@@@@@@@@@ 29 | @@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%??**++*+**@@@@@@@@@@*++++++++++++++++++++++++++++*?%%%%%%%@@@@@@@ 30 | @@@@%%%%???%%%%%%%%%%%%%%%%%%%%???**+****++****+@@@@*++++++++++++++++++++++++++++*?%%%%%%%%%%%%%@@@@ 31 | @%%%%???%%%%%%%%%%%%%%%%%?%%??***+**********+++***+++++++++++++++++++++++++++**?%%%%%%%%%%%%%%%%%%%@ 32 | @???%%%%%%%%%%%%%%%%%%???%?**++****************+++++++++++++++++++++++++++**?%%%%%%%%%%%%%%%%%%%%??@ 33 | @?%%%%%%%%%%%%%%%%%???%?%?+++*+++*****************++++++++++++++++++++++++%%%%%%%%%%%%%%%%%%%%%%%%?@ 34 | @?%%%%%%%%%%%%%%??%%??%@@@@@@****+++**************+++++++++++++++++++++@@@@@@%%%%%%%%%%%%%%%%%%%%%?@ 35 | @?%%%%%%%%%%%%%%%???@@@@@@@@@@@@*****++***********++++++++++++++++++@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%?@ 36 | @?%%%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@+****++********+++++++++++++++@@@@@@@@@@@@@@@@??%%%%%%%%%%%%%%%?@ 37 | @?%%%%%%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@+****++*****++++++++++++@@@@@@@@@@@@@@@%??%%??%%%%%%%%%%%%%%?@ 38 | @?%%%%%%%%%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@+*+**+++*+++++++++@@@@@@@@@@@@@@@?%%%%??%%%%%%%%%%%%%%%%%?@ 39 | @??%%%%%%%%%%%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@@****++++++@@@@@@@@@@@@@@@@?%?%???%%%%%%%%%%%%%%%%%%???@ 40 | @%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@@++++@@@@@@@@@@@@@@@@?%%%???%%%%%%%%%%%%%%%%%%??%%%?@ 41 | @@@S%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%?@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%???%%%%%%%%%%%%%%%%%???%%%??@@@ 42 | @@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%S@@@@@@@@@@@@@@@@@@@@@@%?%%%???%%%%%%%%%%%%%%%%%???%?%?%@@@@@@ 43 | @@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@@?%%%%??%%%%%%%%%%%%%%%%%%???%%%?@@@@@@@@@@ 44 | @@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@@@@@@@@@@%%%%%??%%%%%%%%%%%%%%%%%%???%%%%@@@@@@@@@@@@@ 45 | @@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@@@@??%%???%%%%%%%%%%%%%%%%%%??%%%%%@@@@@@@@@@@@@@@@ 46 | @@@@@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%???%%%%%%%%%%%%%%%%%%??%%%%?@@@@@@@@@@@@@@@@@@@ 47 | @@@@@@@@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%???%%%%%%%%%%%%%%%%%???%%%?%@@@@@@@@@@@@@@@@@@@@@@ 48 | @@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%???%%??@@@@@@@@@@@@@@@@@@@@@@@@@@ 49 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%???%??%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 50 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%??%%%?%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 51 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%??%%%%?@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 52 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%???%%?%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 53 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%%%???%%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 54 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%%%?@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 55 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%?@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ --------------------------------------------------------------------------------