pyimagecuda 0.0.8__cp313-cp313-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.
- pyimagecuda/__init__.py +66 -0
- pyimagecuda/adjust.py +115 -0
- pyimagecuda/blend.py +294 -0
- pyimagecuda/effect.py +165 -0
- pyimagecuda/fill.py +263 -0
- pyimagecuda/filter.py +198 -0
- pyimagecuda/image.py +95 -0
- pyimagecuda/io.py +246 -0
- pyimagecuda/pyimagecuda_internal.cp313-win_amd64.pyd +0 -0
- pyimagecuda/resize.py +109 -0
- pyimagecuda/text.py +107 -0
- pyimagecuda/transform.py +196 -0
- pyimagecuda/utils.py +17 -0
- pyimagecuda-0.0.8.dist-info/METADATA +132 -0
- pyimagecuda-0.0.8.dist-info/RECORD +17 -0
- pyimagecuda-0.0.8.dist-info/WHEEL +5 -0
- pyimagecuda-0.0.8.dist-info/licenses/LICENSE +21 -0
pyimagecuda/io.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import pyvips
|
|
2
|
+
|
|
3
|
+
from .pyimagecuda_internal import upload_to_buffer, convert_f32_to_u8, convert_u8_to_f32, download_from_buffer, copy_buffer #type: ignore
|
|
4
|
+
from .image import Image, ImageU8, ImageBase
|
|
5
|
+
from .utils import ensure_capacity
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import numpy as np
|
|
9
|
+
except ImportError:
|
|
10
|
+
np = None
|
|
11
|
+
|
|
12
|
+
def upload(image: ImageBase, data: bytes | bytearray | memoryview) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Uploads the image data from a bytes-like object to the GPU.
|
|
15
|
+
|
|
16
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#direct-uploaddownload
|
|
17
|
+
"""
|
|
18
|
+
bytes_per_pixel = 4 if isinstance(image, ImageU8) else 16
|
|
19
|
+
expected = image.width * image.height * bytes_per_pixel
|
|
20
|
+
actual = data.nbytes if isinstance(data, memoryview) else len(data)
|
|
21
|
+
|
|
22
|
+
if actual != expected:
|
|
23
|
+
raise ValueError(f"Expected {expected} bytes, got {actual}")
|
|
24
|
+
|
|
25
|
+
upload_to_buffer(image._buffer._handle, data, image.width, image.height)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def download(image: ImageBase) -> bytes:
|
|
29
|
+
"""
|
|
30
|
+
Downloads the image data from the GPU to a bytes object.
|
|
31
|
+
|
|
32
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#direct-uploaddownload
|
|
33
|
+
"""
|
|
34
|
+
return download_from_buffer(image._buffer._handle, image.width, image.height)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def copy(dst: ImageBase, src: ImageBase) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Copies image data from the source image to the destination image.
|
|
40
|
+
|
|
41
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#copy-between-buffers
|
|
42
|
+
"""
|
|
43
|
+
ensure_capacity(dst, src.width, src.height)
|
|
44
|
+
copy_buffer(dst._buffer._handle, src._buffer._handle, src.width, src.height)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def convert_float_to_u8(dst: ImageU8, src: Image) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Converts a floating-point image to an 8-bit unsigned integer image.
|
|
50
|
+
|
|
51
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#manual-conversions
|
|
52
|
+
"""
|
|
53
|
+
ensure_capacity(dst, src.width, src.height)
|
|
54
|
+
convert_f32_to_u8(dst._buffer._handle, src._buffer._handle, src.width, src.height)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def convert_u8_to_float(dst: Image, src: ImageU8) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Converts an 8-bit unsigned integer image to a floating-point image.
|
|
60
|
+
|
|
61
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#manual-conversions
|
|
62
|
+
"""
|
|
63
|
+
ensure_capacity(dst, src.width, src.height)
|
|
64
|
+
convert_u8_to_f32(dst._buffer._handle, src._buffer._handle, src.width, src.height)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load(
|
|
68
|
+
filepath: str,
|
|
69
|
+
f32_buffer: Image | None = None,
|
|
70
|
+
u8_buffer: ImageU8 | None = None
|
|
71
|
+
) -> Image | None:
|
|
72
|
+
"""
|
|
73
|
+
Loads an image from a file (returns new image or writes to buffer).
|
|
74
|
+
|
|
75
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#loading-images
|
|
76
|
+
"""
|
|
77
|
+
vips_img = pyvips.Image.new_from_file(filepath, access='sequential')
|
|
78
|
+
|
|
79
|
+
if vips_img.bands == 1:
|
|
80
|
+
vips_img = vips_img.bandjoin([vips_img, vips_img, vips_img])
|
|
81
|
+
vips_img = vips_img.bandjoin(255)
|
|
82
|
+
elif vips_img.bands == 3:
|
|
83
|
+
vips_img = vips_img.bandjoin(255)
|
|
84
|
+
elif vips_img.bands == 4:
|
|
85
|
+
pass
|
|
86
|
+
else:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"Unsupported image format: {vips_img.bands} channels. "
|
|
89
|
+
f"Only grayscale (1), RGB (3), and RGBA (4) are supported."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
width = vips_img.width
|
|
93
|
+
height = vips_img.height
|
|
94
|
+
|
|
95
|
+
should_return = False
|
|
96
|
+
|
|
97
|
+
if f32_buffer is None:
|
|
98
|
+
f32_buffer = Image(width, height)
|
|
99
|
+
should_return = True
|
|
100
|
+
else:
|
|
101
|
+
ensure_capacity(f32_buffer, width, height)
|
|
102
|
+
should_return = False
|
|
103
|
+
|
|
104
|
+
if u8_buffer is None:
|
|
105
|
+
u8_buffer = ImageU8(width, height)
|
|
106
|
+
owns_u8 = True
|
|
107
|
+
else:
|
|
108
|
+
ensure_capacity(u8_buffer, width, height)
|
|
109
|
+
owns_u8 = False
|
|
110
|
+
|
|
111
|
+
vips_img = vips_img.cast('uchar')
|
|
112
|
+
pixel_data = vips_img.write_to_memory()
|
|
113
|
+
|
|
114
|
+
upload(u8_buffer, pixel_data)
|
|
115
|
+
|
|
116
|
+
convert_u8_to_float(f32_buffer, u8_buffer)
|
|
117
|
+
|
|
118
|
+
if owns_u8:
|
|
119
|
+
u8_buffer.free()
|
|
120
|
+
|
|
121
|
+
return f32_buffer if should_return else None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def save(image: Image, filepath: str, u8_buffer: ImageU8 | None = None, quality: int | None = None) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Saves the floating-point image to a file (using an 8-bit buffer for conversion).
|
|
127
|
+
|
|
128
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#saving-images
|
|
129
|
+
"""
|
|
130
|
+
if u8_buffer is None:
|
|
131
|
+
u8_buffer = ImageU8(image.width, image.height)
|
|
132
|
+
owns_buffer = True
|
|
133
|
+
else:
|
|
134
|
+
ensure_capacity(u8_buffer, image.width, image.height)
|
|
135
|
+
owns_buffer = False
|
|
136
|
+
|
|
137
|
+
convert_float_to_u8(u8_buffer, image)
|
|
138
|
+
pixel_data = download(u8_buffer)
|
|
139
|
+
|
|
140
|
+
vips_img = pyvips.Image.new_from_memory(
|
|
141
|
+
pixel_data,
|
|
142
|
+
image.width,
|
|
143
|
+
image.height,
|
|
144
|
+
bands=4,
|
|
145
|
+
format='uchar'
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
vips_img = vips_img.copy(interpretation='srgb')
|
|
149
|
+
|
|
150
|
+
save_kwargs = {}
|
|
151
|
+
if quality is not None:
|
|
152
|
+
if filepath.lower().endswith(('.jpg', '.jpeg')):
|
|
153
|
+
save_kwargs['Q'] = quality
|
|
154
|
+
elif filepath.lower().endswith('.webp'):
|
|
155
|
+
save_kwargs['Q'] = quality
|
|
156
|
+
elif filepath.lower().endswith(('.heic', '.heif')):
|
|
157
|
+
save_kwargs['Q'] = quality
|
|
158
|
+
|
|
159
|
+
vips_img.write_to_file(filepath, **save_kwargs)
|
|
160
|
+
|
|
161
|
+
if owns_buffer:
|
|
162
|
+
u8_buffer.free()
|
|
163
|
+
|
|
164
|
+
def from_numpy(array, f32_buffer: Image | None = None, u8_buffer: ImageU8 | None = None) -> Image:
|
|
165
|
+
"""
|
|
166
|
+
Creates a PyImageCUDA Image from a NumPy array (e.g. from OpenCV, Pillow, Matplotlib).
|
|
167
|
+
|
|
168
|
+
- Handles uint8 (0-255) -> float32 (0.0-1.0) conversion automatically on GPU.
|
|
169
|
+
- Handles Grayscale/RGB -> RGBA expansion automatically.
|
|
170
|
+
- Optimized: Uploads uint8 data (4x smaller) if possible, then converts on GPU.
|
|
171
|
+
|
|
172
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#numpy-integration
|
|
173
|
+
"""
|
|
174
|
+
if np is None:
|
|
175
|
+
raise ImportError("NumPy is not installed. Run `pip install numpy` to use this feature.")
|
|
176
|
+
|
|
177
|
+
if not isinstance(array, np.ndarray):
|
|
178
|
+
raise TypeError(f"Expected numpy.ndarray, got {type(array)}")
|
|
179
|
+
|
|
180
|
+
target_dtype = array.dtype
|
|
181
|
+
|
|
182
|
+
if array.ndim == 2:
|
|
183
|
+
h, w = array.shape
|
|
184
|
+
alpha_val = 255 if target_dtype == np.uint8 else 1.0
|
|
185
|
+
alpha_channel = np.full((h, w), alpha_val, dtype=target_dtype)
|
|
186
|
+
array = np.dstack((array, array, array, alpha_channel))
|
|
187
|
+
|
|
188
|
+
elif array.ndim == 3:
|
|
189
|
+
h, w, c = array.shape
|
|
190
|
+
if c == 3:
|
|
191
|
+
alpha_val = 255 if target_dtype == np.uint8 else 1.0
|
|
192
|
+
alpha_channel = np.full((h, w), alpha_val, dtype=target_dtype)
|
|
193
|
+
array = np.dstack((array, alpha_channel))
|
|
194
|
+
elif c != 4:
|
|
195
|
+
raise ValueError(f"Unsupported channel count: {c}. PyImageCUDA requires 1, 3, or 4 channels.")
|
|
196
|
+
else:
|
|
197
|
+
raise ValueError(f"Unsupported array shape: {array.shape}. Expected (H, W), (H, W, 3) or (H, W, 4).")
|
|
198
|
+
|
|
199
|
+
if not array.flags['C_CONTIGUOUS']:
|
|
200
|
+
array = np.ascontiguousarray(array)
|
|
201
|
+
|
|
202
|
+
height, width = array.shape[:2]
|
|
203
|
+
|
|
204
|
+
should_return = False
|
|
205
|
+
if f32_buffer is None:
|
|
206
|
+
f32_buffer = Image(width, height)
|
|
207
|
+
should_return = True
|
|
208
|
+
else:
|
|
209
|
+
ensure_capacity(f32_buffer, width, height)
|
|
210
|
+
|
|
211
|
+
if array.dtype == np.uint8:
|
|
212
|
+
owns_u8 = False
|
|
213
|
+
if u8_buffer is None:
|
|
214
|
+
u8_buffer = ImageU8(width, height)
|
|
215
|
+
owns_u8 = True
|
|
216
|
+
else:
|
|
217
|
+
ensure_capacity(u8_buffer, width, height)
|
|
218
|
+
|
|
219
|
+
upload(u8_buffer, array.tobytes())
|
|
220
|
+
convert_u8_to_float(f32_buffer, u8_buffer)
|
|
221
|
+
|
|
222
|
+
if owns_u8:
|
|
223
|
+
u8_buffer.free()
|
|
224
|
+
|
|
225
|
+
elif array.dtype == np.float32:
|
|
226
|
+
upload(f32_buffer, array.tobytes())
|
|
227
|
+
else:
|
|
228
|
+
array = array.astype(np.float32)
|
|
229
|
+
upload(f32_buffer, array.tobytes())
|
|
230
|
+
|
|
231
|
+
return f32_buffer if should_return else None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def to_numpy(image: Image) -> 'np.ndarray': # type: ignore
|
|
235
|
+
"""
|
|
236
|
+
Downloads a PyImageCUDA Image to a NumPy array.
|
|
237
|
+
|
|
238
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#numpy-integration
|
|
239
|
+
"""
|
|
240
|
+
if np is None:
|
|
241
|
+
raise ImportError("NumPy is not installed. Run `pip install numpy` to use this feature.")
|
|
242
|
+
|
|
243
|
+
raw_bytes = download(image)
|
|
244
|
+
array = np.frombuffer(raw_bytes, dtype=np.float32)
|
|
245
|
+
|
|
246
|
+
return array.reshape((image.height, image.width, 4))
|
|
Binary file
|
pyimagecuda/resize.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from .image import Image
|
|
2
|
+
from .utils import ensure_capacity
|
|
3
|
+
from .pyimagecuda_internal import resize_f32 #type: ignore
|
|
4
|
+
from .io import copy
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _resize_internal(
|
|
8
|
+
src: Image,
|
|
9
|
+
width: int | None,
|
|
10
|
+
height: int | None,
|
|
11
|
+
method: int,
|
|
12
|
+
dst_buffer: Image | None = None
|
|
13
|
+
) -> Image | None:
|
|
14
|
+
|
|
15
|
+
if width is None and height is None:
|
|
16
|
+
raise ValueError("At least one of width or height must be specified")
|
|
17
|
+
elif width is None:
|
|
18
|
+
width = int(src.width * (height / src.height))
|
|
19
|
+
elif height is None:
|
|
20
|
+
height = int(src.height * (width / src.width))
|
|
21
|
+
|
|
22
|
+
# No resize needed, just copy
|
|
23
|
+
if width == src.width and height == src.height:
|
|
24
|
+
if dst_buffer is None:
|
|
25
|
+
dst_buffer = Image(width, height)
|
|
26
|
+
copy(dst_buffer, src)
|
|
27
|
+
return dst_buffer
|
|
28
|
+
else:
|
|
29
|
+
copy(dst_buffer, src)
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
# Resize normal
|
|
33
|
+
if dst_buffer is None:
|
|
34
|
+
dst_buffer = Image(width, height)
|
|
35
|
+
return_buffer = True
|
|
36
|
+
else:
|
|
37
|
+
ensure_capacity(dst_buffer, width, height)
|
|
38
|
+
return_buffer = False
|
|
39
|
+
|
|
40
|
+
resize_f32(
|
|
41
|
+
src._buffer._handle,
|
|
42
|
+
dst_buffer._buffer._handle,
|
|
43
|
+
src.width,
|
|
44
|
+
src.height,
|
|
45
|
+
width,
|
|
46
|
+
height,
|
|
47
|
+
method
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return dst_buffer if return_buffer else None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Resize:
|
|
54
|
+
@staticmethod
|
|
55
|
+
def nearest(
|
|
56
|
+
src: Image,
|
|
57
|
+
width: int | None = None,
|
|
58
|
+
height: int | None = None,
|
|
59
|
+
dst_buffer: Image | None = None
|
|
60
|
+
) -> Image | None:
|
|
61
|
+
"""
|
|
62
|
+
Resizes the image using nearest neighbor interpolation (returns new image or writes to buffer).
|
|
63
|
+
|
|
64
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/resize/#nearest
|
|
65
|
+
"""
|
|
66
|
+
return _resize_internal(src, width, height, 0, dst_buffer)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def bilinear(
|
|
70
|
+
src: Image,
|
|
71
|
+
width: int | None = None,
|
|
72
|
+
height: int | None = None,
|
|
73
|
+
dst_buffer: Image | None = None
|
|
74
|
+
) -> Image | None:
|
|
75
|
+
"""
|
|
76
|
+
Resizes the image using bilinear interpolation (returns new image or writes to buffer).
|
|
77
|
+
|
|
78
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/resize/#bilinear
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
return _resize_internal(src, width, height, 1, dst_buffer)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def bicubic(
|
|
85
|
+
src: Image,
|
|
86
|
+
width: int | None = None,
|
|
87
|
+
height: int | None = None,
|
|
88
|
+
dst_buffer: Image | None = None
|
|
89
|
+
) -> Image | None:
|
|
90
|
+
"""
|
|
91
|
+
Resizes the image using bicubic interpolation (returns new image or writes to buffer).
|
|
92
|
+
|
|
93
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/resize/#bicubic
|
|
94
|
+
"""
|
|
95
|
+
return _resize_internal(src, width, height, 2, dst_buffer)
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def lanczos(
|
|
99
|
+
src: Image,
|
|
100
|
+
width: int | None = None,
|
|
101
|
+
height: int | None = None,
|
|
102
|
+
dst_buffer: Image | None = None
|
|
103
|
+
) -> Image | None:
|
|
104
|
+
"""
|
|
105
|
+
Resizes the image using Lanczos interpolation (returns new image or writes to buffer).
|
|
106
|
+
|
|
107
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/resize/#lanczos
|
|
108
|
+
"""
|
|
109
|
+
return _resize_internal(src, width, height, 3, dst_buffer)
|
pyimagecuda/text.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
import pyvips
|
|
3
|
+
|
|
4
|
+
from .image import Image, ImageU8
|
|
5
|
+
from .io import upload, convert_u8_to_float
|
|
6
|
+
from .utils import ensure_capacity
|
|
7
|
+
|
|
8
|
+
class Text:
|
|
9
|
+
@staticmethod
|
|
10
|
+
def create(
|
|
11
|
+
text: str,
|
|
12
|
+
font: str = "Sans",
|
|
13
|
+
size: float = 12.0,
|
|
14
|
+
color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 1.0),
|
|
15
|
+
bg_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
|
|
16
|
+
align: Literal['left', 'centre', 'right'] = 'left',
|
|
17
|
+
justify: bool = False,
|
|
18
|
+
spacing: int = 0,
|
|
19
|
+
letter_spacing: float = 0.0,
|
|
20
|
+
dst_buffer: Image | None = None,
|
|
21
|
+
u8_buffer: ImageU8 | None = None
|
|
22
|
+
) -> Image | None:
|
|
23
|
+
"""
|
|
24
|
+
Renders text into an image with specified font, size, color, alignment, and spacing.
|
|
25
|
+
|
|
26
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/text/#text-rendering
|
|
27
|
+
"""
|
|
28
|
+
full_font_string = f"{font} {size}"
|
|
29
|
+
|
|
30
|
+
text_opts = {
|
|
31
|
+
'font': full_font_string,
|
|
32
|
+
'dpi': 72,
|
|
33
|
+
'align': 0 if align == 'left' else (1 if align == 'centre' else 2),
|
|
34
|
+
'justify': justify,
|
|
35
|
+
'spacing': spacing
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
final_text = text
|
|
39
|
+
if letter_spacing != 0:
|
|
40
|
+
pango_spacing = int(letter_spacing * 1024)
|
|
41
|
+
final_text = f'<span letter_spacing="{pango_spacing}">{text}</span>'
|
|
42
|
+
text_opts['rgba'] = True
|
|
43
|
+
|
|
44
|
+
if '<' in text and '>' in text:
|
|
45
|
+
text_opts['rgba'] = True
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
if text_opts.get('rgba'):
|
|
49
|
+
mask_rgba = pyvips.Image.text(final_text, **text_opts)
|
|
50
|
+
if mask_rgba.bands == 4:
|
|
51
|
+
mask = mask_rgba[3]
|
|
52
|
+
else:
|
|
53
|
+
mask = mask_rgba.colourspace('b-w')[0]
|
|
54
|
+
else:
|
|
55
|
+
mask = pyvips.Image.text(final_text, **text_opts)
|
|
56
|
+
|
|
57
|
+
except pyvips.Error as e:
|
|
58
|
+
raise RuntimeError(f"Failed to render text: {e}")
|
|
59
|
+
|
|
60
|
+
fg_r, fg_g, fg_b, fg_a = [int(c * 255) for c in color]
|
|
61
|
+
bg_r, bg_g, bg_b, bg_a = [int(c * 255) for c in bg_color]
|
|
62
|
+
|
|
63
|
+
fg_rgb = mask.new_from_image([fg_r, fg_g, fg_b])
|
|
64
|
+
|
|
65
|
+
if fg_a < 255:
|
|
66
|
+
fg_alpha = (mask * (fg_a / 255.0)).cast('uchar')
|
|
67
|
+
else:
|
|
68
|
+
fg_alpha = mask
|
|
69
|
+
|
|
70
|
+
fg_layer = fg_rgb.bandjoin(fg_alpha)
|
|
71
|
+
fg_layer = fg_layer.copy(interpretation='srgb')
|
|
72
|
+
|
|
73
|
+
if bg_a > 0:
|
|
74
|
+
bg_layer = mask.new_from_image([bg_r, bg_g, bg_b, bg_a])
|
|
75
|
+
bg_layer = bg_layer.copy(interpretation='srgb')
|
|
76
|
+
final_vips = bg_layer.composite(fg_layer, 'over')
|
|
77
|
+
else:
|
|
78
|
+
final_vips = fg_layer
|
|
79
|
+
|
|
80
|
+
final_vips = final_vips.cast('uchar')
|
|
81
|
+
|
|
82
|
+
w, h = final_vips.width, final_vips.height
|
|
83
|
+
raw_bytes = final_vips.write_to_memory()
|
|
84
|
+
|
|
85
|
+
if dst_buffer is None:
|
|
86
|
+
result = Image(w, h)
|
|
87
|
+
return_result = True
|
|
88
|
+
else:
|
|
89
|
+
ensure_capacity(dst_buffer, w, h)
|
|
90
|
+
result = dst_buffer
|
|
91
|
+
return_result = False
|
|
92
|
+
|
|
93
|
+
if u8_buffer is None:
|
|
94
|
+
u8_temp = ImageU8(w, h)
|
|
95
|
+
free_u8 = True
|
|
96
|
+
else:
|
|
97
|
+
ensure_capacity(u8_buffer, w, h)
|
|
98
|
+
u8_temp = u8_buffer
|
|
99
|
+
free_u8 = False
|
|
100
|
+
|
|
101
|
+
upload(u8_temp, raw_bytes)
|
|
102
|
+
convert_u8_to_float(result, u8_temp)
|
|
103
|
+
|
|
104
|
+
if free_u8:
|
|
105
|
+
u8_temp.free()
|
|
106
|
+
|
|
107
|
+
return result if return_result else None
|
pyimagecuda/transform.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from typing import Literal
|
|
3
|
+
from .image import Image
|
|
4
|
+
from .utils import ensure_capacity
|
|
5
|
+
from .fill import Fill
|
|
6
|
+
from .io import copy
|
|
7
|
+
from .pyimagecuda_internal import ( #type: ignore
|
|
8
|
+
flip_f32, crop_f32, rotate_fixed_f32,
|
|
9
|
+
rotate_arbitrary_f32, copy_buffer
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Transform:
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def flip(
|
|
17
|
+
image: Image,
|
|
18
|
+
direction: Literal['horizontal', 'vertical', 'both'] = 'horizontal',
|
|
19
|
+
dst_buffer: Image | None = None
|
|
20
|
+
) -> Image | None:
|
|
21
|
+
"""
|
|
22
|
+
Flips the image across the specified axis (returns new image or writes to buffer).
|
|
23
|
+
|
|
24
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/transform/#flip
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
direction_map = {
|
|
28
|
+
'horizontal': 0,
|
|
29
|
+
'vertical': 1,
|
|
30
|
+
'both': 2
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
mode = direction_map.get(direction)
|
|
34
|
+
if mode is None:
|
|
35
|
+
raise ValueError(f"Invalid direction: {direction}. Must be {list(direction_map.keys())}")
|
|
36
|
+
|
|
37
|
+
if dst_buffer is None:
|
|
38
|
+
dst_buffer = Image(image.width, image.height)
|
|
39
|
+
return_buffer = True
|
|
40
|
+
else:
|
|
41
|
+
ensure_capacity(dst_buffer, image.width, image.height)
|
|
42
|
+
return_buffer = False
|
|
43
|
+
|
|
44
|
+
flip_f32(
|
|
45
|
+
image._buffer._handle,
|
|
46
|
+
dst_buffer._buffer._handle,
|
|
47
|
+
image.width,
|
|
48
|
+
image.height,
|
|
49
|
+
mode
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return dst_buffer if return_buffer else None
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def rotate(
|
|
56
|
+
image: Image,
|
|
57
|
+
angle: float,
|
|
58
|
+
expand: bool = True,
|
|
59
|
+
dst_buffer: Image | None = None
|
|
60
|
+
) -> Image | None:
|
|
61
|
+
"""
|
|
62
|
+
Rotates the image by any angle in degrees (Clockwise).
|
|
63
|
+
|
|
64
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/transform/#rotate
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
norm_angle = angle % 360
|
|
68
|
+
if norm_angle < 0: norm_angle += 360
|
|
69
|
+
|
|
70
|
+
is_fixed = False
|
|
71
|
+
fixed_mode = 0
|
|
72
|
+
|
|
73
|
+
if abs(norm_angle - 0) < 0.01:
|
|
74
|
+
if dst_buffer is None:
|
|
75
|
+
dst_buffer = Image(image.width, image.height)
|
|
76
|
+
return_buffer = True
|
|
77
|
+
else:
|
|
78
|
+
ensure_capacity(dst_buffer, image.width, image.height)
|
|
79
|
+
return_buffer = False
|
|
80
|
+
|
|
81
|
+
copy_buffer(dst_buffer._buffer._handle, image._buffer._handle, image.width, image.height)
|
|
82
|
+
return dst_buffer if return_buffer else None
|
|
83
|
+
|
|
84
|
+
elif abs(norm_angle - 90) < 0.01: is_fixed = True; fixed_mode = 0
|
|
85
|
+
elif abs(norm_angle - 180) < 0.01: is_fixed = True; fixed_mode = 1
|
|
86
|
+
elif abs(norm_angle - 270) < 0.01: is_fixed = True; fixed_mode = 2
|
|
87
|
+
|
|
88
|
+
if is_fixed:
|
|
89
|
+
if fixed_mode == 1:
|
|
90
|
+
rot_w, rot_h = image.width, image.height
|
|
91
|
+
else:
|
|
92
|
+
rot_w, rot_h = image.height, image.width
|
|
93
|
+
else:
|
|
94
|
+
rads = math.radians(angle)
|
|
95
|
+
sin_a = abs(math.sin(rads))
|
|
96
|
+
cos_a = abs(math.cos(rads))
|
|
97
|
+
rot_w = int(image.width * cos_a + image.height * sin_a)
|
|
98
|
+
rot_h = int(image.width * sin_a + image.height * cos_a)
|
|
99
|
+
|
|
100
|
+
if expand:
|
|
101
|
+
final_w = rot_w
|
|
102
|
+
final_h = rot_h
|
|
103
|
+
offset_x = 0
|
|
104
|
+
offset_y = 0
|
|
105
|
+
else:
|
|
106
|
+
final_w = image.width
|
|
107
|
+
final_h = image.height
|
|
108
|
+
offset_x = (final_w - rot_w) // 2
|
|
109
|
+
offset_y = (final_h - rot_h) // 2
|
|
110
|
+
|
|
111
|
+
if dst_buffer is None:
|
|
112
|
+
dst_buffer = Image(final_w, final_h)
|
|
113
|
+
return_buffer = True
|
|
114
|
+
else:
|
|
115
|
+
ensure_capacity(dst_buffer, final_w, final_h)
|
|
116
|
+
return_buffer = False
|
|
117
|
+
|
|
118
|
+
if is_fixed:
|
|
119
|
+
rotate_fixed_f32(
|
|
120
|
+
image._buffer._handle,
|
|
121
|
+
dst_buffer._buffer._handle,
|
|
122
|
+
image.width, image.height,
|
|
123
|
+
final_w, final_h,
|
|
124
|
+
fixed_mode, offset_x, offset_y
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
rotate_arbitrary_f32(
|
|
128
|
+
image._buffer._handle,
|
|
129
|
+
dst_buffer._buffer._handle,
|
|
130
|
+
image.width, image.height,
|
|
131
|
+
final_w, final_h,
|
|
132
|
+
float(angle)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return dst_buffer if return_buffer else None
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def crop(
|
|
139
|
+
image: Image,
|
|
140
|
+
x: int,
|
|
141
|
+
y: int,
|
|
142
|
+
width: int,
|
|
143
|
+
height: int,
|
|
144
|
+
dst_buffer: Image | None = None
|
|
145
|
+
) -> Image | None:
|
|
146
|
+
"""
|
|
147
|
+
Crops a rectangular region (returns new image or writes to buffer).
|
|
148
|
+
|
|
149
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/transform/#crop
|
|
150
|
+
"""
|
|
151
|
+
if width <= 0 or height <= 0:
|
|
152
|
+
raise ValueError("Crop dimensions must be positive")
|
|
153
|
+
|
|
154
|
+
if x == 0 and y == 0 and width == image.width and height == image.height:
|
|
155
|
+
if dst_buffer is None:
|
|
156
|
+
dst_buffer = Image(width, height)
|
|
157
|
+
copy(dst_buffer, image)
|
|
158
|
+
return dst_buffer
|
|
159
|
+
else:
|
|
160
|
+
copy(dst_buffer, image)
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
if dst_buffer is None:
|
|
164
|
+
dst_buffer = Image(width, height)
|
|
165
|
+
return_buffer = True
|
|
166
|
+
else:
|
|
167
|
+
ensure_capacity(dst_buffer, width, height)
|
|
168
|
+
return_buffer = False
|
|
169
|
+
|
|
170
|
+
Fill.color(dst_buffer, (0.0, 0.0, 0.0, 0.0))
|
|
171
|
+
|
|
172
|
+
crop_left = x
|
|
173
|
+
crop_top = y
|
|
174
|
+
crop_right = x + width
|
|
175
|
+
crop_bottom = y + height
|
|
176
|
+
img_right, img_bottom = image.width, image.height
|
|
177
|
+
|
|
178
|
+
intersect_left = max(crop_left, 0)
|
|
179
|
+
intersect_top = max(crop_top, 0)
|
|
180
|
+
intersect_right = min(crop_right, img_right)
|
|
181
|
+
intersect_bottom = min(crop_bottom, img_bottom)
|
|
182
|
+
|
|
183
|
+
copy_w = intersect_right - intersect_left
|
|
184
|
+
copy_h = intersect_bottom - intersect_top
|
|
185
|
+
|
|
186
|
+
if copy_w > 0 and copy_h > 0:
|
|
187
|
+
crop_f32(
|
|
188
|
+
image._buffer._handle,
|
|
189
|
+
dst_buffer._buffer._handle,
|
|
190
|
+
image.width, dst_buffer.width,
|
|
191
|
+
intersect_left, intersect_top,
|
|
192
|
+
intersect_left - crop_left, intersect_top - crop_top,
|
|
193
|
+
copy_w, copy_h
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return dst_buffer if return_buffer else None
|
pyimagecuda/utils.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .image import ImageBase
|
|
2
|
+
|
|
3
|
+
def ensure_capacity(buffer: ImageBase, required_width: int, required_height: int) -> None:
|
|
4
|
+
"""
|
|
5
|
+
Checks if the buffer has enough capacity and updates its logical dimensions.
|
|
6
|
+
Raises ValueError if capacity is insufficient.
|
|
7
|
+
"""
|
|
8
|
+
max_w, max_h = buffer.get_max_capacity()
|
|
9
|
+
|
|
10
|
+
if required_width > max_w or required_height > max_h:
|
|
11
|
+
raise ValueError(
|
|
12
|
+
f"Buffer capacity too small: need {required_width}×{required_height}, "
|
|
13
|
+
f"got capacity {max_w}×{max_h}"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
buffer.width = required_width
|
|
17
|
+
buffer.height = required_height
|