├── docs ├── reference │ ├── pycht.md │ ├── clustering.md │ └── image_processing.md ├── index.md └── getting-started.md ├── misc ├── alys.png ├── cat.jpg ├── pycht_logo.png ├── stencil_1.png ├── stencil_2.png ├── stencil_3.png ├── stencil_4.png ├── stencil_5.png ├── stencil_cat.jpg ├── cheshire_logo.png ├── pycht_logo_pink.png ├── pycht_logo.py └── pycht_logo.svg ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── deploy-docs.yml │ ├── pytest.yml │ └── release.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── pycht ├── __init__.py ├── clustering.py ├── cli.py ├── pycht.py └── image_processing.py ├── mkdocs.yml ├── tests ├── test_clustering.py └── test_image_processing.py ├── CONTRIBUTING.md ├── .pre-commit-config.yaml ├── LICENSE ├── notebook └── pycht.ipynb ├── SECURITY.md ├── pyproject.toml ├── .gitignore ├── CODE_OF_CONDUCT.md └── README.md /docs/reference/pycht.md: -------------------------------------------------------------------------------- 1 | # pycht.py 2 | 3 | ::: pycht.pycht -------------------------------------------------------------------------------- /docs/reference/clustering.md: -------------------------------------------------------------------------------- 1 | # clustering.py 2 | 3 | ::: pycht.clustering -------------------------------------------------------------------------------- /misc/alys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tlentali/pycht/HEAD/misc/alys.png -------------------------------------------------------------------------------- /misc/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tlentali/pycht/HEAD/misc/cat.jpg -------------------------------------------------------------------------------- /misc/pycht_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tlentali/pycht/HEAD/misc/pycht_logo.png -------------------------------------------------------------------------------- /misc/stencil_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tlentali/pycht/HEAD/misc/stencil_1.png -------------------------------------------------------------------------------- /misc/stencil_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tlentali/pycht/HEAD/misc/stencil_2.png -------------------------------------------------------------------------------- /misc/stencil_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tlentali/pycht/HEAD/misc/stencil_3.png -------------------------------------------------------------------------------- /misc/stencil_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tlentali/pycht/HEAD/misc/stencil_4.png -------------------------------------------------------------------------------- /misc/stencil_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tlentali/pycht/HEAD/misc/stencil_5.png -------------------------------------------------------------------------------- /docs/reference/image_processing.md: -------------------------------------------------------------------------------- 1 | # image_processing.py 2 | 3 | ::: pycht.image_processing -------------------------------------------------------------------------------- /misc/stencil_cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tlentali/pycht/HEAD/misc/stencil_cat.jpg -------------------------------------------------------------------------------- /misc/cheshire_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tlentali/pycht/HEAD/misc/cheshire_logo.png -------------------------------------------------------------------------------- /misc/pycht_logo_pink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tlentali/pycht/HEAD/misc/pycht_logo_pink.png -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Describe your changes 2 | 3 | ## Issue ticket number and link 4 | 5 | ## Checklist before requesting a review 6 | 7 | - [ ] I have performed a self-review of my code 8 | - [ ] If it is a core feature, I have added thorough tests. 9 | - [ ] Do we need to implement analytics? 10 | - [ ] Will this be part of a product update? If yes, please write one phrase about this update. -------------------------------------------------------------------------------- /pycht/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Package building. 3 | """ 4 | 5 | from .pycht import Pycht 6 | from .image_processing import ImageProcessing 7 | from .clustering import Clustering 8 | from .cli import compute 9 | 10 | __all__ = ["Pycht", "ImageProcessing", "Clustering", "compute"] 11 | 12 | 13 | def stencil(input_img: str, nb_colors: int = 3, output_path: str = "."): 14 | return Pycht().stencil(input_img, nb_colors, output_path) 15 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Pycht Documentation 2 | use_directory_urls: false 3 | site_url: https://tlentali.github.io/pycht/ 4 | theme: 5 | name: material 6 | 7 | plugins: 8 | - search 9 | - mkdocstrings: 10 | default_handler: python 11 | 12 | nav: 13 | - Home: index.md 14 | - Getting Started: getting-started.md 15 | - Reference: 16 | - Pycht: reference/pycht.md 17 | - Image Processing: reference/image_processing.md 18 | - Clustering: reference/clustering.md 19 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy MkDocs to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: '3.11' 23 | 24 | - name: Install dependencies 25 | run: | 26 | pip install mkdocs mkdocs-material mkdocstrings[python] 27 | 28 | - name: Deploy to GitHub Pages 29 | run: | 30 | mkdocs gh-deploy --force 31 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | name: Run unit tests 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repo 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: '3.12' 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install pytest 27 | pip install -e . 28 | 29 | - name: Run pytest 30 | run: pytest -v 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /tests/test_clustering.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from pycht.clustering import Clustering 3 | 4 | 5 | def test_clustering_output_shape_and_values(): 6 | # Generate synthetic pixel data with 3 distinct color zones 7 | data = np.vstack( 8 | [ 9 | np.full((10, 3), [255, 0, 0]), # Red 10 | np.full((10, 3), [0, 255, 0]), # Green 11 | np.full((10, 3), [0, 0, 255]), # Blue 12 | ] 13 | ).astype(np.float32) 14 | 15 | clustered = Clustering.compute(data, nb_clusters=3) 16 | 17 | assert clustered.shape == data.shape 18 | unique_colors = np.unique(clustered, axis=0) 19 | assert len(unique_colors) == 3 # Should find 3 distinct clusters 20 | for color in unique_colors: 21 | assert color.dtype == np.uint8 22 | assert color.shape == (3,) # RGB triplet 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a [code of conduct](https://github.com/tlentali/pycht/blob/master/CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.12 3 | 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v2.3.0 7 | hooks: 8 | - id: check-yaml 9 | - id: end-of-file-fixer 10 | - id: trailing-whitespace 11 | - repo: local 12 | hooks: 13 | - id: isort 14 | name: isort 15 | entry: uv 16 | args: ["run", "isort", "--filter-files"] 17 | language: system 18 | types: [python] 19 | require_serial: true 20 | - repo: local 21 | hooks: 22 | - id: black 23 | name: black 24 | entry: uv 25 | args: ["run", "black", "--config", "./pyproject.toml"] 26 | language: system 27 | types: [python] 28 | require_serial: true 29 | - repo: local 30 | hooks: 31 | - id: pylint 32 | name: pylint 33 | entry: uv 34 | args: ["run", "pylint", "pycht", "tests"] 35 | language: system 36 | types: [python] 37 | require_serial: true 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Thomas Lentali 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /misc/pycht_logo.py: -------------------------------------------------------------------------------- 1 | import svgwrite 2 | import base64 3 | 4 | def embed_font_base64(font_path): 5 | with open(font_path, 'rb') as f: 6 | encoded = base64.b64encode(f.read()).decode('utf-8') 7 | return f""" 8 | @font-face {{ 9 | font-family: 'Cloister Black'; 10 | src: url(data:font/truetype;charset=utf-8;base64,{encoded}) format('truetype'); 11 | }} 12 | """ 13 | 14 | def generate_svg_with_embedded_font(word, font_path, font_size=100, fill_color='black', output_file='output.svg'): 15 | dwg = svgwrite.Drawing(output_file, profile='full', size=('275px', '170px')) 16 | 17 | font_css = embed_font_base64(font_path) 18 | dwg.defs.add(dwg.style(font_css)) 19 | 20 | dwg.add(dwg.text( 21 | word, 22 | insert=(5, font_size), 23 | fill=fill_color, 24 | font_family='Cloister Black', 25 | font_size=font_size 26 | )) 27 | 28 | dwg.save() 29 | print(f"SVG with embedded font saved as {output_file}") 30 | 31 | generate_svg_with_embedded_font( 32 | word='Pycht', 33 | font_path='../../../Downloads/cloister-black-font/CloisterBlackLight-axjg.ttf', 34 | font_size=120, 35 | fill_color='#f60386', 36 | output_file='pycht_logo.svg' 37 | ) 38 | -------------------------------------------------------------------------------- /pycht/clustering.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for performing color clustering on images using K-Means. 3 | """ 4 | 5 | import numpy as np 6 | from sklearn.cluster import KMeans 7 | 8 | 9 | class Clustering: 10 | """ 11 | Perform K-Means clustering on image data to group similar colors. 12 | """ 13 | 14 | @staticmethod 15 | def compute(pixel_array: np.ndarray, nb_clusters: int, random_state: int = 0) -> np.ndarray: 16 | """ 17 | Apply K-Means clustering to the given data and return the clustered result. 18 | 19 | Parameters 20 | ---------- 21 | pixel_array : np.ndarray 22 | Flattened image data (pixels), shape (num_pixels, num_channels). 23 | nb_clusters : int 24 | The number of color clusters to form. 25 | random_state : int 26 | Random seed for reproducibility. 27 | 28 | Returns 29 | ------- 30 | np.ndarray 31 | The clustered image data where each pixel is replaced by the centroid of its cluster, 32 | with dtype uint8 and the same shape as pixel_array. 33 | """ 34 | kmeans = KMeans(n_clusters=nb_clusters, n_init=10, random_state=random_state) 35 | labels = kmeans.fit_predict(pixel_array) 36 | centers = np.uint8(kmeans.cluster_centers_) 37 | return centers[labels] 38 | -------------------------------------------------------------------------------- /tests/test_image_processing.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from PIL import Image 3 | import pytest 4 | from pycht.image_processing import ImageProcessing 5 | from pathlib import Path 6 | 7 | 8 | @pytest.fixture(scope="module") 9 | def sample_image(tmp_path_factory): 10 | """Create a 4x4 RGB image with 2 color clusters using Pillow.""" 11 | img = np.zeros((4, 4, 3), dtype=np.uint8) 12 | img[:2, :] = [255, 0, 0] # Red 13 | img[2:, :] = [0, 255, 0] # Green 14 | 15 | img_dir = tmp_path_factory.mktemp("data") 16 | img_path = img_dir / "input.png" 17 | Image.fromarray(img).save(img_path) 18 | return img_path 19 | 20 | 21 | def test_color_separation_creates_stencils(sample_image: Path, tmp_path: Path): 22 | processor = ImageProcessing() 23 | 24 | # Simule la sortie d'un clustering KMeans 25 | img = Image.open(sample_image).convert("RGB") 26 | reshaped = np.array(img).reshape((-1, 3)) 27 | 28 | processor.color_separation(reshaped, sample_image, tmp_path) 29 | 30 | stencil_files = list(tmp_path.glob("stencil_*.png")) 31 | assert len(stencil_files) >= 2 # At least one stencil + final 32 | 33 | for f in stencil_files: 34 | with Image.open(f) as result: 35 | if f.name == "stencil_final.png": 36 | assert result.mode == "RGB" 37 | else: 38 | assert result.mode == "RGBA" 39 | -------------------------------------------------------------------------------- /pycht/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main project settings and execution logic. 3 | """ 4 | 5 | import typer 6 | from typing_extensions import Annotated 7 | 8 | from .pycht import Pycht 9 | 10 | app = typer.Typer(help="Main CLI for Pycht.") 11 | 12 | 13 | @app.command() 14 | def compute( 15 | input_img: Annotated[str, typer.Argument(help="The input image")], 16 | nb_colors: Annotated[int, typer.Option("--nb-colors", "-n", help="Number of color clusters")] = 3, 17 | output_path: Annotated[str, typer.Option("--output-path", "-o", help="The output folder")] = "./", 18 | ): 19 | """Stencil your picture with an input image 🎨 !""" 20 | typer.echo(f"Processing {input_img} into {output_path} with {nb_colors} levels.") 21 | Pycht().stencil(input_img, nb_colors, output_path) 22 | 23 | 24 | # @app.command() 25 | # def compute( 26 | # input_img: Annotated[str, typer.Argument(help="The input image")], 27 | # nb_colors: Annotated[int, typer.Option("--nb-colors", "-n", help="Number of color clusters")] = 3, 28 | # output_path: Annotated[str, typer.Option("--output-path", "-o", help="The output folder")] = "./", 29 | # ): 30 | # """Stencil your picture with an input image 🎨 !""" 31 | # typer.echo(f"Processing {input_img} into {output_path} with {nb_colors} levels.") 32 | # Pycht().stencil(input_img, nb_colors, output_path) 33 | 34 | 35 | if __name__ == "__main__": 36 | app() 37 | -------------------------------------------------------------------------------- /notebook/pycht.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "858d2a4f-de41-4029-a9e8-21ec4fd32fc4", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import sys\n", 11 | "\n", 12 | "sys.path.append(\"..\")" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 2, 18 | "id": "05d69918", 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "import pycht" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 3, 28 | "id": "3829209b-33d0-4ca9-8ea2-25238a54955f", 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "pycht.stencil('../misc/cat.jpg', 5)" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "id": "5668ab94-3015-4c23-bef6-8bfbcf8e6695", 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [] 42 | } 43 | ], 44 | "metadata": { 45 | "kernelspec": { 46 | "display_name": "Python 3 (ipykernel)", 47 | "language": "python", 48 | "name": "python3" 49 | }, 50 | "language_info": { 51 | "codemirror_mode": { 52 | "name": "ipython", 53 | "version": 3 54 | }, 55 | "file_extension": ".py", 56 | "mimetype": "text/x-python", 57 | "name": "python", 58 | "nbconvert_exporter": "python", 59 | "pygments_lexer": "ipython3", 60 | "version": "3.12.3" 61 | } 62 | }, 63 | "nbformat": 4, 64 | "nbformat_minor": 5 65 | } 66 | -------------------------------------------------------------------------------- /pycht/pycht.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main project logic for generating color-separated stencils from an input image. 3 | """ 4 | 5 | from pathlib import Path 6 | from .image_processing import ImageProcessing 7 | from .clustering import Clustering 8 | 9 | 10 | class Pycht: 11 | """ 12 | Main interface for generating color-separated stencils from an input image. 13 | 14 | This class orchestrates the image processing and clustering steps by 15 | using the `ImageProcessing` and `Clustering` components. 16 | """ 17 | 18 | def __init__(self, image_processor: ImageProcessing = None, clustering_model: Clustering = None) -> None: 19 | self.image_processing = image_processor or ImageProcessing() 20 | self.clustering = clustering_model or Clustering() 21 | 22 | def stencil(self, input_img: str | Path, nb_colors: int = 3, output_path: str | Path = "./") -> None: 23 | """ 24 | Generate color stencils from an input image using K-Means clustering. 25 | 26 | Parameters 27 | ---------- 28 | input_img : Path or str 29 | Path to the input image file. 30 | output_path : Path or str 31 | Directory path to save the stencil images. 32 | nb_colors : int 33 | Number of color clusters to segment the image into. 34 | """ 35 | input_img = Path(input_img) 36 | output_path = Path(output_path) 37 | 38 | flattened_img = self.image_processing.process(input_img) 39 | clustered_img = self.clustering.compute(flattened_img, nb_colors) 40 | self.image_processing.color_separation(clustered_img, input_img, output_path) 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: "3.12" 19 | 20 | - name: Install tools 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install bumpver build twine 24 | 25 | - name: Determine version bump type 26 | id: version 27 | run: | 28 | git fetch --tags 29 | LAST_COMMIT_MESSAGE=$(git log -1 --pretty=%B) 30 | echo "Last commit: $LAST_COMMIT_MESSAGE" 31 | 32 | if [[ "$LAST_COMMIT_MESSAGE" == *#major* ]]; then 33 | echo "bump=major" >> $GITHUB_OUTPUT 34 | elif [[ "$LAST_COMMIT_MESSAGE" == *#minor* ]]; then 35 | echo "bump=minor" >> $GITHUB_OUTPUT 36 | else 37 | echo "bump=patch" >> $GITHUB_OUTPUT 38 | fi 39 | 40 | - name: Bump version 41 | run: | 42 | bumpver update --${{ steps.version.outputs.bump }} 43 | 44 | - name: Commit bumped version 45 | run: | 46 | git config user.name "GitHub Actions" 47 | git config user.email "actions@github.com" 48 | git add . 49 | git commit -m "chore: bump version [skip ci]" 50 | git push 51 | 52 | - name: Build and publish 53 | env: 54 | TWINE_USERNAME: __token__ 55 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 56 | run: | 57 | python -m build 58 | twine upload dist/* 59 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # `Pycht` Open Source Security Policies and Procedures 2 | 3 | This document outlines security procedures and general policies for the `Pycht` Open Source projects as found on [https://github.com/tlentali/pycht](https://github.com/tlentali/pycht). 4 | 5 | * [Reporting a Vulnerability](#reporting-a-vulnerability) 6 | * [Disclosure Policy](#disclosure-policy) 7 | 8 | ## Reporting a Vulnerability 9 | 10 | The `Pycht` team and community take all security vulnerabilities 11 | seriously. Thank you for improving the security of our open source 12 | software. We appreciate your efforts and responsible disclosure and will 13 | make every effort to acknowledge your contributions. 14 | 15 | Report security vulnerabilities by emailing the `Pycht` security team at thomas.lentali@gmail.com 16 | 17 | The lead maintainer will acknowledge your email within 24 hours, and will send a more detailed response within 48 hours indicating the next steps in handling your report. 18 | After the initial reply to your report, the security team will endeavor to keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. 19 | 20 | Report security vulnerabilities in third-party modules to the person or team maintaining the module. 21 | 22 | ## Disclosure Policy 23 | 24 | When the security team receives a security bug report, they will assign it to a primary handler. 25 | This person will coordinate the fix and release process, involving the following steps: 26 | 27 | - Confirm the problem and determine the affected versions. 28 | - Audit code to find any potential similar problems. 29 | - Prepare fixes for all releases still under maintenance. 30 | 31 | These fixes will be released as fast as possible to `Pypi`. -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pycht" 3 | version = "0.1.21" 4 | description = "Street art by clustering." 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "pillow>=11.2.1", 9 | "scikit-learn>=1.6.1", 10 | "typer", 11 | "typing-extensions", 12 | ] 13 | 14 | [project.urls] 15 | Repository = "https://github.com/tlentali/pycht" 16 | Documentation = "https://tlentali.github.io/pycht/" 17 | 18 | [dependency-groups] 19 | dev = [ 20 | "black>=25.1.0", 21 | "jupyter>=1.1.1", 22 | "mkdocs>=1.6.1", 23 | "mkdocs-material>=9.6.9", 24 | "mkdocstrings[python]>=0.29.0", 25 | "pylint>=3.3.6", 26 | "pylint-per-file-ignores>=1.4.0", 27 | "pytest>=8.3.5", 28 | "typer>=0.15.2", 29 | ] 30 | 31 | [build-system] 32 | requires = ["setuptools>=61.0"] 33 | build-backend = "setuptools.build_meta" 34 | 35 | [tool.setuptools.packages.find] 36 | where = ["."] 37 | include = ["pycht"] 38 | exclude = ["tests*", "notebook*", "misc*"] 39 | 40 | [tool.uv] 41 | required-version = ">=0.6.11,<0.7" 42 | 43 | [project.scripts] 44 | pycht = "pycht.cli:app" 45 | 46 | [tool.isort] 47 | profile = "black" 48 | filter_files = true 49 | 50 | [tool.black] 51 | line-length = 120 52 | 53 | [tool.pylint.MASTER] 54 | load-plugins=[ 55 | "pylint_per_file_ignores" 56 | ] 57 | [tool.pylint."messages control"] 58 | max-line-length = 120 59 | disable=["missing-module-docstring", "too-few-public-methods", "R0801"] 60 | extension-pkg-allow-list=["ujson"] 61 | per-file-ignores= ["./tests/:missing-function-docstring"] 62 | 63 | [tool.bumpver] 64 | current_version = "0.1.21" 65 | version_pattern = "MAJOR.MINOR.PATCH" 66 | commit_message = "bump: {old_version} → {new_version}" 67 | tag_message = "v{new_version}" 68 | 69 | [tool.bumpver.file_patterns] 70 | "pyproject.toml" = [ 71 | 'current_version = "{version}"', 72 | 'version = "{version}"' 73 | ] 74 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # 🎨 Pycht 2 | 3 | **Pycht** is a lightweight Python tool that transforms images into colorful street art-style stencils using K-Means clustering. 4 | 5 | It automatically reduces an image’s color palette into distinct clusters and generates transparent PNG layers for each one — ideal for digital or physical stencil creation, creative coding projects, or simply exploring image segmentation. 6 | 7 | --- 8 | 9 | ## ✨ Features 10 | 11 | - 🧠 Simple image clustering using Scikit-Learn’s K-Means algorithm 12 | - 🖼️ Color separation with transparency masks 13 | - 📁 Input/output file handling with minimal setup 14 | - 🧰 Modular architecture for easy extension 15 | 16 | --- 17 | 18 | ## 🚀 How It Works 19 | 20 | 1. **Load** an image. 21 | 2. **Process** the pixels into a 2D format. 22 | 3. **Cluster** colors using K-Means. 23 | 4. **Separate** each color cluster into its own transparent stencil. 24 | 5. **Save** the final image and each stencil layer as a `.png`. 25 | 26 | --- 27 | 28 | ## 📦 Example Usage 29 | 30 | ```python 31 | import pycht 32 | 33 | # Create a stencil with 5 color clusters 34 | pycht.stencil("images/input.jpg", nb_colors=5) 35 | ``` 36 | 37 | This will generate: 38 | - stencil_final.png → final clustered image 39 | - stencil_1.png, stencil_2.png, etc. → one per color cluster, with transparency 40 | 41 | --- 42 | 43 | ## 🧪 Try It Out 44 | 45 | Want to experiment? Just provide any image and see how it gets broken down into layers of color. 46 | 47 | --- 48 | 49 | ## 📚 Documentation 50 | 51 | - [Getting Started](getting-started.md) 52 | - API Reference 53 | 54 | --- 55 | 56 | ## 🛠️ Technologies Used 57 | 58 | - Python 3.12+ 59 | - OpenCV 60 | - NumPy 61 | - MkDocs (for this documentation!) 62 | 63 | --- 64 | 65 | ## 🙌 Contributing 66 | 67 | Pull requests are welcome! Feel free to open an issue or suggest improvements. 68 | 69 | --- 70 | 71 | ## 📄 License 72 | 73 | MIT License © Thomas Lentali -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # 🚀 Getting Started 2 | 3 | Welcome to **Pycht** – a tool for transforming images into colorful stencil layers using K-Means clustering. This guide will help you install, use, and customize Pycht step-by-step. 4 | 5 | --- 6 | 7 | ## 📦 Installation 8 | 9 | Clone the repository and install dependencies: 10 | 11 | ```bash 12 | git clone https://github.com/tlentali/pycht.git 13 | cd pycht 14 | pip install -r requirements.txt 15 | ``` 16 | 17 | Alternatively, if you're using a virtual environment: 18 | 19 | ```bash 20 | python -m venv venv 21 | source venv/bin/activate # On Windows: venv\Scripts\activate 22 | pip install -r requirements.txt 23 | ``` 24 | 25 | We suggest you to use [uv](https://docs.astral.sh/uv/): 26 | 27 | ```bash 28 | uv venv --python ~/.pyenv/versions/3.12.2/bin/python 29 | source .venv/bin/activate 30 | uv sync --inexact 31 | ``` 32 | 33 | --- 34 | 35 | ## 🖼️ Usage Example 36 | 37 | Here’s how to process an image and create stencils: 38 | 39 | ```python 40 | import Pycht 41 | 42 | pycht.stencil("images/input.jpg", nb_colors=4, output_path="output/") 43 | ``` 44 | 45 | This will: 46 | - Load `input.jpg` 47 | - Cluster its colors into 4 dominant tones 48 | - Save: 49 | - `output/`: the final image with clustered colors 50 | - `stencil_1.png`, `stencil_2.png`, ...: one per color, with transparency 51 | 52 | --- 53 | 54 | ## ⚙️ Parameters 55 | 56 | - `input_img` (str): Path to the original image. 57 | - `output_path` (str): Path to save the clustered version of the image. 58 | - `nb_colors` (int): Number of color clusters (stencils) to generate. 59 | 60 | --- 61 | 62 | ## 📁 Project Structure 63 | 64 | ``` 65 | pycht/ 66 | │ 67 | ├── clustering.py # Handles K-Means color clustering 68 | ├── image_processing.py # Image reading, reshaping, saving, display 69 | ├── pycht.py # Main class combining everything 70 | ├── images/ # Your input/output folder (create it) 71 | └── ... 72 | ``` 73 | 74 | --- 75 | 76 | ## 🧪 Try It With Your Own Image 77 | 78 | Put any `.jpg` or `.png` in the `images/` folder and run the script. 79 | The tool will output one clustered image and one stencil per color. 80 | 81 | --- 82 | 83 | ## 🛠️ Customization Ideas 84 | 85 | - Try different numbers of `nb_colors` to control stencil complexity. 86 | - Preprocess the image (e.g., resize or blur) before clustering. 87 | - Adjust the clustering criteria in `clustering.py` if needed. 88 | - Change output paths to organize your stencil layers better. 89 | 90 | --- 91 | 92 | ## 📚 Next Steps 93 | 94 | - [Browse the API Reference](reference/pycht.md) 95 | - [View example images](https://github.com/tlentali/pycht/tree/main/examples) *(if available)* 96 | 97 | --- 98 | 99 | ## ❓ Need Help? 100 | 101 | Feel free to [open an issue](https://github.com/tlentali/pycht/issues) on GitHub if you run into trouble. 102 | 103 | Happy stenciling! 🎨 104 | -------------------------------------------------------------------------------- /pycht/image_processing.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | from PIL import Image 3 | import numpy as np 4 | from pathlib import Path 5 | 6 | 7 | class ImageProcessing: 8 | """ 9 | A collection of image processing methods for loading, transforming, 10 | and segmenting colors within an image using Pillow instead of OpenCV. 11 | """ 12 | 13 | def process(self, input_path: Path) -> np.ndarray: 14 | """ 15 | Load an image from disk and flatten it into a 2D array of float32 pixels. 16 | """ 17 | if not input_path.exists(): 18 | raise FileNotFoundError(f"Image not found at: {input_path}") 19 | img = Image.open(input_path).convert("RGB") 20 | return np.float32(np.array(img).reshape((-1, 3))) 21 | 22 | @staticmethod 23 | def write_image(image: np.ndarray, output_path: Path) -> None: 24 | """Write image to a file.""" 25 | output_path.parent.mkdir(parents=True, exist_ok=True) 26 | img = Image.fromarray(image) 27 | img.save(output_path) 28 | 29 | @staticmethod 30 | def to_bgra_with_alpha(image: np.ndarray, alpha_mask: np.ndarray) -> np.ndarray: 31 | """ 32 | Convert an RGB image to RGBA using a binary alpha mask. 33 | """ 34 | h, w, _ = image.shape 35 | rgba = np.zeros((h, w, 4), dtype=np.uint8) 36 | rgba[:, :, :3] = image 37 | rgba[:, :, 3] = alpha_mask 38 | return rgba 39 | 40 | def color_separation( 41 | self, 42 | clustered_pixels: np.ndarray, 43 | input_path: Path, 44 | output_dir: Path, 45 | background_color: Tuple[int, int, int] = (0, 0, 0), 46 | ) -> None: 47 | """ 48 | Generate and save separate stencil images for each color cluster in the input image. 49 | """ 50 | clustered_image, shape = self._load_and_prepare(input_path, clustered_pixels) 51 | unique_colors = np.unique(clustered_pixels, axis=0) 52 | 53 | for i, color in enumerate(unique_colors, start=1): 54 | stencil_bgra = self._create_stencil_image(clustered_image, color, background_color) 55 | self.write_image(stencil_bgra, output_dir / f"stencil_{i}.png") 56 | 57 | self.write_image(clustered_image, output_dir / "stencil_final.png") 58 | 59 | def _load_and_prepare(self, input_path: Path, clustered_pixels: np.ndarray) -> Tuple[np.ndarray, Tuple[int, int]]: 60 | """ 61 | Load the original image to extract its shape and reshape the clustered pixel data accordingly. 62 | """ 63 | if not input_path.exists(): 64 | raise FileNotFoundError(f"Image not found at: {input_path}") 65 | img = Image.open(input_path).convert("RGB") 66 | w, h = img.size 67 | clustered_image = clustered_pixels.reshape((h, w, 3)).astype(np.uint8) 68 | return clustered_image, (h, w) 69 | 70 | def _create_stencil_image( 71 | self, clustered_image: np.ndarray, color: np.ndarray, background_color: Tuple[int, int, int] 72 | ) -> np.ndarray: 73 | """ 74 | Create a RGBA stencil image where only the selected color cluster is visible and the rest is transparent. 75 | """ 76 | mask = np.all(clustered_image == color, axis=2) 77 | stencil = np.full_like(clustered_image, background_color, dtype=np.uint8) 78 | stencil[mask] = color 79 | alpha = (mask * 255).astype(np.uint8) 80 | return self.to_bgra_with_alpha(stencil, alpha) 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | # Avoid poetry.lock 163 | poetry.lock 164 | 165 | # No track of png or jpeg files 166 | *.png 167 | *.jpg 168 | 169 | # UV 170 | *.lock 171 | .python-version 172 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | thomas.lentali@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
8 |
9 |
10 |
13 | Street art by clustering. 14 |
15 | 16 | 19 | 20 |21 | Pics by @alys.cheshire 22 |
23 | 24 | 25 | ## ⚡️ Quick start 26 | 27 | Take a nice picture : 28 | 31 | 32 | Generate a 5 colors stencil model : 33 | ```python 34 | >>> import pycht 35 | 36 | >>> pycht.stencil('cat.jpg', 5) 37 | ``` 38 | 39 | | Stencil 1 | stencil 2 | stencil 3 | stencil 4 | stencil 5 | 40 | | :-----------------------------------------------------------------------------: | :-----------------------------------------------------------------------------: | :-----------------------------------------------------------------------------: | :-----------------------------------------------------------------------------: | :-----------------------------------------------------------------------------: | 41 | |  |  |  |  |  | 42 | 43 | 44 | Final result rendering with all stencils : 45 | 46 | 49 | 50 | Cut it, paint it, stare at it. 51 | Enjoy ! 52 | 53 | ## 📚 Documentation 54 | 55 | The full documentation for this project is available at: 56 | 57 | 👉 [https://tlentali.github.io/pycht/](https://tlentali.github.io/pycht/) 58 | 59 | It includes [installation instructions](https://tlentali.github.io/pycht/getting-started.html), usage [examples](https://tlentali.github.io/pycht/getting-started.html), and the [API](https://tlentali.github.io/pycht/reference/pycht.html) reference. 60 | 61 | ## 🛠 Installation 62 | 63 | 🐍 You need to install **Python 3.12** or above. 64 | 65 | Installation can be done by using `pip`. 66 | There are [wheels available](https://pypi.org/project/pycht/#files) for **Linux**, **MacOS**, and **Windows**. 67 | 68 | ```bash 69 | pip install pycht 70 | ``` 71 | 72 | You can also install the latest development version as so: 73 | 74 | ```bash 75 | pip install git+https://github.com/tlentali/pycht 76 | 77 | # Or, through SSH: 78 | pip install git+ssh://git@github.com/tlentali/pycht.git 79 | ``` 80 | 81 | ## 🥄 How Does It Work? 82 | 83 | Imagine `pycht` as your personal digital street artist. Here's what happens under the hood, step-by-step: 84 | 85 | 1. **🖼️ Image loading** 86 | `pycht` grabs your input image and flattens it like a pancake — every pixel becomes a 3-value row (B, G, R) in a giant NumPy array. Think of it as turning your photo into a spreadsheet of colors. 87 | 88 | 2. **🎯 K-Means clustering** 89 | Then comes the science. Using Scikit-Learn’s `kmeans`, we ask: *“Hey, what are the `N` most dominant colors in this image?”* 90 | The algorithm groups similar pixels into `nb_colors` clusters and assigns each one a centroid — like reducing a rainbow into just a few paint buckets. 91 | 92 | 3. **🎨 Color mapping** 93 | Every pixel in your image is replaced by its cluster's centroid. Boom — you've got a stylized version of your image with just `N` bold, poster-style colors. 94 | 95 | 4. **🔍 Color separation** 96 | Now the magic: for each color, `pycht` creates a mask. All pixels that **don’t** belong to the current color cluster are set to black (and later transparent). 97 | Each color gets its own PNG file — like cutting stencils for spray-painting layers IRL. 98 | 99 | 5. **📁 File drop** 100 | Your output includes: 101 | - `output.png` → The clustered image 102 | - `stencil_1.png`, `stencil_2.png`, ... → Transparent layers, one per color 103 | 104 | > It's like building silkscreen layers, but with Python, pixels, and zero mess. 105 | 106 | Ready to turn your cat photo into street art? Let `pycht` paint it. 107 | 108 | ## 🧑💻 Development 109 | 110 | You can use `pip` or [uv](https://docs.astral.sh/uv/). From the `pycht` root folder, do: 111 | 112 | * `uv venv --python /path/to/3.12.x/python`. *Tips*: you can use [pyenv](https://github.com/pyenv/pyenv) to manage and 113 | install multiple Python versions. You can find a specific version at `~/.pyenv/versions/3.12.2/bin/python` for 114 | instance. 115 | * `source .venv/bin/activate` to activate the virtualenv `.venv` created by `uv` 116 | * `uv sync --inexact` to install all dependencies 117 | * `pre-commit install` (just one time). The pre-commit hook will run black, isort and pylint before your commit :) 118 | 119 | You're ready to hack! 120 | 121 | ## 🧰 Command-Line Interface (CLI) 122 | 123 | You can use `pycht` as a command-line tool to generate stencil layers from an image — perfect for street art, posters, or digital illustration. 124 | 125 | ### 🖥️ Installation 126 | 127 | Install in editable mode (dev mode) with [uv](https://github.com/astral-sh/uv) or pip: 128 | 129 | ```bash 130 | uv pip install -e . 131 | ``` 132 | 133 | Make sure you have the required dependencies listed in `pyproject.toml`. 134 | 135 | ### 🚀 Usage 136 | 137 | ```bash 138 | pycht