anshitsu 3.2.0__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
anshitsu/__init__.py ADDED
File without changes
@@ -0,0 +1 @@
1
+ version: str = "v2.0.2"
Binary file
anshitsu/cli.py ADDED
@@ -0,0 +1,193 @@
1
+ import datetime
2
+ import glob
3
+ import os
4
+ import os.path
5
+ import re
6
+ from typing import Optional
7
+
8
+ import fire
9
+ import fire.core
10
+ from PIL import Image, UnidentifiedImageError
11
+
12
+ from anshitsu.__version__ import version as __version__
13
+ from anshitsu.process.processor import Processor
14
+
15
+
16
+ def cli(
17
+ path: Optional[str] = None,
18
+ keep_alpha: bool = False,
19
+ colorautoadjust: bool = False,
20
+ colorstretch: bool = False,
21
+ grayscale: bool = False,
22
+ orthochromatic: bool = False,
23
+ invert: bool = False,
24
+ color: Optional[float] = None,
25
+ brightness: Optional[float] = None,
26
+ sharpness: Optional[float] = None,
27
+ contrast: Optional[float] = None,
28
+ tosaka: Optional[float] = None,
29
+ outputrgb: bool = False,
30
+ sepia: bool = False,
31
+ cyanotype: bool = False,
32
+ rochester: bool = False,
33
+ ashigara: bool = False,
34
+ crossprocess: bool = False,
35
+ apocalypse: bool = False,
36
+ ultramarine: bool = False,
37
+ roppongi: bool = False,
38
+ classic: bool = False,
39
+ noise: Optional[float] = None,
40
+ overwrite: bool = False,
41
+ version: bool = False,
42
+ line_drawing: bool = False,
43
+ posterize: Optional[int] = None,
44
+ vignette: Optional[float] = None,
45
+ ) -> str:
46
+ """
47
+ Process Runnner for Command Line Interface
48
+
49
+ This utility converts the colors of images such as photos.
50
+
51
+ If you specify a directory path, it will convert
52
+ the image files in the specified directory.
53
+ If you specify a file path, it will convert the specified file.
54
+ If you specify an option, the specified conversion will be performed.
55
+
56
+ Tosaka mode is named after Tosaka-senpai's "Tri-X de banzen"
57
+ line from "Kyūkyoku Chōjin R". It aims for a grainy
58
+ black-and-white photo look similar to Kodak Tri-X film.
59
+ This mode converts the image to grayscale and adjusts contrast.
60
+ Use floating-point numbers; values around 2.4 usually work well.
61
+
62
+ Args:
63
+ path (Optional[str], optional): Directory or file path. Defaults to None.
64
+ keep_alpha (bool, optional): Keep the alpha channel. Defaults to False.
65
+ colorautoadjust (bool, optional): Correct colors using Automatic Color Equalization. Defaults to False.
66
+ colorstretch (bool, optional): Apply gray-world white balance and color stretching. Defaults to False.
67
+ grayscale (bool, optional): Convert to grayscale. Defaults to False.
68
+ orthochromatic (bool, optional): Convert to orthochromatic-style grayscale. Defaults to False.
69
+ invert (bool, optional): Invert image colors. Defaults to False.
70
+ color (Optional[float], optional): Adjust color. Defaults to None.
71
+ brightness (Optional[float], optional): Adjust brightness. Defaults to None.
72
+ sharpness (Optional[float], optional): Adjust sharpness. Defaults to None.
73
+ contrast (Optional[float], optional): Adjust contrast. Defaults to None.
74
+ tosaka (Optional[float], optional): Use Tosaka mode. Defaults to None.
75
+ outputrgb (bool, optional): Convert a monochrome image to RGB. Defaults to False.
76
+ sepia (bool, optional): Colorize a monochrome image with sepia tones. Defaults to False.
77
+ cyanotype (bool, optional): Colorize a monochrome image with cyanotype-like Prussian blue. Defaults to False.
78
+ rochester (bool, optional): Apply a warm color grade inspired by Kodak PORTRA 400. Defaults to False.
79
+ ashigara (bool, optional): Apply a vivid color grade inspired by Fujifilm Velvia 100. Defaults to False.
80
+ crossprocess (bool, optional): Apply a random cross-process-style color grade. Defaults to False.
81
+ apocalypse (bool, optional): Apply a red-orange Velvia 100 cross-process preset. Defaults to False.
82
+ ultramarine (bool, optional): Apply a blue-forward color grade inspired by Kodak Ultramax. Defaults to False.
83
+ roppongi (bool, optional): Apply a smooth fine-grain monochrome preset. Defaults to False.
84
+ classic (bool, optional): Apply a classic high-acutance monochrome preset. Defaults to False.
85
+ noise (Optional[float], optional): Add Gaussian noise. Defaults to None.
86
+ overwrite (bool, optional): Overwrite original files. Defaults to False.
87
+ version (bool, optional): Show version. Defaults to False.
88
+ line_drawing (bool, optional): Convert to a line drawing. Defaults to False.
89
+ posterize (Optional[int], optional): Posterize the image. Defaults to None.
90
+ vignette (Optional[float], optional): Darken image edges with a radial vignette. Defaults to None.
91
+
92
+ Raises:
93
+ fire.core.FireError: Error that occurs when the specified string is not a path.
94
+
95
+ Returns:
96
+ str: Message.
97
+ """
98
+ if version:
99
+ return "Anshitsu version {}".format(__version__)
100
+ if path is None:
101
+ raise fire.core.FireError("No path specified!")
102
+ types = ("*.jpg", "*.JPG", "*.jpeg", "*.JPEG", "*.png", "*.PNG")
103
+ files_glob = []
104
+ return_path = ""
105
+ now_s = datetime.datetime.now()
106
+ output_dir = "anshitsu_out"
107
+ original_dir = "anshitsu_orig"
108
+ if os.path.isdir(path):
109
+ for type in types:
110
+ files_glob.extend(glob.glob(os.path.join(path, "**", type), recursive=True))
111
+ files_glob = [file for file in files_glob if not file.__contains__(output_dir)]
112
+ return_path = path
113
+
114
+ if len(files_glob) == 0:
115
+ raise fire.core.FireError(
116
+ "There are no JPEG or PNG files in this directory."
117
+ )
118
+ elif os.path.isfile(path):
119
+ files_glob.extend(glob.glob(path))
120
+ return_path = os.path.abspath(os.path.join(path, os.pardir))
121
+ else:
122
+ raise fire.core.FireError("A non-path string was passed.")
123
+ if overwrite is True:
124
+ os.makedirs(os.path.join(return_path, original_dir))
125
+ for i, file in enumerate(files_glob):
126
+ try:
127
+ image = Image.open(file)
128
+ except UnidentifiedImageError as e:
129
+ raise fire.core.FireError(e)
130
+ exif = image.getexif()
131
+ original_filename: str = os.path.split(file)[1]
132
+ extension = original_filename.split(".")[-1]
133
+ timestamp = now_s.strftime("%Y-%m-%d_%H-%M-%S")
134
+ if overwrite is True:
135
+ backup_filename = original_filename
136
+ image.save(os.path.join(return_path, original_dir, backup_filename))
137
+ filename = os.path.join(
138
+ return_path, re.sub(r"\.[^.]+$", "", original_filename) + ".png"
139
+ )
140
+ remove_file_list = [".jpg", ".JPG", ".jpeg", ".JPEG", ".PNG"]
141
+ for remove_file in remove_file_list:
142
+ remove_file_name = (
143
+ re.sub(r"\.[^.]+$", "", original_filename) + remove_file
144
+ )
145
+ remove_file_path = os.path.join(return_path, remove_file_name)
146
+ if os.path.isfile(remove_file_path):
147
+ os.remove(remove_file_path)
148
+ else:
149
+ filename = os.path.join(
150
+ return_path,
151
+ output_dir,
152
+ re.sub(r"\.[^.]+$", "_", original_filename)
153
+ + "_{0}_converted_at_{1}.png".format(extension, timestamp),
154
+ )
155
+ psr = Processor(
156
+ image=image,
157
+ keep_alpha=keep_alpha,
158
+ colorautoadjust=colorautoadjust,
159
+ colorstretch=colorstretch,
160
+ grayscale=grayscale,
161
+ orthochromatic=orthochromatic,
162
+ color=color,
163
+ contrast=contrast,
164
+ brightness=brightness,
165
+ sharpness=sharpness,
166
+ invert=invert,
167
+ tosaka=tosaka,
168
+ outputrgb=outputrgb,
169
+ cyanotype=cyanotype,
170
+ sepia=sepia,
171
+ rochester=rochester,
172
+ ashigara=ashigara,
173
+ crossprocess=crossprocess,
174
+ apocalypse=apocalypse,
175
+ ultramarine=ultramarine,
176
+ roppongi=roppongi,
177
+ classic=classic,
178
+ noise=noise,
179
+ line_drawing=line_drawing,
180
+ posterize=posterize,
181
+ vignette=vignette,
182
+ )
183
+ saved_image = psr.process()
184
+ os.makedirs(os.path.join(return_path, output_dir), exist_ok=True)
185
+ saved_image.save(
186
+ filename,
187
+ quality=100, # Specify 100 as the highest image quality
188
+ subsampling=0,
189
+ exif=exif,
190
+ )
191
+ print("{0}/{1} done!".format((i + 1), str(len(files_glob))))
192
+
193
+ return "The cli was completed successfully."
anshitsu/main.py ADDED
@@ -0,0 +1,14 @@
1
+ import fire
2
+
3
+ from anshitsu.cli import cli
4
+
5
+
6
+ def main():
7
+ """
8
+ main [summary]
9
+ """
10
+ fire.Fire(cli)
11
+
12
+
13
+ if __name__ == "__main__":
14
+ main()
File without changes
@@ -0,0 +1,168 @@
1
+ import ctypes
2
+ import os
3
+ import platform
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from PIL import Image
8
+
9
+
10
+ def automatic_color_equalization(
11
+ image: Image,
12
+ samples: int = 500,
13
+ slope: float = 10.0,
14
+ limit: float = 1000.0,
15
+ ) -> Optional[Image]:
16
+ """
17
+ Apply the Rust Automatic Color Equalization backend when available.
18
+
19
+ Args:
20
+ image: RGB image to process.
21
+ samples: Maximum number of spatial samples used for ACE comparison.
22
+ slope: Slope applied to channel differences before clipping.
23
+ limit: Maximum absolute contrast contribution.
24
+
25
+ Returns:
26
+ Processed RGB image, or None when the native backend is unavailable.
27
+ """
28
+ library = _load_library()
29
+ if library is None:
30
+ return None
31
+
32
+ rgb_image = image.convert("RGB")
33
+ width, height = rgb_image.size
34
+ buffer = bytearray(rgb_image.tobytes())
35
+ array_type = ctypes.c_ubyte * len(buffer)
36
+ c_buffer = array_type.from_buffer(buffer)
37
+
38
+ result = library.anshitsu_ace_rgb(
39
+ c_buffer,
40
+ ctypes.c_size_t(len(buffer)),
41
+ ctypes.c_size_t(width),
42
+ ctypes.c_size_t(height),
43
+ ctypes.c_size_t(samples),
44
+ ctypes.c_float(slope),
45
+ ctypes.c_float(limit),
46
+ )
47
+ if result != 0:
48
+ return None
49
+
50
+ return Image.frombytes("RGB", rgb_image.size, bytes(buffer))
51
+
52
+
53
+ def color_stretch(image: Image) -> Optional[Image]:
54
+ """
55
+ Apply the Rust gray-world and color stretching backend when available.
56
+
57
+ Args:
58
+ image: RGB image to process.
59
+
60
+ Returns:
61
+ Processed RGB image, or None when the native backend is unavailable.
62
+ """
63
+ library = _load_library()
64
+ if library is None:
65
+ return None
66
+
67
+ rgb_image = image.convert("RGB")
68
+ width, height = rgb_image.size
69
+ buffer = bytearray(rgb_image.tobytes())
70
+ array_type = ctypes.c_ubyte * len(buffer)
71
+ c_buffer = array_type.from_buffer(buffer)
72
+
73
+ result = library.anshitsu_color_stretch_rgb(
74
+ c_buffer,
75
+ ctypes.c_size_t(len(buffer)),
76
+ ctypes.c_size_t(width),
77
+ ctypes.c_size_t(height),
78
+ )
79
+ if result != 0:
80
+ return None
81
+
82
+ return Image.frombytes("RGB", rgb_image.size, bytes(buffer))
83
+
84
+
85
+ def _load_library() -> Optional[ctypes.CDLL]:
86
+ path = _find_library_path()
87
+ if path is None:
88
+ return None
89
+
90
+ try:
91
+ library = ctypes.CDLL(str(path))
92
+ except OSError:
93
+ return None
94
+
95
+ try:
96
+ library.anshitsu_ace_rgb.argtypes = [
97
+ ctypes.POINTER(ctypes.c_ubyte),
98
+ ctypes.c_size_t,
99
+ ctypes.c_size_t,
100
+ ctypes.c_size_t,
101
+ ctypes.c_size_t,
102
+ ctypes.c_float,
103
+ ctypes.c_float,
104
+ ]
105
+ library.anshitsu_ace_rgb.restype = ctypes.c_int
106
+ library.anshitsu_color_stretch_rgb.argtypes = [
107
+ ctypes.POINTER(ctypes.c_ubyte),
108
+ ctypes.c_size_t,
109
+ ctypes.c_size_t,
110
+ ctypes.c_size_t,
111
+ ]
112
+ library.anshitsu_color_stretch_rgb.restype = ctypes.c_int
113
+ except AttributeError:
114
+ return None
115
+ return library
116
+
117
+
118
+ def _find_library_path() -> Optional[Path]:
119
+ env_path = os.environ.get("ANSHITSU_COLOR_LIB")
120
+ if env_path:
121
+ path = Path(env_path)
122
+ if path.exists():
123
+ return path
124
+
125
+ library_name = _library_name()
126
+ candidates = [
127
+ Path(__file__).resolve().parents[1] / "_native" / library_name,
128
+ ]
129
+ repository_root = _repository_root()
130
+ if repository_root is not None:
131
+ candidates.append(
132
+ repository_root
133
+ / "native"
134
+ / "anshitsu-color"
135
+ / "target"
136
+ / "release"
137
+ / library_name
138
+ )
139
+ candidates.append(
140
+ repository_root
141
+ / "native"
142
+ / "anshitsu-color"
143
+ / "target"
144
+ / "debug"
145
+ / library_name
146
+ )
147
+
148
+ for candidate in candidates:
149
+ if candidate.exists():
150
+ return candidate
151
+
152
+ return None
153
+
154
+
155
+ def _library_name() -> str:
156
+ system = platform.system()
157
+ if system == "Darwin":
158
+ return "libanshitsu_color.dylib"
159
+ if system == "Windows":
160
+ return "anshitsu_color.dll"
161
+ return "libanshitsu_color.so"
162
+
163
+
164
+ def _repository_root() -> Optional[Path]:
165
+ for parent in Path(__file__).resolve().parents:
166
+ if (parent / "pyproject.toml").exists():
167
+ return parent
168
+ return None
@@ -0,0 +1,67 @@
1
+ from typing import Optional
2
+
3
+ import numpy as np
4
+ from PIL import Image
5
+
6
+
7
+ def _soft_clip_highlights(image_array: np.ndarray) -> np.ndarray:
8
+ """
9
+ Compress highlights after the warm cross-process color shift.
10
+
11
+ Parameters:
12
+ image_array: normalized RGB image array
13
+
14
+ Returns:
15
+ np.ndarray: highlight-compressed normalized RGB image array.
16
+ """
17
+ highlight_start = 0.80
18
+ compression = 2.6
19
+ highlights = np.maximum(image_array - highlight_start, 0.0)
20
+ compressed = highlights / (1.0 + highlights * compression)
21
+ return np.minimum(image_array, highlight_start + compressed)
22
+
23
+
24
+ def apocalypse(image: Image, seed: Optional[int] = None) -> Image:
25
+ """
26
+ Apply a red-orange cross-process preset inspired by Velvia 100.
27
+
28
+ This preset leans into the orange-to-red color cast often associated with
29
+ cross-processing Velvia 100. It aims for a dramatic end-of-days palette:
30
+ hot reds, heavy oranges, suppressed blues, strong saturation, and controlled
31
+ highlights. The strength of the red shift varies slightly each time it runs;
32
+ pass a seed to make the result reproducible for tests or comparisons.
33
+
34
+ Parameters:
35
+ image: Image
36
+ seed: optional random seed
37
+
38
+ Returns:
39
+ Image: processed RGB image.
40
+ """
41
+ rng = np.random.default_rng(seed)
42
+ red_shift = float(rng.uniform(0.75, 1.25))
43
+ image_array = np.array(image.convert("RGB"), dtype="float32") / 255.0
44
+
45
+ image_array = np.power(np.clip(image_array, 0.0, 1.0), (0.88, 0.98, 1.18))
46
+ red = image_array[:, :, 0] * (1.18 + 0.10 * red_shift) + (
47
+ 0.035 + 0.020 * red_shift
48
+ )
49
+ green = image_array[:, :, 1] * (0.98 - 0.035 * red_shift) + 0.020
50
+ blue = image_array[:, :, 2] * (0.78 - 0.12 * red_shift) - (
51
+ 0.010 + 0.010 * red_shift
52
+ )
53
+ image_array = np.stack((red, green, blue), axis=2)
54
+ image_array = (image_array - 0.5) * (1.12 + 0.10 * red_shift) + 0.5
55
+
56
+ luminance = (
57
+ image_array[:, :, 0] * 0.299
58
+ + image_array[:, :, 1] * 0.587
59
+ + image_array[:, :, 2] * 0.114
60
+ )
61
+ image_array = luminance[:, :, np.newaxis] + (
62
+ image_array - luminance[:, :, np.newaxis]
63
+ ) * (1.18 + 0.20 * red_shift)
64
+
65
+ image_array = _soft_clip_highlights(image_array)
66
+ image_array = np.clip(image_array, 0.0, 0.992)
67
+ return Image.fromarray((image_array * 255).astype("uint8"))
@@ -0,0 +1,44 @@
1
+ import numpy as np
2
+ from PIL import Image
3
+
4
+
5
+ def _soft_clip_highlights(image_array: np.ndarray) -> np.ndarray:
6
+ highlight_start = 0.78
7
+ compression = 3.8
8
+ highlights = np.maximum(image_array - highlight_start, 0.0)
9
+ compressed = highlights / (1.0 + highlights * compression)
10
+ return np.minimum(image_array, highlight_start + compressed)
11
+
12
+
13
+ def ashigara(image: Image) -> Image:
14
+ """
15
+ Apply a vivid, high-contrast color grade inspired by Fujifilm Velvia 100.
16
+
17
+ Returns:
18
+ Image: processed image.
19
+ """
20
+ rgb_image = image.convert("RGB")
21
+ image_array = np.array(rgb_image, dtype="float32") / 255.0
22
+
23
+ # Increase contrast with a restrained S-curve.
24
+ image_array = 3 * image_array**2 - 2 * image_array**3
25
+ image_array = (image_array - 0.5) * 1.035 + 0.5
26
+
27
+ red = image_array[:, :, 0] * 1.025
28
+ green = image_array[:, :, 1] * 1.02
29
+ blue = image_array[:, :, 2] * 1.03
30
+ image_array = np.stack((red, green, blue), axis=2)
31
+
32
+ luminance = (
33
+ image_array[:, :, 0] * 0.299
34
+ + image_array[:, :, 1] * 0.587
35
+ + image_array[:, :, 2] * 0.114
36
+ )
37
+ saturation = 1.14
38
+ image_array = luminance[:, :, np.newaxis] + (
39
+ image_array - luminance[:, :, np.newaxis]
40
+ ) * saturation
41
+
42
+ image_array = _soft_clip_highlights(image_array)
43
+ image_array = np.clip(image_array, 0.0, 1.0)
44
+ return Image.fromarray((image_array * 255).astype("uint8"))
@@ -0,0 +1,17 @@
1
+ from PIL import Image, ImageEnhance
2
+
3
+
4
+ def brightness(image: Image, brightness: float) -> Image:
5
+ """
6
+ Adjust image brightness.
7
+
8
+ Parameters:
9
+ image: Image
10
+ brightness: enhancement factor
11
+
12
+ Returns:
13
+ Image: processed image.
14
+ """
15
+ enhancer = ImageEnhance.Brightness(image)
16
+ image = enhancer.enhance(brightness)
17
+ return image
@@ -0,0 +1,65 @@
1
+ import numpy as np
2
+ from PIL import Image, ImageChops, ImageFilter
3
+
4
+ from anshitsu.process.grayscale import grayscale
5
+
6
+
7
+ def _classic_tone_curve(gray: np.ndarray) -> np.ndarray:
8
+ """
9
+ Apply a classic high-acutance monochrome tone curve.
10
+
11
+ Parameters:
12
+ gray: normalized grayscale image array
13
+
14
+ Returns:
15
+ np.ndarray: tone-adjusted normalized image array.
16
+ """
17
+ shadows = np.clip((gray - 0.015) / 0.985, 0.0, 1.0)
18
+ contrast = 3 * shadows**2 - 2 * shadows**3
19
+ contrast = (contrast - 0.5) * 1.12 + 0.5
20
+
21
+ highlight_start = 0.86
22
+ highlights = np.maximum(contrast - highlight_start, 0.0)
23
+ compressed = highlights / (1.0 + highlights * 1.6)
24
+ return np.minimum(contrast, highlight_start + compressed)
25
+
26
+
27
+ def _add_classic_grain(image: Image, amount: float) -> Image:
28
+ """
29
+ Add visible but restrained monochrome grain to an L image.
30
+
31
+ Parameters:
32
+ image: L mode image
33
+ amount: grain strength
34
+
35
+ Returns:
36
+ Image: grain-adjusted L image.
37
+ """
38
+ noise_image = Image.effect_noise(image.size, amount)
39
+ table = [x * 2 for x in range(256)]
40
+ return ImageChops.multiply(image, noise_image).point(table)
41
+
42
+
43
+ def classic(image: Image) -> Image:
44
+ """
45
+ Apply a classic monochrome preset inspired by Kodak TRI-X in Rodinal.
46
+
47
+ The preset converts the image to L mode, adds a firm but not extreme tone
48
+ curve, protects the brightest highlights, adds visible fine grain, and
49
+ applies mild sharpening. It is intended to be less aggressive than Tosaka
50
+ mode while keeping the traditional high-acutance feel of TRI-X developed in
51
+ Rodinal.
52
+
53
+ Parameters:
54
+ image: Image
55
+
56
+ Returns:
57
+ Image: processed image in L mode.
58
+ """
59
+ monochrome = grayscale(image)
60
+ gray = np.array(monochrome, dtype="float32") / 255.0
61
+ toned = np.clip(_classic_tone_curve(gray), 0.0, 1.0)
62
+ processed = Image.fromarray((toned * 255).astype("uint8"))
63
+ processed = _add_classic_grain(processed, 7.0)
64
+ processed = processed.filter(ImageFilter.UnsharpMask(radius=1.0, percent=70, threshold=2))
65
+ return processed.point(lambda value: min(value, 252))
@@ -0,0 +1,17 @@
1
+ from PIL import Image, ImageEnhance
2
+
3
+
4
+ def color(image: Image, color: float) -> Image:
5
+ """
6
+ Adjust image color.
7
+
8
+ Parameters:
9
+ image: Image
10
+ color: enhancement factor
11
+
12
+ Returns:
13
+ Image: processed image.
14
+ """
15
+ enhancer = ImageEnhance.Contrast(image)
16
+ image = enhancer.enhance(color)
17
+ return image
@@ -0,0 +1,23 @@
1
+ import colorcorrect.algorithm as cca
2
+ from PIL import Image
3
+ from colorcorrect.util import to_pil, from_pil
4
+
5
+ from anshitsu.process import _native_color
6
+
7
+
8
+ def color_auto_adjust(image: Image) -> Image:
9
+ """
10
+ Correct colors using the Automatic Color Equalization algorithm.
11
+
12
+ This process is based on the 2002 paper on Automatic Color Equalization
13
+ by Carlo Gatta and coauthors.
14
+
15
+ Returns:
16
+ Image: processed image.
17
+ """
18
+ if image.mode == "L":
19
+ return image
20
+ native_image = _native_color.automatic_color_equalization(image)
21
+ if native_image is not None:
22
+ return native_image
23
+ return to_pil(cca.automatic_color_equalization(from_pil(image)))
@@ -0,0 +1,23 @@
1
+ import colorcorrect.algorithm as cca
2
+ from PIL import Image
3
+ from colorcorrect.util import to_pil, from_pil
4
+
5
+ from anshitsu.process import _native_color
6
+
7
+
8
+ def color_stretch(image: Image) -> Image:
9
+ """
10
+ Apply gray-world white balance and color stretching.
11
+
12
+ Parameters:
13
+ image: Image
14
+
15
+ Returns:
16
+ Image: processed image.
17
+ """
18
+ if image.mode == "L":
19
+ return image
20
+ native_image = _native_color.color_stretch(image)
21
+ if native_image is not None:
22
+ return native_image
23
+ return to_pil(cca.stretch(cca.grey_world(from_pil(image))))
@@ -0,0 +1,24 @@
1
+ from PIL import Image, ImageEnhance
2
+
3
+
4
+ def contrast(image: Image, contrast: float) -> Image:
5
+ """
6
+ Adjust image contrast.
7
+
8
+ Used by Tosaka mode.
9
+
10
+ Tosaka mode is named after Tosaka-senpai's "Tri-X de banzen"
11
+ line from "Kyūkyoku Chōjin R". It aims for a grainy
12
+ black-and-white photo look similar to Kodak Tri-X film.
13
+ Values around 2.4 usually work well.
14
+
15
+ Parameters:
16
+ image: Image
17
+ contrast: enhancement factor
18
+
19
+ Returns:
20
+ Image: processed image.
21
+ """
22
+ enhancer = ImageEnhance.Contrast(image)
23
+ image = enhancer.enhance(contrast)
24
+ return image