pyimagecuda 0.0.5__cp310-cp310-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 +64 -0
- pyimagecuda/adjust.py +101 -0
- pyimagecuda/blend.py +273 -0
- pyimagecuda/effect.py +164 -0
- pyimagecuda/fill.py +263 -0
- pyimagecuda/filter.py +168 -0
- pyimagecuda/image.py +95 -0
- pyimagecuda/io.py +158 -0
- pyimagecuda/pyimagecuda_internal.cp310-win_amd64.pyd +0 -0
- pyimagecuda/resize.py +97 -0
- pyimagecuda/transform.py +186 -0
- pyimagecuda/utils.py +17 -0
- pyimagecuda-0.0.5.dist-info/METADATA +99 -0
- pyimagecuda-0.0.5.dist-info/RECORD +16 -0
- pyimagecuda-0.0.5.dist-info/WHEEL +5 -0
- pyimagecuda-0.0.5.dist-info/licenses/LICENSE +21 -0
pyimagecuda/fill.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
from .image import Image
|
|
3
|
+
from .pyimagecuda_internal import (fill_color_f32, #type: ignore
|
|
4
|
+
fill_gradient_f32,
|
|
5
|
+
fill_circle_f32,
|
|
6
|
+
fill_checkerboard_f32,
|
|
7
|
+
fill_grid_f32,
|
|
8
|
+
fill_stripes_f32,
|
|
9
|
+
fill_dots_f32,
|
|
10
|
+
fill_noise_f32,
|
|
11
|
+
fill_perlin_f32,
|
|
12
|
+
fill_ngon_f32
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Fill:
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def color(image: Image, rgba: tuple[float, float, float, float]) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Fills the image with a solid color (in-place).
|
|
22
|
+
|
|
23
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#solid-colors
|
|
24
|
+
"""
|
|
25
|
+
fill_color_f32(image._buffer._handle, rgba, image.width, image.height)
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def gradient(image: Image,
|
|
29
|
+
rgba1: tuple[float, float, float, float],
|
|
30
|
+
rgba2: tuple[float, float, float, float],
|
|
31
|
+
direction: Literal['horizontal', 'vertical', 'diagonal', 'radial'] = 'horizontal',
|
|
32
|
+
seamless: bool = False) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Fills the image with a gradient (in-place).
|
|
35
|
+
|
|
36
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#gradients
|
|
37
|
+
"""
|
|
38
|
+
direction_map = {
|
|
39
|
+
'horizontal': 0,
|
|
40
|
+
'vertical': 1,
|
|
41
|
+
'diagonal': 2,
|
|
42
|
+
'radial': 3
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
dir_int = direction_map.get(direction)
|
|
46
|
+
if dir_int is None:
|
|
47
|
+
raise ValueError(f"Invalid direction: {direction}. Must be one of {list(direction_map.keys())}")
|
|
48
|
+
|
|
49
|
+
fill_gradient_f32(
|
|
50
|
+
image._buffer._handle,
|
|
51
|
+
rgba1,
|
|
52
|
+
rgba2,
|
|
53
|
+
image.width,
|
|
54
|
+
image.height,
|
|
55
|
+
dir_int,
|
|
56
|
+
seamless
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def checkerboard(
|
|
61
|
+
image: Image,
|
|
62
|
+
size: int = 20,
|
|
63
|
+
color1: tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1.0),
|
|
64
|
+
color2: tuple[float, float, float, float] = (0.5, 0.5, 0.5, 1.0),
|
|
65
|
+
offset_x: int = 0,
|
|
66
|
+
offset_y: int = 0
|
|
67
|
+
) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Fills buffer with a checkerboard pattern.
|
|
70
|
+
|
|
71
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#checkerboard
|
|
72
|
+
"""
|
|
73
|
+
if size <= 0:
|
|
74
|
+
raise ValueError("Checkerboard size must be positive")
|
|
75
|
+
|
|
76
|
+
fill_checkerboard_f32(
|
|
77
|
+
image._buffer._handle,
|
|
78
|
+
image.width, image.height,
|
|
79
|
+
int(size),
|
|
80
|
+
int(offset_x), int(offset_y),
|
|
81
|
+
color1, color2
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def grid(
|
|
86
|
+
image: Image,
|
|
87
|
+
spacing: int = 50,
|
|
88
|
+
line_width: int = 1,
|
|
89
|
+
color: tuple[float, float, float, float] = (0.5, 0.5, 0.5, 1.0),
|
|
90
|
+
bg_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
|
|
91
|
+
offset_x: int = 0,
|
|
92
|
+
offset_y: int = 0
|
|
93
|
+
) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Fills buffer with a grid pattern.
|
|
96
|
+
|
|
97
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#grid
|
|
98
|
+
"""
|
|
99
|
+
if spacing <= 0:
|
|
100
|
+
raise ValueError("Grid spacing must be positive")
|
|
101
|
+
if line_width <= 0:
|
|
102
|
+
raise ValueError("Line width must be positive")
|
|
103
|
+
|
|
104
|
+
fill_grid_f32(
|
|
105
|
+
image._buffer._handle,
|
|
106
|
+
image.width, image.height,
|
|
107
|
+
int(spacing), int(line_width),
|
|
108
|
+
int(offset_x), int(offset_y),
|
|
109
|
+
color, bg_color
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def stripes(
|
|
114
|
+
image: Image,
|
|
115
|
+
angle: float = 45.0,
|
|
116
|
+
spacing: int = 40,
|
|
117
|
+
width: int = 20,
|
|
118
|
+
color1: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
|
119
|
+
color2: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
|
|
120
|
+
offset: int = 0
|
|
121
|
+
) -> None:
|
|
122
|
+
"""
|
|
123
|
+
Fills buffer with alternating stripes with Anti-Aliasing.
|
|
124
|
+
|
|
125
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#stripes
|
|
126
|
+
"""
|
|
127
|
+
if spacing <= 0:
|
|
128
|
+
raise ValueError("Stripes spacing must be positive")
|
|
129
|
+
|
|
130
|
+
fill_stripes_f32(
|
|
131
|
+
image._buffer._handle,
|
|
132
|
+
image.width, image.height,
|
|
133
|
+
float(angle), int(spacing), int(width), int(offset),
|
|
134
|
+
color1, color2
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def dots(
|
|
139
|
+
image: Image,
|
|
140
|
+
spacing: int = 40,
|
|
141
|
+
radius: float = 10.0,
|
|
142
|
+
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
|
143
|
+
bg_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
|
|
144
|
+
offset_x: int = 0,
|
|
145
|
+
offset_y: int = 0,
|
|
146
|
+
softness: float = 0.0
|
|
147
|
+
) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Fills buffer with a Polka Dot pattern.
|
|
150
|
+
- softness: 0.0 = Hard edge, 1.0 = Soft glow.
|
|
151
|
+
|
|
152
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#dots
|
|
153
|
+
"""
|
|
154
|
+
if spacing <= 0:
|
|
155
|
+
raise ValueError("Spacing must be positive")
|
|
156
|
+
|
|
157
|
+
fill_dots_f32(
|
|
158
|
+
image._buffer._handle,
|
|
159
|
+
image.width, image.height,
|
|
160
|
+
int(spacing), float(radius),
|
|
161
|
+
int(offset_x), int(offset_y), float(softness),
|
|
162
|
+
color, bg_color
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def circle(
|
|
167
|
+
image: Image,
|
|
168
|
+
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
|
169
|
+
bg_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
|
|
170
|
+
softness: float = 0.0
|
|
171
|
+
) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Fills the buffer with a centered circle fitted to the image size.
|
|
174
|
+
- softness: Edge softness. 0.0 = Hard edge (with AA), >0.0 = Soft gradient.
|
|
175
|
+
|
|
176
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#circle
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
fill_circle_f32(
|
|
180
|
+
image._buffer._handle,
|
|
181
|
+
image.width, image.height,
|
|
182
|
+
float(softness),
|
|
183
|
+
color, bg_color
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def noise(
|
|
188
|
+
image: Image,
|
|
189
|
+
seed: float = 0.0,
|
|
190
|
+
monochrome: bool = True
|
|
191
|
+
) -> None:
|
|
192
|
+
"""
|
|
193
|
+
Fills the buffer with random White Noise.
|
|
194
|
+
- seed: Random seed. Change this to animate the noise.
|
|
195
|
+
- monochrome: True for grayscale noise, False for RGB noise.
|
|
196
|
+
|
|
197
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#noise
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
fill_noise_f32(
|
|
201
|
+
image._buffer._handle,
|
|
202
|
+
image.width, image.height,
|
|
203
|
+
float(seed),
|
|
204
|
+
int(monochrome)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def perlin(
|
|
209
|
+
image: Image,
|
|
210
|
+
scale: float = 50.0,
|
|
211
|
+
seed: float = 0.0,
|
|
212
|
+
octaves: int = 1,
|
|
213
|
+
persistence: float = 0.5,
|
|
214
|
+
lacunarity: float = 2.0,
|
|
215
|
+
offset_x: float = 0.0,
|
|
216
|
+
offset_y: float = 0.0,
|
|
217
|
+
color1: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 1.0),
|
|
218
|
+
color2: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0)
|
|
219
|
+
) -> None:
|
|
220
|
+
"""
|
|
221
|
+
Fills buffer with Perlin Noise (Gradient Noise).
|
|
222
|
+
- scale: "Zoom" level. Higher values = bigger features (zoomed in).
|
|
223
|
+
- octaves: Detail layers. 1 = smooth, 6 = rocky/detailed.
|
|
224
|
+
- persistence: How much each octave contributes (0.0 to 1.0).
|
|
225
|
+
- lacunarity: Detail frequency multiplier (usually 2.0).
|
|
226
|
+
|
|
227
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#perlin-noise
|
|
228
|
+
"""
|
|
229
|
+
if scale <= 0: scale = 0.001
|
|
230
|
+
|
|
231
|
+
fill_perlin_f32(
|
|
232
|
+
image._buffer._handle,
|
|
233
|
+
image.width, image.height,
|
|
234
|
+
float(scale), float(seed),
|
|
235
|
+
int(octaves), float(persistence), float(lacunarity),
|
|
236
|
+
float(offset_x), float(offset_y),
|
|
237
|
+
color1, color2
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
@staticmethod
|
|
241
|
+
def ngon(
|
|
242
|
+
image: Image,
|
|
243
|
+
sides: int = 3,
|
|
244
|
+
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
|
245
|
+
bg_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
|
|
246
|
+
rotation: float = 0.0,
|
|
247
|
+
softness: float = 0.0
|
|
248
|
+
) -> None:
|
|
249
|
+
"""
|
|
250
|
+
Fills buffer with a Regular Polygon (Triangle, Pentagon, Hexagon...).
|
|
251
|
+
- softness: Edge softness (0.0 = Hard AA, >0.0 = Glow).
|
|
252
|
+
|
|
253
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#ngon
|
|
254
|
+
"""
|
|
255
|
+
if sides < 3:
|
|
256
|
+
raise ValueError("Polygon must have at least 3 sides")
|
|
257
|
+
|
|
258
|
+
fill_ngon_f32(
|
|
259
|
+
image._buffer._handle,
|
|
260
|
+
image.width, image.height,
|
|
261
|
+
int(sides), float(rotation), float(softness),
|
|
262
|
+
color, bg_color
|
|
263
|
+
)
|
pyimagecuda/filter.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from .image import Image
|
|
2
|
+
from .utils import ensure_capacity
|
|
3
|
+
from .pyimagecuda_internal import (gaussian_blur_separable_f32, #type: ignore
|
|
4
|
+
sharpen_f32,
|
|
5
|
+
sepia_f32,
|
|
6
|
+
invert_f32,
|
|
7
|
+
threshold_f32,
|
|
8
|
+
solarize_f32,
|
|
9
|
+
filter_sobel_f32,
|
|
10
|
+
filter_emboss_f32)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Filter:
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def gaussian_blur(
|
|
17
|
+
src: Image,
|
|
18
|
+
radius: int = 3,
|
|
19
|
+
sigma: float | None = None,
|
|
20
|
+
dst_buffer: Image | None = None,
|
|
21
|
+
temp_buffer: Image | None = None
|
|
22
|
+
) -> Image | None:
|
|
23
|
+
"""
|
|
24
|
+
Applies a Gaussian blur to the image (returns new image or writes to buffer).
|
|
25
|
+
|
|
26
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#gaussian-blur
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
if sigma is None:
|
|
30
|
+
sigma = radius / 3.0
|
|
31
|
+
|
|
32
|
+
if dst_buffer is None:
|
|
33
|
+
dst_buffer = Image(src.width, src.height)
|
|
34
|
+
return_dst = True
|
|
35
|
+
else:
|
|
36
|
+
ensure_capacity(dst_buffer, src.width, src.height)
|
|
37
|
+
return_dst = False
|
|
38
|
+
|
|
39
|
+
if temp_buffer is None:
|
|
40
|
+
temp_buffer = Image(src.width, src.height)
|
|
41
|
+
owns_temp = True
|
|
42
|
+
else:
|
|
43
|
+
ensure_capacity(temp_buffer, src.width, src.height)
|
|
44
|
+
owns_temp = False
|
|
45
|
+
|
|
46
|
+
gaussian_blur_separable_f32(
|
|
47
|
+
src._buffer._handle,
|
|
48
|
+
temp_buffer._buffer._handle,
|
|
49
|
+
dst_buffer._buffer._handle,
|
|
50
|
+
src.width,
|
|
51
|
+
src.height,
|
|
52
|
+
radius,
|
|
53
|
+
float(sigma)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if owns_temp:
|
|
57
|
+
temp_buffer.free()
|
|
58
|
+
|
|
59
|
+
return dst_buffer if return_dst else None
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def sharpen(
|
|
63
|
+
src: Image,
|
|
64
|
+
strength: float = 1.0,
|
|
65
|
+
dst_buffer: Image | None = None
|
|
66
|
+
) -> Image | None:
|
|
67
|
+
"""
|
|
68
|
+
Sharpens the image (returns new image or writes to buffer).
|
|
69
|
+
|
|
70
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#sharpen
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
if dst_buffer is None:
|
|
74
|
+
dst_buffer = Image(src.width, src.height)
|
|
75
|
+
return_buffer = True
|
|
76
|
+
else:
|
|
77
|
+
ensure_capacity(dst_buffer, src.width, src.height)
|
|
78
|
+
return_buffer = False
|
|
79
|
+
|
|
80
|
+
sharpen_f32(
|
|
81
|
+
src._buffer._handle,
|
|
82
|
+
dst_buffer._buffer._handle,
|
|
83
|
+
src.width,
|
|
84
|
+
src.height,
|
|
85
|
+
float(strength)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return dst_buffer if return_buffer else None
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def sepia(image: Image, intensity: float = 1.0) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Applies Sepia tone (in-place).
|
|
94
|
+
|
|
95
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#sepia
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
sepia_f32(image._buffer._handle, image.width, image.height, float(intensity))
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def invert(image: Image) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Inverts colors (Negative effect) in-place.
|
|
104
|
+
|
|
105
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#invert
|
|
106
|
+
"""
|
|
107
|
+
invert_f32(image._buffer._handle, image.width, image.height)
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def threshold(image: Image, value: float = 0.5) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Converts to pure Black & White based on luminance threshold.
|
|
113
|
+
value: 0.0 to 1.0. Pixels brighter than value become white, others black.
|
|
114
|
+
|
|
115
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#threshold
|
|
116
|
+
"""
|
|
117
|
+
threshold_f32(image._buffer._handle, image.width, image.height, float(value))
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def solarize(image: Image, threshold: float = 0.5) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Inverts only pixels brighter than threshold. Creates a psychedelic/retro look.
|
|
123
|
+
|
|
124
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#solarize
|
|
125
|
+
"""
|
|
126
|
+
solarize_f32(image._buffer._handle, image.width, image.height, float(threshold))
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def sobel(src: Image, dst_buffer: Image | None = None) -> Image | None:
|
|
130
|
+
"""
|
|
131
|
+
Detects edges using Sobel operator. Returns a black & white image with edges.
|
|
132
|
+
|
|
133
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#sobel
|
|
134
|
+
"""
|
|
135
|
+
if dst_buffer is None:
|
|
136
|
+
dst_buffer = Image(src.width, src.height)
|
|
137
|
+
return_buffer = True
|
|
138
|
+
else:
|
|
139
|
+
ensure_capacity(dst_buffer, src.width, src.height)
|
|
140
|
+
return_buffer = False
|
|
141
|
+
|
|
142
|
+
filter_sobel_f32(
|
|
143
|
+
src._buffer._handle,
|
|
144
|
+
dst_buffer._buffer._handle,
|
|
145
|
+
src.width, src.height
|
|
146
|
+
)
|
|
147
|
+
return dst_buffer if return_buffer else None
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def emboss(src: Image, strength: float = 1.0, dst_buffer: Image | None = None) -> Image | None:
|
|
151
|
+
"""
|
|
152
|
+
Applies Emboss (Relief) effect.
|
|
153
|
+
|
|
154
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#emboss
|
|
155
|
+
"""
|
|
156
|
+
if dst_buffer is None:
|
|
157
|
+
dst_buffer = Image(src.width, src.height)
|
|
158
|
+
return_buffer = True
|
|
159
|
+
else:
|
|
160
|
+
ensure_capacity(dst_buffer, src.width, src.height)
|
|
161
|
+
return_buffer = False
|
|
162
|
+
|
|
163
|
+
filter_emboss_f32(
|
|
164
|
+
src._buffer._handle,
|
|
165
|
+
dst_buffer._buffer._handle,
|
|
166
|
+
src.width, src.height, float(strength)
|
|
167
|
+
)
|
|
168
|
+
return dst_buffer if return_buffer else None
|
pyimagecuda/image.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from .pyimagecuda_internal import create_buffer_f32, free_buffer, create_buffer_u8 #type: ignore
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Buffer:
|
|
5
|
+
|
|
6
|
+
def __init__(self, width: int, height: int, is_u8: bool = False):
|
|
7
|
+
create_func = create_buffer_u8 if is_u8 else create_buffer_f32
|
|
8
|
+
self._handle = create_func(width, height)
|
|
9
|
+
self.capacity_width = width
|
|
10
|
+
self.capacity_height = height
|
|
11
|
+
|
|
12
|
+
def free(self) -> None:
|
|
13
|
+
free_buffer(self._handle)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ImageBase:
|
|
17
|
+
|
|
18
|
+
def __init__(self, width: int, height: int, is_u8: bool = False):
|
|
19
|
+
self._buffer = Buffer(width, height, is_u8)
|
|
20
|
+
self._width = width
|
|
21
|
+
self._height = height
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def width(self) -> int:
|
|
25
|
+
return self._width
|
|
26
|
+
|
|
27
|
+
@width.setter
|
|
28
|
+
def width(self, value: int) -> None:
|
|
29
|
+
value = int(value)
|
|
30
|
+
if value <= 0:
|
|
31
|
+
raise ValueError(f"Width must be positive, got {value}")
|
|
32
|
+
|
|
33
|
+
if value > self._buffer.capacity_width:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"Width {value} exceeds buffer capacity "
|
|
36
|
+
f"{self._buffer.capacity_width}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
self._width = value
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def height(self) -> int:
|
|
43
|
+
return self._height
|
|
44
|
+
|
|
45
|
+
@height.setter
|
|
46
|
+
def height(self, value: int) -> None:
|
|
47
|
+
value = int(value)
|
|
48
|
+
if value <= 0:
|
|
49
|
+
raise ValueError(f"Height must be positive, got {value}")
|
|
50
|
+
|
|
51
|
+
if value > self._buffer.capacity_height:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"Height {value} exceeds buffer capacity "
|
|
54
|
+
f"{self._buffer.capacity_height}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
self._height = value
|
|
58
|
+
|
|
59
|
+
def free(self) -> None:
|
|
60
|
+
self._buffer.free()
|
|
61
|
+
|
|
62
|
+
def __enter__(self):
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
66
|
+
self.free()
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
def get_max_capacity(self) -> tuple[int, int]:
|
|
70
|
+
return (self._buffer.capacity_width, self._buffer.capacity_height)
|
|
71
|
+
|
|
72
|
+
def __repr__(self) -> str:
|
|
73
|
+
return f"{self.__class__.__name__}({self.width}×{self.height})"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Image(ImageBase):
|
|
77
|
+
|
|
78
|
+
def __init__(self, width: int, height: int):
|
|
79
|
+
"""
|
|
80
|
+
Creates a floating-point image with the given width and height.
|
|
81
|
+
|
|
82
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/image/#image-float32-precision
|
|
83
|
+
"""
|
|
84
|
+
super().__init__(width, height, is_u8=False)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ImageU8(ImageBase):
|
|
88
|
+
|
|
89
|
+
def __init__(self, width: int, height: int):
|
|
90
|
+
"""
|
|
91
|
+
Creates an 8-bit unsigned integer image with the given width and height.
|
|
92
|
+
|
|
93
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/image/#imageu8-8-bit-precision
|
|
94
|
+
"""
|
|
95
|
+
super().__init__(width, height, is_u8=True)
|
pyimagecuda/io.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
|
|
8
|
+
def upload(image: ImageBase, data: bytes | bytearray | memoryview) -> None:
|
|
9
|
+
"""
|
|
10
|
+
Uploads the image data from a bytes-like object to the GPU.
|
|
11
|
+
|
|
12
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#direct-uploaddownload
|
|
13
|
+
"""
|
|
14
|
+
bytes_per_pixel = 4 if isinstance(image, ImageU8) else 16
|
|
15
|
+
expected = image.width * image.height * bytes_per_pixel
|
|
16
|
+
actual = data.nbytes if isinstance(data, memoryview) else len(data)
|
|
17
|
+
|
|
18
|
+
if actual != expected:
|
|
19
|
+
raise ValueError(f"Expected {expected} bytes, got {actual}")
|
|
20
|
+
|
|
21
|
+
upload_to_buffer(image._buffer._handle, data, image.width, image.height)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def download(image: ImageBase) -> bytes:
|
|
25
|
+
"""
|
|
26
|
+
Downloads the image data from the GPU to a bytes object.
|
|
27
|
+
|
|
28
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#direct-uploaddownload
|
|
29
|
+
"""
|
|
30
|
+
return download_from_buffer(image._buffer._handle, image.width, image.height)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def copy(dst: ImageBase, src: ImageBase) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Copies image data from the source image to the destination image.
|
|
36
|
+
|
|
37
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#copy-between-buffers
|
|
38
|
+
"""
|
|
39
|
+
ensure_capacity(dst, src.width, src.height)
|
|
40
|
+
copy_buffer(dst._buffer._handle, src._buffer._handle, src.width, src.height)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def convert_float_to_u8(dst: ImageU8, src: Image) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Converts a floating-point image to an 8-bit unsigned integer image.
|
|
46
|
+
|
|
47
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#manual-conversions
|
|
48
|
+
"""
|
|
49
|
+
ensure_capacity(dst, src.width, src.height)
|
|
50
|
+
convert_f32_to_u8(dst._buffer._handle, src._buffer._handle, src.width, src.height)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def convert_u8_to_float(dst: Image, src: ImageU8) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Converts an 8-bit unsigned integer image to a floating-point image.
|
|
56
|
+
|
|
57
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#manual-conversions
|
|
58
|
+
"""
|
|
59
|
+
ensure_capacity(dst, src.width, src.height)
|
|
60
|
+
convert_u8_to_f32(dst._buffer._handle, src._buffer._handle, src.width, src.height)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load(
|
|
64
|
+
filepath: str,
|
|
65
|
+
f32_buffer: Image | None = None,
|
|
66
|
+
u8_buffer: ImageU8 | None = None
|
|
67
|
+
) -> Image | None:
|
|
68
|
+
"""
|
|
69
|
+
Loads an image from a file (returns new image or writes to buffer).
|
|
70
|
+
|
|
71
|
+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#loading-images
|
|
72
|
+
"""
|
|
73
|
+
vips_img = pyvips.Image.new_from_file(filepath, access='sequential')
|
|
74
|
+
|
|
75
|
+
if vips_img.bands == 1:
|
|
76
|
+
vips_img = vips_img.bandjoin([vips_img, vips_img, vips_img])
|
|
77
|
+
vips_img = vips_img.bandjoin(255)
|
|
78
|
+
elif vips_img.bands == 3:
|
|
79
|
+
vips_img = vips_img.bandjoin(255)
|
|
80
|
+
elif vips_img.bands == 4:
|
|
81
|
+
pass
|
|
82
|
+
else:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
f"Unsupported image format: {vips_img.bands} channels. "
|
|
85
|
+
f"Only grayscale (1), RGB (3), and RGBA (4) are supported."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
width = vips_img.width
|
|
89
|
+
height = vips_img.height
|
|
90
|
+
|
|
91
|
+
should_return = False
|
|
92
|
+
|
|
93
|
+
if f32_buffer is None:
|
|
94
|
+
f32_buffer = Image(width, height)
|
|
95
|
+
should_return = True
|
|
96
|
+
else:
|
|
97
|
+
ensure_capacity(f32_buffer, width, height)
|
|
98
|
+
should_return = False
|
|
99
|
+
|
|
100
|
+
if u8_buffer is None:
|
|
101
|
+
u8_buffer = ImageU8(width, height)
|
|
102
|
+
owns_u8 = True
|
|
103
|
+
else:
|
|
104
|
+
ensure_capacity(u8_buffer, width, height)
|
|
105
|
+
owns_u8 = False
|
|
106
|
+
|
|
107
|
+
vips_img = vips_img.cast('uchar')
|
|
108
|
+
pixel_data = vips_img.write_to_memory()
|
|
109
|
+
|
|
110
|
+
upload(u8_buffer, pixel_data)
|
|
111
|
+
|
|
112
|
+
convert_u8_to_float(f32_buffer, u8_buffer)
|
|
113
|
+
|
|
114
|
+
if owns_u8:
|
|
115
|
+
u8_buffer.free()
|
|
116
|
+
|
|
117
|
+
return f32_buffer if should_return else None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def save(image: Image, filepath: str, u8_buffer: ImageU8 | None = None, quality: int | None = None) -> None:
|
|
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)
|
|
135
|
+
|
|
136
|
+
vips_img = pyvips.Image.new_from_memory(
|
|
137
|
+
pixel_data,
|
|
138
|
+
image.width,
|
|
139
|
+
image.height,
|
|
140
|
+
bands=4,
|
|
141
|
+
format='uchar'
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
vips_img = vips_img.copy(interpretation='srgb')
|
|
145
|
+
|
|
146
|
+
save_kwargs = {}
|
|
147
|
+
if quality is not None:
|
|
148
|
+
if filepath.lower().endswith(('.jpg', '.jpeg')):
|
|
149
|
+
save_kwargs['Q'] = quality
|
|
150
|
+
elif filepath.lower().endswith('.webp'):
|
|
151
|
+
save_kwargs['Q'] = quality
|
|
152
|
+
elif filepath.lower().endswith(('.heic', '.heif')):
|
|
153
|
+
save_kwargs['Q'] = quality
|
|
154
|
+
|
|
155
|
+
vips_img.write_to_file(filepath, **save_kwargs)
|
|
156
|
+
|
|
157
|
+
if owns_buffer:
|
|
158
|
+
u8_buffer.free()
|
|
Binary file
|