├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── examples ├── edges.png ├── file.png ├── image.jpg ├── intervals.png ├── mask.png ├── masked.png ├── none.png ├── random.png └── waves.png ├── pixelsort ├── __init__.py ├── __main__.py ├── argparams.py ├── constants.py ├── interval.py ├── main.py ├── sorter.py ├── sorting.py └── util.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | #Python 2 | *.pyc 3 | 4 | #VirtualEnv 5 | venv 6 | 7 | #PyCharm 8 | .idea 9 | 10 | #VSCode 11 | .vscode 12 | 13 | #Output images in root (examples are not ignored) 14 | /*.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 satyarth (satyarth.me) 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pixelsort 2 | 3 | ### What is Pixel Sorting? 4 | 5 | Have a look at [this post](http://satyarth.me/articles/pixel-sorting/) or [/r/pixelsorting](http://www.reddit.com/r/pixelsorting/top/) 6 | 7 | ### Dependencies 8 | 9 | Should work in both Python 2 and 3, but Python 3 is recommended. 10 | 11 | ## Usage 12 | 13 | From the command line: 14 | 15 | ``` 16 | pip install pixelsort 17 | python3 -m pixelsort %PathToImage% [options] 18 | ``` 19 | 20 | Tip: To replicate Kim Asendorf's original [processing script](https://github.com/kimasendorf/ASDFPixelSort), first sort vertically and then horizontally in `threshold` (default) mode: 21 | 22 | ``` 23 | python3 -m pixelsort %PathToImage% -a 90 24 | python3 -m pixelsort %PathToSortedImage% 25 | ``` 26 | 27 | As a package: 28 | 29 | ``` 30 | >>> from pixelsort import pixelsort 31 | >>> from PIL import Image 32 | >>> a = Image.open("examples/image.jpg") 33 | >>> a 34 | 35 | >>> pixelsort(a) 36 | 37 | ``` 38 | 39 | #### Parameters: 40 | 41 | Parameter | Flag | Description 42 | ------------------------|-------|------------ 43 | Interval function | `-i` | Controls how the intervals used for sorting are defined. See below for more details and examples. Threshold by default. 44 | Output path | `-o` | Path of output file. Uses the current time for the file name by default. 45 | Randomness | `-r` | What percentage of intervals *not* to sort. 0 by default. 46 | Threshold (lower) | `-t` | How dark must a pixel be to be considered as a 'border' for sorting? Takes values from 0-1. 0.25 by default. Used in `edges` and `threshold` modes. 47 | Threshold (upper) | `-u` | How bright must a pixel be to be considered as a 'border' for sorting? Takes values from 0-1. 0.8 by default. Used in `threshold` mode. 48 | Char. length | `-c` | Characteristic length for the random width generator. Used in mode `random` and `waves`. 49 | Angle | `-a` | Angle at which you're pixel sorting in degrees. `0` (horizontal) by default. 50 | External interval file | `-f` | Image used to define intervals. Must be black and white. 51 | Sorting function | `-s` | Sorting function to use for sorting the pixels. Lightness by default. 52 | Mask | `-m` | Image used for masking parts of the image. 53 | Logging level | `-l` | Level of logging statements made visible. Choices include `DEBUG`, `INFO`, `WARNING`, `ERROR`, and `CRITICAL`. `WARNING` by default. 54 | 55 | #### Interval Functions 56 | 57 | Interval function | Description 58 | ------------------|------------ 59 | `random` | Randomly generate intervals. Distribution of widths is linear by default. Interval widths can be scaled using `char_length`. 60 | `edges` | Performs an edge detection, which is used to define intervals. Tweak threshold with `threshold`. 61 | `threshold` | Intervals defined by lightness thresholds; only pixels with a lightness between the upper and lower thresholds are sorted. 62 | `waves` | Intervals are waves of nearly uniform widths. Control width of waves with `char_length`. 63 | `file` | Intervals taken from another specified input image. Must be black and white, and the same size as the input image. 64 | `file-edges` | Intevals defined by performing edge detection on the file specified by `-f`. Must be the same size as the input image. 65 | `none` | Sort whole rows, only stopping at image borders. 66 | 67 | 68 | #### Sorting Functions 69 | 70 | Sorting function | Description 71 | ------------------|------------ 72 | `lightness` | Sort by the lightness of a pixel according to a HSL representation. 73 | `hue` | Sort by the hue of a pixel according to a HSL representation. 74 | `saturation` | Sort by the saturation of a pixel according to a HSL representation. 75 | `intensity` | Sort by the intensity of a pixel, i.e. the sum of all the RGB values. 76 | `minimum` | Sort on the minimum RGB value of a pixel (either the R, G or B). 77 | 78 | #### Examples 79 | 80 | `python3 -m pixelsort examples/image.jpg -i random -c 20` 81 | 82 | ![random](/examples/random.png) 83 | 84 | `python3 -m pixelsort examples/image.jpg -i edges -t .5` 85 | 86 | ![edges](/examples/edges.png) 87 | 88 | * `file`: Intervals taken from image specified with `-f`. Must be black and white. 89 | 90 | `python3 -m pixelsort examples/image.jpg -i file -f examples/intervals.png ` 91 | 92 | ![file](/examples/intervals.png) 93 | 94 | (generated with [elementary-ca](https://github.com/satyarth/elementary-ca)) 95 | 96 | ![file](/examples/file.png) 97 | 98 | * `mask`: Mask taken from image specified with `-m`. Must be black and white. 99 | 100 | `python3 -m pixelsort examples/image.jpg -i random -c 20 -m examples/mask.png` 101 | 102 | ![file](/examples/mask.png) 103 | 104 | ![file](/examples/masked.png) 105 | 106 | ### Todo 107 | 108 | * Allow defining different intervals for different channels. 109 | 110 | --- 111 | 112 | Based on https://gist.github.com/prophetgoddess/667c5554e5d9d9a25ae6 113 | -------------------------------------------------------------------------------- /examples/edges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satyarth/pixelsort/6830930d34963fbdc36009212b32993657fec8de/examples/edges.png -------------------------------------------------------------------------------- /examples/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satyarth/pixelsort/6830930d34963fbdc36009212b32993657fec8de/examples/file.png -------------------------------------------------------------------------------- /examples/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satyarth/pixelsort/6830930d34963fbdc36009212b32993657fec8de/examples/image.jpg -------------------------------------------------------------------------------- /examples/intervals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satyarth/pixelsort/6830930d34963fbdc36009212b32993657fec8de/examples/intervals.png -------------------------------------------------------------------------------- /examples/mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satyarth/pixelsort/6830930d34963fbdc36009212b32993657fec8de/examples/mask.png -------------------------------------------------------------------------------- /examples/masked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satyarth/pixelsort/6830930d34963fbdc36009212b32993657fec8de/examples/masked.png -------------------------------------------------------------------------------- /examples/none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satyarth/pixelsort/6830930d34963fbdc36009212b32993657fec8de/examples/none.png -------------------------------------------------------------------------------- /examples/random.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satyarth/pixelsort/6830930d34963fbdc36009212b32993657fec8de/examples/random.png -------------------------------------------------------------------------------- /examples/waves.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satyarth/pixelsort/6830930d34963fbdc36009212b32993657fec8de/examples/waves.png -------------------------------------------------------------------------------- /pixelsort/__init__.py: -------------------------------------------------------------------------------- 1 | from pixelsort.main import pixelsort 2 | NAME = "pixelsort" 3 | -------------------------------------------------------------------------------- /pixelsort/__main__.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import logging 3 | from pixelsort.argparams import parse_args 4 | from pixelsort.main import pixelsort 5 | from pixelsort.util import id_generator 6 | 7 | args = parse_args() 8 | image_input_path = args.pop("image_input_path") 9 | image_output_path = args.pop("image_output_path") 10 | interval_file_path = args.pop("interval_file_path") 11 | mask_path = args.pop("mask_path") 12 | 13 | if image_output_path is None: 14 | image_output_path = id_generator() + ".png" 15 | logging.warning("No output path provided, using " + image_output_path) 16 | 17 | logging.debug("Opening image...") 18 | args["image"] = Image.open(image_input_path) 19 | if mask_path: 20 | logging.debug("Opening mask...") 21 | args["mask_image"] = Image.open(mask_path) 22 | if interval_file_path: 23 | logging.debug("Opening interval file...") 24 | args["interval_image"] = Image.open(interval_file_path) 25 | 26 | logging.debug("Saving image...") 27 | pixelsort(**args).save(image_output_path) 28 | -------------------------------------------------------------------------------- /pixelsort/argparams.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | 4 | from pixelsort.constants import DEFAULTS 5 | from pixelsort.interval import choices as interval_choices 6 | from pixelsort.sorting import choices as sorting_choices 7 | 8 | 9 | def parse_args(): 10 | parser = argparse.ArgumentParser(description="Pixel mangle an image.") 11 | parser.add_argument("image", help="Input image file path.") 12 | parser.add_argument( 13 | "-o", 14 | "--output", 15 | help="Output image file path, DEFAULTS to the time created.") 16 | parser.add_argument("-i", "--int_function", 17 | choices=interval_choices.keys(), 18 | default=DEFAULTS["interval_function"], 19 | help="Function to determine sorting intervals") 20 | parser.add_argument("-f", "--int_file", 21 | help="Image used for defining intervals.") 22 | parser.add_argument( 23 | "-t", 24 | "--threshold", 25 | type=float, 26 | default=DEFAULTS["lower_threshold"], 27 | help="Pixels darker than this are not sorted, between 0 and 1") 28 | parser.add_argument( 29 | "-u", 30 | "--upper_threshold", 31 | type=float, 32 | default=DEFAULTS["upper_threshold"], 33 | help="Pixels brighter than this are not sorted, between 0 and 1") 34 | parser.add_argument( 35 | "-c", 36 | "--char_length", 37 | type=int, 38 | default=DEFAULTS["char_length"], 39 | help="Characteristic length of random intervals") 40 | parser.add_argument( 41 | "-a", 42 | "--angle", 43 | type=float, 44 | default=DEFAULTS["angle"], 45 | help="Rotate the image by an angle (in degrees) before sorting") 46 | parser.add_argument( 47 | "-r", 48 | "--randomness", 49 | type=float, 50 | default=DEFAULTS["randomness"], 51 | help="What percentage of intervals are NOT sorted") 52 | parser.add_argument("-s", "--sorting_function", 53 | choices=sorting_choices.keys(), 54 | default=DEFAULTS["sorting_function"], 55 | help="Function to sort pixels by.") 56 | parser.add_argument( 57 | "-m", "--mask", help="Image used for masking parts of the image") 58 | parser.add_argument( 59 | "-l", 60 | "--log_level", 61 | default="WARNING", 62 | help="Print more or less info", 63 | choices=[ 64 | "DEBUG", 65 | "INFO", 66 | "WARNING", 67 | "ERROR", 68 | "CRITICAL"]) 69 | 70 | _args = parser.parse_args() 71 | 72 | logging.basicConfig( 73 | format="%(name)s: %(levelname)s - %(message)s", 74 | level=logging.getLevelName( 75 | _args.log_level)) 76 | 77 | return { 78 | "image_input_path": _args.image, 79 | "image_output_path": _args.output, 80 | "interval_function": _args.int_function, 81 | "interval_file_path": _args.int_file, 82 | "lower_threshold": _args.threshold, 83 | "upper_threshold": _args.upper_threshold, 84 | "char_length": _args.char_length, 85 | "angle": _args.angle, 86 | "randomness": _args.randomness, 87 | "sorting_function": _args.sorting_function, 88 | "mask_path": _args.mask 89 | } 90 | -------------------------------------------------------------------------------- /pixelsort/constants.py: -------------------------------------------------------------------------------- 1 | DEFAULTS = { 2 | "interval_function": "threshold", 3 | "lower_threshold": 0.25, 4 | "upper_threshold": 0.8, 5 | "char_length": 50, 6 | "angle": 0, 7 | "randomness": 0, 8 | "sorting_function": "lightness", 9 | } 10 | -------------------------------------------------------------------------------- /pixelsort/interval.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from random import randint, random as random_range 3 | 4 | from PIL import ImageFilter, Image 5 | 6 | from pixelsort.sorting import lightness 7 | 8 | 9 | def edge(image: Image.Image, lower_threshold: float, **_) -> typing.List[typing.List[int]]: 10 | """Performs an edge detection, which is used to define intervals. Tweak threshold with threshold.""" 11 | edge_data = image.filter(ImageFilter.FIND_EDGES).convert('RGBA').load() 12 | intervals = [] 13 | 14 | for y in range(image.size[1]): 15 | intervals.append([]) 16 | flag = True 17 | for x in range(image.size[0]): 18 | if lightness(edge_data[x, y]) < lower_threshold * 255: 19 | flag = True 20 | elif flag: 21 | intervals[y].append(x) 22 | flag = False 23 | return intervals 24 | 25 | 26 | def threshold(image: Image.Image, lower_threshold: float, upper_threshold: float, **_) -> typing.List[typing.List[int]]: 27 | """Intervals defined by lightness thresholds; only pixels with a lightness between the upper and lower thresholds 28 | are sorted.""" 29 | intervals = [] 30 | image_data = image.load() 31 | for y in range(image.size[1]): 32 | intervals.append([]) 33 | for x in range(image.size[0]): 34 | level = lightness(image_data[x, y]) 35 | if level < lower_threshold * 255 or level > upper_threshold * 255: 36 | intervals[y].append(x) 37 | return intervals 38 | 39 | 40 | def random(image, char_length, **_) -> typing.List[typing.List[int]]: 41 | """Randomly generate intervals. Distribution of widths is linear by default. Interval widths can be scaled using 42 | char_length.""" 43 | intervals = [] 44 | 45 | for y in range(image.size[1]): 46 | intervals.append([]) 47 | x = 0 48 | while True: 49 | x += int(char_length * random_range()) 50 | if x > image.size[0]: 51 | break 52 | else: 53 | intervals[y].append(x) 54 | return intervals 55 | 56 | 57 | def waves(image, char_length, **_) -> typing.List[typing.List[int]]: 58 | """Intervals are waves of nearly uniform widths. Control width of waves with char_length.""" 59 | intervals = [] 60 | 61 | for y in range(image.size[1]): 62 | intervals.append([]) 63 | x = 0 64 | while True: 65 | x += char_length + randint(0, 10) 66 | if x > image.size[0]: 67 | break 68 | else: 69 | intervals[y].append(x) 70 | return intervals 71 | 72 | 73 | def file_mask(image, interval_image, **_) -> typing.List[typing.List[int]]: 74 | """Intervals taken from another specified input image. Must be black and white, and the same size as the input 75 | image.""" 76 | intervals = [] 77 | data = interval_image.load() 78 | 79 | for y in range(image.size[1]): 80 | intervals.append([]) 81 | flag = True 82 | for x in range(image.size[0]): 83 | if data[x, y]: 84 | flag = True 85 | elif flag: 86 | intervals[y].append(x) 87 | flag = False 88 | return intervals 89 | 90 | 91 | def file_edges(image, interval_image, lower_threshold, **_) -> typing.List[typing.List[int]]: 92 | """Intervals defined by performing edge detection on the file specified by -f. Must be the same size as the input 93 | image.""" 94 | edge_data = interval_image.filter( 95 | ImageFilter.FIND_EDGES).convert('RGBA').load() 96 | intervals = [] 97 | 98 | for y in range(image.size[1]): 99 | intervals.append([]) 100 | flag = True 101 | for x in range(image.size[0]): 102 | if lightness(edge_data[x, y]) < lower_threshold * 255: 103 | flag = True 104 | elif flag: 105 | intervals[y].append(x) 106 | flag = False 107 | return intervals 108 | 109 | 110 | def none(image, **_) -> typing.List[typing.List[int]]: 111 | """Sort whole rows, only stopping at image borders.""" 112 | intervals = [] 113 | for y in range(image.size[1]): 114 | intervals.append([]) 115 | return intervals 116 | 117 | 118 | choices = { 119 | "random": random, 120 | "threshold": threshold, 121 | "edges": edge, 122 | "waves": waves, 123 | "file": file_mask, 124 | "file-edges": file_edges, 125 | "none": none 126 | } 127 | -------------------------------------------------------------------------------- /pixelsort/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import typing 3 | 4 | from PIL import Image 5 | # python implementation of the PixelAccess class returned by im.load(), has the same functions so is fine for type hints 6 | from PIL import PyAccess 7 | 8 | from pixelsort.constants import DEFAULTS 9 | from pixelsort.interval import choices as interval_choices 10 | from pixelsort.sorter import sort_image 11 | from pixelsort.sorting import choices as sorting_choices 12 | from pixelsort.util import crop_to 13 | 14 | 15 | def pixelsort( 16 | image: Image.Image, 17 | mask_image: typing.Optional[Image.Image] = None, 18 | interval_image: typing.Optional[Image.Image] = None, 19 | randomness: float = DEFAULTS["randomness"], 20 | char_length: float = DEFAULTS["char_length"], 21 | sorting_function: typing.Literal["lightness", "hue", "saturation", "intensity", "minimum"] = DEFAULTS[ 22 | "sorting_function"], 23 | interval_function: typing.Literal["random", "threshold", "edges", "waves", "file", "file-edges", "none"] = 24 | DEFAULTS["interval_function"], 25 | lower_threshold: float = DEFAULTS["lower_threshold"], 26 | upper_threshold: float = DEFAULTS["upper_threshold"], 27 | angle: float = DEFAULTS["angle"] 28 | ) -> Image.Image: 29 | """ 30 | pixelsorts an image 31 | :param image: image to pixelsort 32 | :param mask_image: Image used for masking parts of the image. 33 | :param interval_image: Image used to define intervals. Must be black and white. 34 | :param randomness: What percentage of intervals *not* to sort. 0 by default. 35 | :param char_length: Characteristic length for the random width generator. Used in mode `random` and `waves`. 36 | :param sorting_function: Sorting function to use for sorting the pixels. 37 | :param interval_function: Controls how the intervals used for sorting are defined. 38 | :param lower_threshold: How dark must a pixel be to be considered as a 'border' for sorting? Takes values from 0-1. 39 | Used in edges and threshold modes. 40 | :param upper_threshold: How bright must a pixel be to be considered as a 'border' for sorting? Takes values from 41 | 0-1. Used in threshold mode. 42 | :param angle: Angle at which you're pixel sorting in degrees. 43 | :return: pixelsorted image 44 | """ 45 | original = image 46 | image = image.convert('RGBA').rotate(angle, expand=True) 47 | image_data = image.load() 48 | 49 | mask_image = mask_image if mask_image else Image.new( 50 | "1", original.size, color=255) 51 | mask_image = mask_image.convert('1').rotate(angle, expand=True, fillcolor=0) 52 | mask_data = mask_image.load() 53 | 54 | interval_image = (interval_image 55 | .convert('1') 56 | .rotate(angle, expand=True)) if interval_image else None 57 | logging.debug("Determining intervals...") 58 | intervals = interval_choices[interval_function]( 59 | image, 60 | lower_threshold=lower_threshold, 61 | upper_threshold=upper_threshold, 62 | char_length=char_length, 63 | interval_image=interval_image, 64 | ) 65 | logging.debug("Sorting pixels...") 66 | sorted_pixels = sort_image( 67 | image.size, 68 | image_data, 69 | mask_data, 70 | intervals, 71 | randomness, 72 | sorting_choices[sorting_function]) 73 | 74 | output_img = _place_pixels( 75 | sorted_pixels, 76 | mask_data, 77 | image_data, 78 | image.size) 79 | if angle != 0: 80 | output_img = output_img.rotate(-angle, expand=True) 81 | output_img = crop_to(output_img, original) 82 | 83 | return output_img 84 | 85 | 86 | def _place_pixels(pixels: PyAccess.PyAccess, mask: PyAccess.PyAccess, original: PyAccess.PyAccess, 87 | size: typing.Tuple[int, int]): 88 | output_img = Image.new('RGBA', size) 89 | outputdata = output_img.load() # modifying pixelaccess modified original 90 | for y in range(size[1]): 91 | count = 0 92 | for x in range(size[0]): 93 | if not mask[x, y]: 94 | outputdata[x, y] = original[x, y] 95 | else: 96 | outputdata[x, y] = pixels[y][count] 97 | count += 1 98 | return output_img 99 | -------------------------------------------------------------------------------- /pixelsort/sorter.py: -------------------------------------------------------------------------------- 1 | import random 2 | import typing 3 | 4 | # python implementation of the PixelAccess class returned by im.load(), has the same functions so is fine for type hints 5 | from PIL import PyAccess 6 | 7 | 8 | def sort_image( 9 | size: typing.Tuple[int, int], 10 | image_data: PyAccess.PyAccess, 11 | mask_data: PyAccess.PyAccess, 12 | intervals: typing.List[typing.List[int]], 13 | randomness: float, 14 | sorting_function: typing.Callable[[typing.List[typing.Tuple[int, int, int]]], float]): 15 | sorted_pixels = [] 16 | 17 | for y in range(size[1]): 18 | row = [] 19 | x_min = 0 20 | for x_max in intervals[y] + [size[0]]: 21 | interval = [] 22 | for x in range(x_min, x_max): 23 | if mask_data[x, y]: 24 | interval.append(image_data[x, y]) 25 | if random.random() * 100 < randomness: 26 | row += interval 27 | else: 28 | row += sort_interval(interval, sorting_function) 29 | x_min = x_max 30 | sorted_pixels.append(row) 31 | return sorted_pixels 32 | 33 | 34 | def sort_interval(interval: typing.List, 35 | sorting_function: typing.Callable[[typing.List[typing.Tuple[int, int, int]]], float]): 36 | return [] if interval == [] else sorted(interval, key=sorting_function) 37 | -------------------------------------------------------------------------------- /pixelsort/sorting.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import typing 3 | 4 | 5 | @functools.cache 6 | def lightness(pixel: typing.Tuple[int, int, int]) -> float: 7 | """Sort by the lightness of a pixel according to a HLS representation.""" 8 | # taken from rgb_to_hls 9 | r, g, b = pixel[:3] 10 | maxc = max(r, g, b) 11 | minc = min(r, g, b) 12 | return (minc + maxc) / 2.0 13 | 14 | 15 | @functools.cache 16 | def hue(pixel: typing.Tuple[int, int, int]) -> float: 17 | """Sort by the hue of a pixel according to a HLS representation.""" 18 | # taken from rgb_to_hls 19 | r, g, b = pixel[:3] 20 | maxc = max(r, g, b) 21 | minc = min(r, g, b) 22 | # XXX Can optimize (maxc+minc) and (maxc-minc) 23 | if minc == maxc: 24 | return 0.0 25 | mcminusmc = maxc - minc 26 | rc = (maxc - r) / mcminusmc 27 | gc = (maxc - g) / mcminusmc 28 | bc = (maxc - b) / mcminusmc 29 | if r == maxc: 30 | h = bc - gc 31 | elif g == maxc: 32 | h = 2.0 + rc - bc 33 | else: 34 | h = 4.0 + gc - rc 35 | h = (h / 6.0) % 1.0 36 | return h 37 | 38 | 39 | @functools.cache 40 | def saturation(pixel: typing.Tuple[int, int, int]) -> float: 41 | """Sort by the saturation of a pixel according to a HLS representation.""" 42 | # taken from rgb_to_hls 43 | r, g, b = pixel[:3] 44 | maxc = max(r, g, b) 45 | minc = min(r, g, b) 46 | sumc = minc + maxc 47 | diffc = maxc - minc 48 | sdiv = 2.0 - diffc 49 | 50 | if minc == maxc or sumc == 0 or sdiv == 0: 51 | return 0.0 52 | if sumc / 2.0 <= 0.5: 53 | s = diffc / sumc 54 | else: 55 | s = diffc / sdiv 56 | return s 57 | 58 | 59 | def intensity(pixel: typing.Tuple[int, int, int]) -> int: 60 | """Sort by the intensity of a pixel, i.e. the sum of all the RGB values.""" 61 | return pixel[0] + pixel[1] + pixel[2] 62 | 63 | 64 | def minimum(pixel: typing.Tuple[int, int, int]) -> int: 65 | """Sort on the minimum RGB value of a pixel (either the R, G or B).""" 66 | return min(pixel[0], pixel[1], pixel[2]) 67 | 68 | 69 | choices = { 70 | "lightness": lightness, 71 | "hue": hue, 72 | "intensity": intensity, 73 | "minimum": minimum, 74 | "saturation": saturation 75 | } 76 | -------------------------------------------------------------------------------- /pixelsort/util.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from PIL import Image 4 | 5 | 6 | def id_generator() -> str: 7 | timestr = time.strftime("%Y%m%d-%H%M%S") 8 | return timestr 9 | 10 | 11 | def crop_to(image_to_crop: Image.Image, reference_image: Image.Image) -> Image.Image: 12 | """ 13 | Crops image to the size of a reference image. This function assumes that the relevant image is located in the center 14 | and you want to crop away equal sizes on both the left and right as well on both the top and bottom. 15 | :param image_to_crop 16 | :param reference_image 17 | :return: image cropped to the size of the reference image 18 | """ 19 | reference_size = reference_image.size 20 | current_size = image_to_crop.size 21 | dx = current_size[0] - reference_size[0] 22 | dy = current_size[1] - reference_size[1] 23 | left = dx / 2 24 | upper = dy / 2 25 | right = dx / 2 + reference_size[0] 26 | lower = dy / 2 + reference_size[1] 27 | return image_to_crop.crop( 28 | box=( 29 | int(left), 30 | int(upper), 31 | int(right), 32 | int(lower))) 33 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | with open('requirements.txt') as fh: 7 | requirements = fh.read().splitlines() 8 | 9 | setuptools.setup( 10 | name="pixelsort", 11 | version="1.0.1", 12 | author="Bernard Zhao", 13 | author_email="bernardzhao@berkeley.edu", 14 | description="An image pixelsorter for Python.", 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | url="https://github.com/satyarth/pixelsort", 18 | packages=setuptools.find_packages(), 19 | install_requires=requirements, 20 | classifiers=[ 21 | "Programming Language :: Python", 22 | "License :: OSI Approved :: MIT License", 23 | "Operating System :: OS Independent", 24 | ] 25 | ) 26 | --------------------------------------------------------------------------------