pyimagecuda 0.0.4__cp312-cp312-win_amd64.whl → 0.1.0__cp312-cp312-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 +5 -2
- pyimagecuda/adjust.py +14 -0
- pyimagecuda/blend.py +21 -0
- pyimagecuda/effect.py +89 -37
- pyimagecuda/filter.py +45 -14
- pyimagecuda/gl_interop.py +84 -0
- pyimagecuda/image.py +40 -14
- pyimagecuda/io.py +126 -25
- pyimagecuda/pyimagecuda_internal.cp312-win_amd64.pyd +0 -0
- pyimagecuda/resize.py +14 -3
- pyimagecuda/text.py +106 -0
- pyimagecuda/transform.py +103 -14
- {pyimagecuda-0.0.4.dist-info → pyimagecuda-0.1.0.dist-info}/METADATA +67 -12
- pyimagecuda-0.1.0.dist-info/RECORD +17 -0
- pyimagecuda/utils.py +0 -17
- pyimagecuda-0.0.4.dist-info/RECORD +0 -16
- {pyimagecuda-0.0.4.dist-info → pyimagecuda-0.1.0.dist-info}/WHEEL +0 -0
- {pyimagecuda-0.0.4.dist-info → pyimagecuda-0.1.0.dist-info}/licenses/LICENSE +0 -0
pyimagecuda/io.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import pyvips
|
|
2
2
|
|
|
3
|
-
from .pyimagecuda_internal import upload_to_buffer, convert_f32_to_u8, convert_u8_to_f32, download_from_buffer, copy_buffer
|
|
3
|
+
from .pyimagecuda_internal import upload_to_buffer, convert_f32_to_u8, convert_u8_to_f32, download_from_buffer, copy_buffer #type: ignore
|
|
4
4
|
from .image import Image, ImageU8, ImageBase
|
|
5
|
-
from .utils import ensure_capacity
|
|
6
5
|
|
|
6
|
+
try:
|
|
7
|
+
import numpy as np
|
|
8
|
+
except ImportError:
|
|
9
|
+
np = None
|
|
7
10
|
|
|
8
11
|
def upload(image: ImageBase, data: bytes | bytearray | memoryview) -> None:
|
|
9
12
|
"""
|
|
@@ -36,7 +39,7 @@ def copy(dst: ImageBase, src: ImageBase) -> None:
|
|
|
36
39
|
|
|
37
40
|
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#copy-between-buffers
|
|
38
41
|
"""
|
|
39
|
-
|
|
42
|
+
dst.resize(src.width, src.height)
|
|
40
43
|
copy_buffer(dst._buffer._handle, src._buffer._handle, src.width, src.height)
|
|
41
44
|
|
|
42
45
|
|
|
@@ -46,7 +49,7 @@ def convert_float_to_u8(dst: ImageU8, src: Image) -> None:
|
|
|
46
49
|
|
|
47
50
|
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#manual-conversions
|
|
48
51
|
"""
|
|
49
|
-
|
|
52
|
+
dst.resize(src.width, src.height)
|
|
50
53
|
convert_f32_to_u8(dst._buffer._handle, src._buffer._handle, src.width, src.height)
|
|
51
54
|
|
|
52
55
|
|
|
@@ -56,7 +59,7 @@ def convert_u8_to_float(dst: Image, src: ImageU8) -> None:
|
|
|
56
59
|
|
|
57
60
|
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#manual-conversions
|
|
58
61
|
"""
|
|
59
|
-
|
|
62
|
+
dst.resize(src.width, src.height)
|
|
60
63
|
convert_u8_to_f32(dst._buffer._handle, src._buffer._handle, src.width, src.height)
|
|
61
64
|
|
|
62
65
|
|
|
@@ -94,14 +97,14 @@ def load(
|
|
|
94
97
|
f32_buffer = Image(width, height)
|
|
95
98
|
should_return = True
|
|
96
99
|
else:
|
|
97
|
-
|
|
100
|
+
f32_buffer.resize(width, height)
|
|
98
101
|
should_return = False
|
|
99
102
|
|
|
100
103
|
if u8_buffer is None:
|
|
101
104
|
u8_buffer = ImageU8(width, height)
|
|
102
105
|
owns_u8 = True
|
|
103
106
|
else:
|
|
104
|
-
|
|
107
|
+
u8_buffer.resize(width, height)
|
|
105
108
|
owns_u8 = False
|
|
106
109
|
|
|
107
110
|
vips_img = vips_img.cast('uchar')
|
|
@@ -117,26 +120,13 @@ def load(
|
|
|
117
120
|
return f32_buffer if should_return else None
|
|
118
121
|
|
|
119
122
|
|
|
120
|
-
def
|
|
121
|
-
|
|
122
|
-
Saves the floating-point image to a file (using an 8-bit buffer for conversion).
|
|
123
|
-
|
|
124
|
-
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#saving-images
|
|
125
|
-
"""
|
|
126
|
-
if u8_buffer is None:
|
|
127
|
-
u8_buffer = ImageU8(image.width, image.height)
|
|
128
|
-
owns_buffer = True
|
|
129
|
-
else:
|
|
130
|
-
ensure_capacity(u8_buffer, image.width, image.height)
|
|
131
|
-
owns_buffer = False
|
|
132
|
-
|
|
133
|
-
convert_float_to_u8(u8_buffer, image)
|
|
134
|
-
pixel_data = download(u8_buffer)
|
|
123
|
+
def _save_internal(u8_image: ImageU8, filepath: str, quality: int | None = None) -> None:
|
|
124
|
+
pixel_data = download(u8_image)
|
|
135
125
|
|
|
136
126
|
vips_img = pyvips.Image.new_from_memory(
|
|
137
127
|
pixel_data,
|
|
138
|
-
|
|
139
|
-
|
|
128
|
+
u8_image.width,
|
|
129
|
+
u8_image.height,
|
|
140
130
|
bands=4,
|
|
141
131
|
format='uchar'
|
|
142
132
|
)
|
|
@@ -153,6 +143,117 @@ def save(image: Image, filepath: str, u8_buffer: ImageU8 | None = None, quality:
|
|
|
153
143
|
save_kwargs['Q'] = quality
|
|
154
144
|
|
|
155
145
|
vips_img.write_to_file(filepath, **save_kwargs)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def save(image: Image, filepath: str, u8_buffer: ImageU8 | None = None, quality: int | None = None) -> None:
|
|
149
|
+
"""
|
|
150
|
+
Saves the floating-point image to a file (using an 8-bit buffer for conversion).
|
|
151
|
+
|
|
152
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#saving-images
|
|
153
|
+
"""
|
|
154
|
+
if u8_buffer is None:
|
|
155
|
+
u8_buffer = ImageU8(image.width, image.height)
|
|
156
|
+
owns_buffer = True
|
|
157
|
+
else:
|
|
158
|
+
u8_buffer.resize(image.width, image.height)
|
|
159
|
+
owns_buffer = False
|
|
160
|
+
|
|
161
|
+
convert_float_to_u8(u8_buffer, image)
|
|
162
|
+
_save_internal(u8_buffer, filepath, quality)
|
|
156
163
|
|
|
157
164
|
if owns_buffer:
|
|
158
|
-
u8_buffer.free()
|
|
165
|
+
u8_buffer.free()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def save_u8(image: ImageU8, filepath: str, quality: int | None = None) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Saves an 8-bit unsigned integer image directly to a file.
|
|
171
|
+
|
|
172
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#saving-images
|
|
173
|
+
"""
|
|
174
|
+
_save_internal(image, filepath, quality)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def from_numpy(array, f32_buffer: Image | None = None, u8_buffer: ImageU8 | None = None) -> Image:
|
|
178
|
+
"""
|
|
179
|
+
Creates a PyImageCUDA Image from a NumPy array (e.g. from OpenCV, Pillow, Matplotlib).
|
|
180
|
+
|
|
181
|
+
- Handles uint8 (0-255) -> float32 (0.0-1.0) conversion automatically on GPU.
|
|
182
|
+
- Handles Grayscale/RGB -> RGBA expansion automatically.
|
|
183
|
+
- Optimized: Uploads uint8 data (4x smaller) if possible, then converts on GPU.
|
|
184
|
+
|
|
185
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#numpy-integration
|
|
186
|
+
"""
|
|
187
|
+
if np is None:
|
|
188
|
+
raise ImportError("NumPy is not installed. Run `pip install numpy` to use this feature.")
|
|
189
|
+
|
|
190
|
+
if not isinstance(array, np.ndarray):
|
|
191
|
+
raise TypeError(f"Expected numpy.ndarray, got {type(array)}")
|
|
192
|
+
|
|
193
|
+
target_dtype = array.dtype
|
|
194
|
+
|
|
195
|
+
if array.ndim == 2:
|
|
196
|
+
h, w = array.shape
|
|
197
|
+
alpha_val = 255 if target_dtype == np.uint8 else 1.0
|
|
198
|
+
alpha_channel = np.full((h, w), alpha_val, dtype=target_dtype)
|
|
199
|
+
array = np.dstack((array, array, array, alpha_channel))
|
|
200
|
+
|
|
201
|
+
elif array.ndim == 3:
|
|
202
|
+
h, w, c = array.shape
|
|
203
|
+
if c == 3:
|
|
204
|
+
alpha_val = 255 if target_dtype == np.uint8 else 1.0
|
|
205
|
+
alpha_channel = np.full((h, w), alpha_val, dtype=target_dtype)
|
|
206
|
+
array = np.dstack((array, alpha_channel))
|
|
207
|
+
elif c != 4:
|
|
208
|
+
raise ValueError(f"Unsupported channel count: {c}. PyImageCUDA requires 1, 3, or 4 channels.")
|
|
209
|
+
else:
|
|
210
|
+
raise ValueError(f"Unsupported array shape: {array.shape}. Expected (H, W), (H, W, 3) or (H, W, 4).")
|
|
211
|
+
|
|
212
|
+
if not array.flags['C_CONTIGUOUS']:
|
|
213
|
+
array = np.ascontiguousarray(array)
|
|
214
|
+
|
|
215
|
+
height, width = array.shape[:2]
|
|
216
|
+
|
|
217
|
+
should_return = False
|
|
218
|
+
if f32_buffer is None:
|
|
219
|
+
f32_buffer = Image(width, height)
|
|
220
|
+
should_return = True
|
|
221
|
+
else:
|
|
222
|
+
f32_buffer.resize(width, height)
|
|
223
|
+
|
|
224
|
+
if array.dtype == np.uint8:
|
|
225
|
+
owns_u8 = False
|
|
226
|
+
if u8_buffer is None:
|
|
227
|
+
u8_buffer = ImageU8(width, height)
|
|
228
|
+
owns_u8 = True
|
|
229
|
+
else:
|
|
230
|
+
u8_buffer.resize(width, height)
|
|
231
|
+
|
|
232
|
+
upload(u8_buffer, array.tobytes())
|
|
233
|
+
convert_u8_to_float(f32_buffer, u8_buffer)
|
|
234
|
+
|
|
235
|
+
if owns_u8:
|
|
236
|
+
u8_buffer.free()
|
|
237
|
+
|
|
238
|
+
elif array.dtype == np.float32:
|
|
239
|
+
upload(f32_buffer, array.tobytes())
|
|
240
|
+
else:
|
|
241
|
+
array = array.astype(np.float32)
|
|
242
|
+
upload(f32_buffer, array.tobytes())
|
|
243
|
+
|
|
244
|
+
return f32_buffer if should_return else None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def to_numpy(image: Image) -> 'np.ndarray': # type: ignore
|
|
248
|
+
"""
|
|
249
|
+
Downloads a PyImageCUDA Image to a NumPy array.
|
|
250
|
+
|
|
251
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#numpy-integration
|
|
252
|
+
"""
|
|
253
|
+
if np is None:
|
|
254
|
+
raise ImportError("NumPy is not installed. Run `pip install numpy` to use this feature.")
|
|
255
|
+
|
|
256
|
+
raw_bytes = download(image)
|
|
257
|
+
array = np.frombuffer(raw_bytes, dtype=np.float32)
|
|
258
|
+
|
|
259
|
+
return array.reshape((image.height, image.width, 4))
|
|
Binary file
|
pyimagecuda/resize.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from .image import Image
|
|
2
|
-
from .
|
|
3
|
-
from .
|
|
2
|
+
from .pyimagecuda_internal import resize_f32 #type: ignore
|
|
3
|
+
from .io import copy
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def _resize_internal(
|
|
@@ -18,11 +18,22 @@ def _resize_internal(
|
|
|
18
18
|
elif height is None:
|
|
19
19
|
height = int(src.height * (width / src.width))
|
|
20
20
|
|
|
21
|
+
# No resize needed, just copy
|
|
22
|
+
if width == src.width and height == src.height:
|
|
23
|
+
if dst_buffer is None:
|
|
24
|
+
dst_buffer = Image(width, height)
|
|
25
|
+
copy(dst_buffer, src)
|
|
26
|
+
return dst_buffer
|
|
27
|
+
else:
|
|
28
|
+
copy(dst_buffer, src)
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
# Resize normal
|
|
21
32
|
if dst_buffer is None:
|
|
22
33
|
dst_buffer = Image(width, height)
|
|
23
34
|
return_buffer = True
|
|
24
35
|
else:
|
|
25
|
-
|
|
36
|
+
dst_buffer.resize(width, height)
|
|
26
37
|
return_buffer = False
|
|
27
38
|
|
|
28
39
|
resize_f32(
|
pyimagecuda/text.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
|
|
7
|
+
class Text:
|
|
8
|
+
@staticmethod
|
|
9
|
+
def create(
|
|
10
|
+
text: str,
|
|
11
|
+
font: str = "Sans",
|
|
12
|
+
size: float = 12.0,
|
|
13
|
+
color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 1.0),
|
|
14
|
+
bg_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
|
|
15
|
+
align: Literal['left', 'centre', 'right'] = 'left',
|
|
16
|
+
justify: bool = False,
|
|
17
|
+
spacing: int = 0,
|
|
18
|
+
letter_spacing: float = 0.0,
|
|
19
|
+
dst_buffer: Image | None = None,
|
|
20
|
+
u8_buffer: ImageU8 | None = None
|
|
21
|
+
) -> Image | None:
|
|
22
|
+
"""
|
|
23
|
+
Renders text into an image with specified font, size, color, alignment, and spacing.
|
|
24
|
+
|
|
25
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/text/#text-rendering
|
|
26
|
+
"""
|
|
27
|
+
full_font_string = f"{font} {size}"
|
|
28
|
+
|
|
29
|
+
text_opts = {
|
|
30
|
+
'font': full_font_string,
|
|
31
|
+
'dpi': 72,
|
|
32
|
+
'align': 0 if align == 'left' else (1 if align == 'centre' else 2),
|
|
33
|
+
'justify': justify,
|
|
34
|
+
'spacing': spacing
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
final_text = text
|
|
38
|
+
if letter_spacing != 0:
|
|
39
|
+
pango_spacing = int(letter_spacing * 1024)
|
|
40
|
+
final_text = f'<span letter_spacing="{pango_spacing}">{text}</span>'
|
|
41
|
+
text_opts['rgba'] = True
|
|
42
|
+
|
|
43
|
+
if '<' in text and '>' in text:
|
|
44
|
+
text_opts['rgba'] = True
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
if text_opts.get('rgba'):
|
|
48
|
+
mask_rgba = pyvips.Image.text(final_text, **text_opts)
|
|
49
|
+
if mask_rgba.bands == 4:
|
|
50
|
+
mask = mask_rgba[3]
|
|
51
|
+
else:
|
|
52
|
+
mask = mask_rgba.colourspace('b-w')[0]
|
|
53
|
+
else:
|
|
54
|
+
mask = pyvips.Image.text(final_text, **text_opts)
|
|
55
|
+
|
|
56
|
+
except pyvips.Error as e:
|
|
57
|
+
raise RuntimeError(f"Failed to render text: {e}")
|
|
58
|
+
|
|
59
|
+
fg_r, fg_g, fg_b, fg_a = [int(c * 255) for c in color]
|
|
60
|
+
bg_r, bg_g, bg_b, bg_a = [int(c * 255) for c in bg_color]
|
|
61
|
+
|
|
62
|
+
fg_rgb = mask.new_from_image([fg_r, fg_g, fg_b])
|
|
63
|
+
|
|
64
|
+
if fg_a < 255:
|
|
65
|
+
fg_alpha = (mask * (fg_a / 255.0)).cast('uchar')
|
|
66
|
+
else:
|
|
67
|
+
fg_alpha = mask
|
|
68
|
+
|
|
69
|
+
fg_layer = fg_rgb.bandjoin(fg_alpha)
|
|
70
|
+
fg_layer = fg_layer.copy(interpretation='srgb')
|
|
71
|
+
|
|
72
|
+
if bg_a > 0:
|
|
73
|
+
bg_layer = mask.new_from_image([bg_r, bg_g, bg_b, bg_a])
|
|
74
|
+
bg_layer = bg_layer.copy(interpretation='srgb')
|
|
75
|
+
final_vips = bg_layer.composite(fg_layer, 'over')
|
|
76
|
+
else:
|
|
77
|
+
final_vips = fg_layer
|
|
78
|
+
|
|
79
|
+
final_vips = final_vips.cast('uchar')
|
|
80
|
+
|
|
81
|
+
w, h = final_vips.width, final_vips.height
|
|
82
|
+
raw_bytes = final_vips.write_to_memory()
|
|
83
|
+
|
|
84
|
+
if dst_buffer is None:
|
|
85
|
+
result = Image(w, h)
|
|
86
|
+
return_result = True
|
|
87
|
+
else:
|
|
88
|
+
dst_buffer.resize(w, h)
|
|
89
|
+
result = dst_buffer
|
|
90
|
+
return_result = False
|
|
91
|
+
|
|
92
|
+
if u8_buffer is None:
|
|
93
|
+
u8_temp = ImageU8(w, h)
|
|
94
|
+
free_u8 = True
|
|
95
|
+
else:
|
|
96
|
+
u8_buffer.resize(w, h)
|
|
97
|
+
u8_temp = u8_buffer
|
|
98
|
+
free_u8 = False
|
|
99
|
+
|
|
100
|
+
upload(u8_temp, raw_bytes)
|
|
101
|
+
convert_u8_to_float(result, u8_temp)
|
|
102
|
+
|
|
103
|
+
if free_u8:
|
|
104
|
+
u8_temp.free()
|
|
105
|
+
|
|
106
|
+
return result if return_result else None
|
pyimagecuda/transform.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import math
|
|
2
2
|
from typing import Literal
|
|
3
3
|
from .image import Image
|
|
4
|
-
from .utils import ensure_capacity
|
|
5
4
|
from .fill import Fill
|
|
5
|
+
from .io import copy
|
|
6
6
|
from .pyimagecuda_internal import ( #type: ignore
|
|
7
7
|
flip_f32, crop_f32, rotate_fixed_f32,
|
|
8
|
-
rotate_arbitrary_f32, copy_buffer
|
|
8
|
+
rotate_arbitrary_f32, copy_buffer, zoom_f32
|
|
9
9
|
)
|
|
10
10
|
|
|
11
11
|
|
|
@@ -37,7 +37,7 @@ class Transform:
|
|
|
37
37
|
dst_buffer = Image(image.width, image.height)
|
|
38
38
|
return_buffer = True
|
|
39
39
|
else:
|
|
40
|
-
|
|
40
|
+
dst_buffer.resize(image.width, image.height)
|
|
41
41
|
return_buffer = False
|
|
42
42
|
|
|
43
43
|
flip_f32(
|
|
@@ -55,6 +55,7 @@ class Transform:
|
|
|
55
55
|
image: Image,
|
|
56
56
|
angle: float,
|
|
57
57
|
expand: bool = True,
|
|
58
|
+
interpolation: Literal['nearest', 'bilinear', 'bicubic', 'lanczos'] = 'bilinear',
|
|
58
59
|
dst_buffer: Image | None = None
|
|
59
60
|
) -> Image | None:
|
|
60
61
|
"""
|
|
@@ -63,8 +64,20 @@ class Transform:
|
|
|
63
64
|
Docs & Examples: https://offerrall.github.io/pyimagecuda/transform/#rotate
|
|
64
65
|
"""
|
|
65
66
|
|
|
67
|
+
interp_map = {
|
|
68
|
+
'nearest': 0,
|
|
69
|
+
'bilinear': 1,
|
|
70
|
+
'bicubic': 2,
|
|
71
|
+
'lanczos': 3
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interp_method = interp_map.get(interpolation)
|
|
75
|
+
if interp_method is None:
|
|
76
|
+
raise ValueError(f"Invalid interpolation: {interpolation}. Must be {list(interp_map.keys())}")
|
|
77
|
+
|
|
66
78
|
norm_angle = angle % 360
|
|
67
|
-
if norm_angle < 0:
|
|
79
|
+
if norm_angle < 0:
|
|
80
|
+
norm_angle += 360
|
|
68
81
|
|
|
69
82
|
is_fixed = False
|
|
70
83
|
fixed_mode = 0
|
|
@@ -74,21 +87,29 @@ class Transform:
|
|
|
74
87
|
dst_buffer = Image(image.width, image.height)
|
|
75
88
|
return_buffer = True
|
|
76
89
|
else:
|
|
77
|
-
|
|
90
|
+
dst_buffer.resize(image.width, image.height)
|
|
78
91
|
return_buffer = False
|
|
79
92
|
|
|
80
93
|
copy_buffer(dst_buffer._buffer._handle, image._buffer._handle, image.width, image.height)
|
|
81
94
|
return dst_buffer if return_buffer else None
|
|
82
95
|
|
|
83
|
-
elif abs(norm_angle - 90) < 0.01:
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
elif abs(norm_angle - 90) < 0.01:
|
|
97
|
+
is_fixed = True
|
|
98
|
+
fixed_mode = 0
|
|
99
|
+
elif abs(norm_angle - 180) < 0.01:
|
|
100
|
+
is_fixed = True
|
|
101
|
+
fixed_mode = 1
|
|
102
|
+
elif abs(norm_angle - 270) < 0.01:
|
|
103
|
+
is_fixed = True
|
|
104
|
+
fixed_mode = 2
|
|
86
105
|
|
|
87
106
|
if is_fixed:
|
|
88
107
|
if fixed_mode == 1:
|
|
89
|
-
rot_w
|
|
108
|
+
rot_w = image.width
|
|
109
|
+
rot_h = image.height
|
|
90
110
|
else:
|
|
91
|
-
rot_w
|
|
111
|
+
rot_w = image.height
|
|
112
|
+
rot_h = image.width
|
|
92
113
|
else:
|
|
93
114
|
rads = math.radians(angle)
|
|
94
115
|
sin_a = abs(math.sin(rads))
|
|
@@ -111,7 +132,7 @@ class Transform:
|
|
|
111
132
|
dst_buffer = Image(final_w, final_h)
|
|
112
133
|
return_buffer = True
|
|
113
134
|
else:
|
|
114
|
-
|
|
135
|
+
dst_buffer.resize(final_w, final_h)
|
|
115
136
|
return_buffer = False
|
|
116
137
|
|
|
117
138
|
if is_fixed:
|
|
@@ -128,7 +149,8 @@ class Transform:
|
|
|
128
149
|
dst_buffer._buffer._handle,
|
|
129
150
|
image.width, image.height,
|
|
130
151
|
final_w, final_h,
|
|
131
|
-
float(angle)
|
|
152
|
+
float(angle),
|
|
153
|
+
interp_method
|
|
132
154
|
)
|
|
133
155
|
|
|
134
156
|
return dst_buffer if return_buffer else None
|
|
@@ -150,11 +172,20 @@ class Transform:
|
|
|
150
172
|
if width <= 0 or height <= 0:
|
|
151
173
|
raise ValueError("Crop dimensions must be positive")
|
|
152
174
|
|
|
175
|
+
if x == 0 and y == 0 and width == image.width and height == image.height:
|
|
176
|
+
if dst_buffer is None:
|
|
177
|
+
dst_buffer = Image(width, height)
|
|
178
|
+
copy(dst_buffer, image)
|
|
179
|
+
return dst_buffer
|
|
180
|
+
else:
|
|
181
|
+
copy(dst_buffer, image)
|
|
182
|
+
return None
|
|
183
|
+
|
|
153
184
|
if dst_buffer is None:
|
|
154
185
|
dst_buffer = Image(width, height)
|
|
155
186
|
return_buffer = True
|
|
156
187
|
else:
|
|
157
|
-
|
|
188
|
+
dst_buffer.resize(width, height)
|
|
158
189
|
return_buffer = False
|
|
159
190
|
|
|
160
191
|
Fill.color(dst_buffer, (0.0, 0.0, 0.0, 0.0))
|
|
@@ -163,7 +194,8 @@ class Transform:
|
|
|
163
194
|
crop_top = y
|
|
164
195
|
crop_right = x + width
|
|
165
196
|
crop_bottom = y + height
|
|
166
|
-
img_right
|
|
197
|
+
img_right = image.width
|
|
198
|
+
img_bottom = image.height
|
|
167
199
|
|
|
168
200
|
intersect_left = max(crop_left, 0)
|
|
169
201
|
intersect_top = max(crop_top, 0)
|
|
@@ -183,4 +215,61 @@ class Transform:
|
|
|
183
215
|
copy_w, copy_h
|
|
184
216
|
)
|
|
185
217
|
|
|
218
|
+
return dst_buffer if return_buffer else None
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def zoom(
|
|
222
|
+
image: Image,
|
|
223
|
+
zoom_factor: float = 2.0,
|
|
224
|
+
center_x: float | None = None,
|
|
225
|
+
center_y: float | None = None,
|
|
226
|
+
interpolation: Literal['nearest', 'bilinear', 'bicubic', 'lanczos'] = 'bilinear',
|
|
227
|
+
dst_buffer: Image | None = None
|
|
228
|
+
) -> Image | None:
|
|
229
|
+
"""
|
|
230
|
+
Zoom into an image by a specified factor, centered at (center_x, center_y).
|
|
231
|
+
|
|
232
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/transform/#zoom
|
|
233
|
+
"""
|
|
234
|
+
if zoom_factor <= 0:
|
|
235
|
+
raise ValueError("Zoom factor must be positive")
|
|
236
|
+
|
|
237
|
+
if center_x is None:
|
|
238
|
+
center_x = image.width / 2.0
|
|
239
|
+
if center_y is None:
|
|
240
|
+
center_y = image.height / 2.0
|
|
241
|
+
|
|
242
|
+
center_x = max(0.0, min(float(image.width - 1), float(center_x)))
|
|
243
|
+
center_y = max(0.0, min(float(image.height - 1), float(center_y)))
|
|
244
|
+
|
|
245
|
+
interp_map = {
|
|
246
|
+
'nearest': 0,
|
|
247
|
+
'bilinear': 1,
|
|
248
|
+
'bicubic': 2,
|
|
249
|
+
'lanczos': 3
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
interp_method = interp_map.get(interpolation)
|
|
253
|
+
if interp_method is None:
|
|
254
|
+
raise ValueError(f"Invalid interpolation: {interpolation}. Must be {list(interp_map.keys())}")
|
|
255
|
+
|
|
256
|
+
if dst_buffer is None:
|
|
257
|
+
dst_buffer = Image(image.width, image.height)
|
|
258
|
+
return_buffer = True
|
|
259
|
+
else:
|
|
260
|
+
return_buffer = False
|
|
261
|
+
|
|
262
|
+
zoom_f32(
|
|
263
|
+
image._buffer._handle,
|
|
264
|
+
dst_buffer._buffer._handle,
|
|
265
|
+
image.width,
|
|
266
|
+
image.height,
|
|
267
|
+
dst_buffer.width,
|
|
268
|
+
dst_buffer.height,
|
|
269
|
+
float(zoom_factor),
|
|
270
|
+
float(center_x),
|
|
271
|
+
float(center_y),
|
|
272
|
+
interp_method
|
|
273
|
+
)
|
|
274
|
+
|
|
186
275
|
return dst_buffer if return_buffer else None
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: pyimagecuda
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: GPU-accelerated image processing library for Python
|
|
5
5
|
Author: Beltrán Offerrall
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Requires-Dist: pyvips[binary]
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
|
|
10
|
-
# PyImageCUDA 0.0
|
|
10
|
+
# PyImageCUDA 0.1.0
|
|
11
11
|
|
|
12
12
|
[](https://pypi.org/project/pyimagecuda/)
|
|
13
13
|
[](https://github.com/offerrall/pyimagecuda/actions)
|
|
14
14
|

|
|
15
|
+

|
|
16
|
+

|
|
17
|
+
[](https://developer.nvidia.com/cuda-zone)
|
|
15
18
|
|
|
16
19
|
**GPU-accelerated image compositing for Python.**
|
|
17
20
|
|
|
18
|
-
> PyImageCUDA
|
|
21
|
+
> PyImageCUDA is built for image composition, not computer vision. It provides GPU tools to create, modify, and blend images, rather than analyze or recognize objects within them.
|
|
19
22
|
|
|
20
23
|
## Quick Example
|
|
21
24
|
|
|
@@ -40,10 +43,11 @@ with Image(1024, 1024) as bg:
|
|
|
40
43
|
|
|
41
44
|
## Key Features
|
|
42
45
|
|
|
43
|
-
* ✅ **Zero Dependencies:** No CUDA Toolkit, Visual Studio, or complex compilers needed. Is Plug & Play.
|
|
44
|
-
* ✅ **Ultra-lightweight:** library weighs
|
|
46
|
+
* ✅ **Zero Dependencies:** No CUDA Toolkit, Visual Studio, or complex compilers needed. Is Plug & Play on both Windows and Linux.
|
|
47
|
+
* ✅ **Ultra-lightweight:** library weighs **~1 MB**.
|
|
45
48
|
* ✅ **Studio Quality:** 32-bit floating-point precision (float32) to prevent color banding.
|
|
46
49
|
* ✅ **Advanced Memory Control:** Reuse GPU buffers across operations and resize without reallocation—critical for video processing and batch workflows.
|
|
50
|
+
* ✅ **OpenGL Integration:** Direct GPU-to-GPU display for real-time preview widgets.
|
|
47
51
|
* ✅ **API Simplicity:** Intuitive, Pythonic API designed for ease of use.
|
|
48
52
|
|
|
49
53
|
## Use Cases
|
|
@@ -51,43 +55,94 @@ with Image(1024, 1024) as bg:
|
|
|
51
55
|
* **Generative Art:** Create thousands of unique variations in seconds.
|
|
52
56
|
* **Motion Graphics:** Process video frames or generate effects in real-time.
|
|
53
57
|
* **Image Compositing:** Complex multi-layer designs with GPU-accelerated effects.
|
|
58
|
+
* **Node Editors & Real-time Tools:** Build responsive image editors with instant preview.
|
|
54
59
|
* **Game Development:** Procedural UI assets, icons, and sprite generation.
|
|
55
60
|
* **Marketing Automation:** Mass-produce personalized graphics from templates.
|
|
56
61
|
* **Data Augmentation:** High-speed batch transformations for ML datasets.
|
|
57
62
|
|
|
63
|
+
## PyImageCUDA Ecosystem
|
|
64
|
+
|
|
65
|
+
This library is the foundation. For visual workflows:
|
|
66
|
+
|
|
67
|
+
**[PyImageCUDA Studio](https://github.com/offerrall/pyimagecuda-studio)**
|
|
68
|
+
- Node-based image compositor with real-time preview
|
|
69
|
+
- Design templates visually, automate with Python
|
|
70
|
+
- 40+ nodes: generators, effects, filters, transforms
|
|
71
|
+
- Headless batch processing API
|
|
72
|
+
```bash
|
|
73
|
+
pip install pyimagecuda-studio
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
58
80
|
## Installation
|
|
59
81
|
```bash
|
|
60
82
|
pip install pyimagecuda
|
|
61
83
|
```
|
|
62
84
|
|
|
63
|
-
**Note:**
|
|
85
|
+
**Note:** `pyvips` is the only mandatory dependency (installed automatically). It is used strictly for robust file I/O (JPG, PNG, WEBP...) and high-quality Text rendering on the CPU.
|
|
64
86
|
|
|
65
87
|
## Documentation
|
|
66
88
|
|
|
67
|
-
**⚠️ Alpha Release:** Many more features are planned and under development. If you have specific needs or bug reports, please open an issue on GitHub.
|
|
68
|
-
|
|
69
89
|
### Core Concepts
|
|
70
90
|
* [Getting Started Guide](https://offerrall.github.io/pyimagecuda/)
|
|
71
91
|
* [Image & Memory](https://offerrall.github.io/pyimagecuda/image/) (Buffer management)
|
|
72
92
|
* [IO](https://offerrall.github.io/pyimagecuda/io/) (Loading and Saving)
|
|
93
|
+
* [OpenGL Integration](https://offerrall.github.io/pyimagecuda/opengl/) (Real-time preview, zero-copy display)
|
|
73
94
|
|
|
74
95
|
### Operations
|
|
75
|
-
* [
|
|
76
|
-
* [
|
|
96
|
+
* [Fill](https://offerrall.github.io/pyimagecuda/fill/) (Solid colors, Gradients, Checkerboard, Grid, Stripes, Dots, Circle, Ngon, Noise, Perlin)
|
|
97
|
+
* [Text](https://offerrall.github.io/pyimagecuda/text/) (Rich typography, system fonts, HTML-like markup, letter spacing...)
|
|
77
98
|
* [Blend](https://offerrall.github.io/pyimagecuda/blend/) (Normal, Multiply, Screen, Add, Overlay, Soft Light, Hard Light, Mask)
|
|
78
99
|
* [Resize](https://offerrall.github.io/pyimagecuda/resize/) (Nearest, Bilinear, Bicubic, Lanczos)
|
|
100
|
+
* [Adjust](https://offerrall.github.io/pyimagecuda/adjust/) (Brightness, Contrast, Saturation, Gamma, Opacity)
|
|
101
|
+
* [Transform](https://offerrall.github.io/pyimagecuda/transform/) (Flip, Rotate, Crop, Zoom)
|
|
79
102
|
* [Filter](https://offerrall.github.io/pyimagecuda/filter/) (Gaussian Blur, Sharpen, Sepia, Invert, Threshold, Solarize, Sobel, Emboss)
|
|
80
103
|
* [Effect](https://offerrall.github.io/pyimagecuda/effect/) (Drop Shadow, Rounded Corners, Stroke, Vignette)
|
|
81
|
-
|
|
104
|
+
|
|
105
|
+
## Performance
|
|
106
|
+
|
|
107
|
+
PyImageCUDA shows significant speedups for GPU-friendly operations like blending, filtering, and transformations. Performance varies by operation complexity and workflow:
|
|
108
|
+
|
|
109
|
+
- Complex operations (blur, blend, rotate) see **10-260x improvements**
|
|
110
|
+
- Simple operations (flip, crop) see **3-20x improvements**
|
|
111
|
+
- Real-world pipelines with file I/O typically see **1.5-2.5x speedups**
|
|
112
|
+
|
|
113
|
+
Results depend on your hardware, batch size, and whether you reuse GPU buffers.
|
|
114
|
+
|
|
115
|
+
**[→ View Detailed Benchmarks](https://offerrall.github.io/pyimagecuda/benchmarks/)**
|
|
82
116
|
|
|
83
117
|
## Requirements
|
|
84
118
|
|
|
85
|
-
* **OS:**
|
|
119
|
+
* **OS:**
|
|
120
|
+
- Windows 10 or 11 (64-bit).
|
|
121
|
+
- Linux: Any modern distribution (Ubuntu, Fedora, Debian, Arch, WSL2, etc.).
|
|
86
122
|
* **GPU:** NVIDIA GPU (Maxwell architecture / GTX 900 series or newer).
|
|
87
123
|
* **Drivers:** Standard NVIDIA Drivers installed.
|
|
88
124
|
|
|
89
125
|
**NOT REQUIRED:** Visual Studio, CUDA Toolkit, or Conda.
|
|
90
126
|
|
|
127
|
+
## Linux Compatibility & Troubleshooting
|
|
128
|
+
|
|
129
|
+
PyImageCUDA is currently tested primarily on **Ubuntu LTS** releases with up-to-date NVIDIA drivers.
|
|
130
|
+
|
|
131
|
+
If you encounter the following error on Linux:
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
RuntimeError: Kernel launch failed: the provided PTX was compiled with an unsupported toolchain.
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Solution: This indicates your installed NVIDIA drivers are too old to execute the kernels included in the library. Please update your NVIDIA drivers to the latest version available for your distribution (Proprietary drivers recommended).
|
|
138
|
+
|
|
139
|
+
We are actively investigating ways to broaden compatibility for older drivers and legacy Linux distributions in future releases.
|
|
140
|
+
|
|
141
|
+
## Tests
|
|
142
|
+
```bash
|
|
143
|
+
pytest tests/tests.py
|
|
144
|
+
```
|
|
145
|
+
|
|
91
146
|
## Contributing
|
|
92
147
|
Contributions welcome! Open issues or submit PRs
|
|
93
148
|
|