pyimagecuda 0.1.0__cp311-cp311-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/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,199 @@
1
+ from .image import Image
2
+ from .io import copy
3
+ from .pyimagecuda_internal import ( #type: ignore
4
+ gaussian_blur_separable_f32,
5
+ sharpen_f32,
6
+ sepia_f32,
7
+ invert_f32,
8
+ threshold_f32,
9
+ solarize_f32,
10
+ filter_sobel_f32,
11
+ filter_emboss_f32
12
+ )
13
+
14
+
15
+ class Filter:
16
+
17
+ @staticmethod
18
+ def gaussian_blur(
19
+ src: Image,
20
+ radius: int = 3,
21
+ sigma: float | None = None,
22
+ dst_buffer: Image | None = None,
23
+ temp_buffer: Image | None = None
24
+ ) -> Image | None:
25
+ """
26
+ Applies a Gaussian blur to the image (returns new image or writes to buffer).
27
+
28
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#gaussian-blur
29
+ """
30
+
31
+ if radius == 0 or (sigma is not None and sigma <= 0.001):
32
+ if dst_buffer is None:
33
+ dst_buffer = Image(src.width, src.height)
34
+ copy(dst_buffer, src)
35
+ return dst_buffer
36
+ else:
37
+ copy(dst_buffer, src)
38
+ return None
39
+
40
+ if sigma is None:
41
+ sigma = radius / 3.0
42
+
43
+ if dst_buffer is None:
44
+ dst_buffer = Image(src.width, src.height)
45
+ return_dst = True
46
+ else:
47
+ dst_buffer.resize(src.width, src.height)
48
+ return_dst = False
49
+
50
+ if temp_buffer is None:
51
+ temp_buffer = Image(src.width, src.height)
52
+ owns_temp = True
53
+ else:
54
+ temp_buffer.resize(src.width, src.height)
55
+ owns_temp = False
56
+
57
+ gaussian_blur_separable_f32(
58
+ src._buffer._handle,
59
+ temp_buffer._buffer._handle,
60
+ dst_buffer._buffer._handle,
61
+ src.width,
62
+ src.height,
63
+ radius,
64
+ float(sigma)
65
+ )
66
+
67
+ if owns_temp:
68
+ temp_buffer.free()
69
+
70
+ return dst_buffer if return_dst else None
71
+
72
+ @staticmethod
73
+ def sharpen(
74
+ src: Image,
75
+ strength: float = 1.0,
76
+ dst_buffer: Image | None = None
77
+ ) -> Image | None:
78
+ """
79
+ Sharpens the image (returns new image or writes to buffer).
80
+
81
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#sharpen
82
+ """
83
+
84
+ if abs(strength) < 1e-6:
85
+ if dst_buffer is None:
86
+ dst_buffer = Image(src.width, src.height)
87
+ copy(dst_buffer, src)
88
+ return dst_buffer
89
+ else:
90
+ copy(dst_buffer, src)
91
+ return None
92
+
93
+ if dst_buffer is None:
94
+ dst_buffer = Image(src.width, src.height)
95
+ return_buffer = True
96
+ else:
97
+ dst_buffer.resize(src.width, src.height)
98
+ return_buffer = False
99
+
100
+ sharpen_f32(
101
+ src._buffer._handle,
102
+ dst_buffer._buffer._handle,
103
+ src.width,
104
+ src.height,
105
+ float(strength)
106
+ )
107
+
108
+ return dst_buffer if return_buffer else None
109
+
110
+ @staticmethod
111
+ def sepia(image: Image, intensity: float = 1.0) -> None:
112
+ """
113
+ Applies Sepia tone (in-place).
114
+
115
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#sepia
116
+ """
117
+ if abs(intensity) < 1e-6:
118
+ return
119
+
120
+ sepia_f32(image._buffer._handle, image.width, image.height, float(intensity))
121
+
122
+ @staticmethod
123
+ def invert(image: Image) -> None:
124
+ """
125
+ Inverts colors (Negative effect) in-place.
126
+
127
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#invert
128
+ """
129
+ invert_f32(image._buffer._handle, image.width, image.height)
130
+
131
+ @staticmethod
132
+ def threshold(image: Image, value: float = 0.5) -> None:
133
+ """
134
+ Converts to pure Black & White based on luminance threshold.
135
+ value: 0.0 to 1.0. Pixels brighter than value become white, others black.
136
+
137
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#threshold
138
+ """
139
+ threshold_f32(image._buffer._handle, image.width, image.height, float(value))
140
+
141
+ @staticmethod
142
+ def solarize(image: Image, threshold: float = 0.5) -> None:
143
+ """
144
+ Inverts only pixels brighter than threshold. Creates a psychedelic/retro look.
145
+
146
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#solarize
147
+ """
148
+ solarize_f32(image._buffer._handle, image.width, image.height, float(threshold))
149
+
150
+ @staticmethod
151
+ def sobel(src: Image, dst_buffer: Image | None = None) -> Image | None:
152
+ """
153
+ Detects edges using Sobel operator. Returns a black & white image with edges.
154
+
155
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#sobel
156
+ """
157
+ if dst_buffer is None:
158
+ dst_buffer = Image(src.width, src.height)
159
+ return_buffer = True
160
+ else:
161
+ dst_buffer.resize(src.width, src.height)
162
+ return_buffer = False
163
+
164
+ filter_sobel_f32(
165
+ src._buffer._handle,
166
+ dst_buffer._buffer._handle,
167
+ src.width, src.height
168
+ )
169
+ return dst_buffer if return_buffer else None
170
+
171
+ @staticmethod
172
+ def emboss(src: Image, strength: float = 1.0, dst_buffer: Image | None = None) -> Image | None:
173
+ """
174
+ Applies Emboss (Relief) effect.
175
+
176
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#emboss
177
+ """
178
+ if abs(strength) < 1e-6:
179
+ if dst_buffer is None:
180
+ dst_buffer = Image(src.width, src.height)
181
+ copy(dst_buffer, src)
182
+ return dst_buffer
183
+ else:
184
+ copy(dst_buffer, src)
185
+ return None
186
+
187
+ if dst_buffer is None:
188
+ dst_buffer = Image(src.width, src.height)
189
+ return_buffer = True
190
+ else:
191
+ dst_buffer.resize(src.width, src.height)
192
+ return_buffer = False
193
+
194
+ filter_emboss_f32(
195
+ src._buffer._handle,
196
+ dst_buffer._buffer._handle,
197
+ src.width, src.height, float(strength)
198
+ )
199
+ return dst_buffer if return_buffer else None
@@ -0,0 +1,84 @@
1
+ from .pyimagecuda_internal import ( # type: ignore
2
+ register_gl_pbo,
3
+ unregister_gl_resource,
4
+ copy_to_gl_pbo
5
+ )
6
+ from .image import ImageU8
7
+
8
+
9
+ class GLResource:
10
+ """
11
+ CUDA-OpenGL interop resource for direct GPU-to-GPU transfers.
12
+
13
+ Represents a registered OpenGL PBO that can receive ImageU8 data
14
+ directly without CPU roundtrips.
15
+
16
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/opengl/
17
+ """
18
+
19
+ def __init__(self, pbo_id: int):
20
+ """
21
+ Parameters:
22
+ pbo_id: Valid OpenGL PBO ID. The PBO must exist and remain
23
+ valid for the lifetime of this GLResource.
24
+
25
+ Raises:
26
+ ValueError: If pbo_id is invalid
27
+ RuntimeError: If CUDA registration fails
28
+
29
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/opengl/
30
+ """
31
+ if not isinstance(pbo_id, int) or pbo_id <= 0:
32
+ raise ValueError(f"Invalid PBO ID: {pbo_id}")
33
+
34
+ self._handle = None
35
+ self.pbo_id = pbo_id
36
+ self._handle = register_gl_pbo(pbo_id)
37
+
38
+ def copy_from(self, image: ImageU8, sync: bool = True) -> None:
39
+ """
40
+ Copies ImageU8 data directly to the registered PBO (GPU→GPU).
41
+
42
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/opengl/
43
+ """
44
+ if not isinstance(image, ImageU8):
45
+ raise TypeError(f"Expected ImageU8, got {type(image).__name__}")
46
+
47
+ if self._handle is None:
48
+ raise RuntimeError("GLResource has been freed")
49
+
50
+ copy_to_gl_pbo(
51
+ image._buffer._handle,
52
+ self._handle,
53
+ image.width,
54
+ image.height,
55
+ sync
56
+ )
57
+
58
+ def free(self) -> None:
59
+ """
60
+ Unregisters the OpenGL resource from CUDA.
61
+
62
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/opengl/
63
+ """
64
+ if self._handle is not None:
65
+ unregister_gl_resource(self._handle)
66
+ self._handle = None
67
+
68
+ def __enter__(self):
69
+ return self
70
+
71
+ def __exit__(self, exc_type, exc_value, traceback):
72
+ self.free()
73
+ return False
74
+
75
+ def __del__(self):
76
+ if hasattr(self, '_handle') and self._handle is not None:
77
+ try:
78
+ self.free()
79
+ except Exception:
80
+ pass
81
+
82
+ def __repr__(self) -> str:
83
+ status = "active" if getattr(self, '_handle', None) is not None else "freed"
84
+ return f"GLResource(pbo_id={self.pbo_id}, status={status})"
pyimagecuda/image.py ADDED
@@ -0,0 +1,121 @@
1
+ from .pyimagecuda_internal import create_buffer_f32, free_buffer, create_buffer_u8 #type: ignore
2
+
3
+ class Buffer:
4
+
5
+ def __init__(self, width: int, height: int, is_u8: bool = False):
6
+ create_func = create_buffer_u8 if is_u8 else create_buffer_f32
7
+ self._handle = create_func(width, height)
8
+ self.capacity_pixels = width * height
9
+
10
+ def free(self) -> None:
11
+ free_buffer(self._handle)
12
+
13
+
14
+ class ImageBase:
15
+
16
+ def __init__(self, width: int, height: int, is_u8: bool = False):
17
+ self._buffer = Buffer(width, height, is_u8)
18
+ self._width = width
19
+ self._height = height
20
+
21
+ @property
22
+ def width(self) -> int:
23
+ return self._width
24
+
25
+ @width.setter
26
+ def width(self, value: int) -> None:
27
+ value = int(value)
28
+ if value <= 0:
29
+ raise ValueError(f"Width must be positive, got {value}")
30
+
31
+ if value * self._height > self._buffer.capacity_pixels:
32
+ raise ValueError(
33
+ f"Dimensions {value}×{self._height} exceed buffer capacity "
34
+ f"({self._buffer.capacity_pixels:,} pixels)"
35
+ )
36
+
37
+ self._width = value
38
+
39
+ @property
40
+ def height(self) -> int:
41
+ return self._height
42
+
43
+ @height.setter
44
+ def height(self, value: int) -> None:
45
+ value = int(value)
46
+ if value <= 0:
47
+ raise ValueError(f"Height must be positive, got {value}")
48
+
49
+ if self._width * value > self._buffer.capacity_pixels:
50
+ raise ValueError(
51
+ f"Dimensions {self._width}×{value} exceed buffer capacity "
52
+ f"({self._buffer.capacity_pixels:,} pixels)"
53
+ )
54
+
55
+ self._height = value
56
+
57
+ def resize(self, width: int, height: int) -> None:
58
+ """
59
+ Sets both width and height atomically with proper validation.
60
+
61
+ Recommended when changing aspect ratio to avoid temporary invalid states.
62
+
63
+ Example:
64
+ img = Image(1920, 1080)
65
+ img.resize(3840, 540) # Changes aspect ratio safely
66
+
67
+ Note: This not change image data, only dimensions.
68
+ """
69
+ width = int(width)
70
+ height = int(height)
71
+
72
+ if width <= 0 or height <= 0:
73
+ raise ValueError(f"Dimensions must be positive, got {width}×{height}")
74
+
75
+ total_pixels = width * height
76
+ if total_pixels > self._buffer.capacity_pixels:
77
+ raise ValueError(
78
+ f"Dimensions {width}×{height} ({total_pixels:,} pixels) "
79
+ f"exceed buffer capacity ({self._buffer.capacity_pixels:,} pixels)"
80
+ )
81
+
82
+ self._width = width
83
+ self._height = height
84
+
85
+ def get_max_capacity(self) -> int:
86
+ """Returns the total pixel capacity of the buffer."""
87
+ return self._buffer.capacity_pixels
88
+
89
+ def free(self) -> None:
90
+ self._buffer.free()
91
+
92
+ def __enter__(self):
93
+ return self
94
+
95
+ def __exit__(self, exc_type, exc_value, traceback):
96
+ self.free()
97
+ return False
98
+
99
+ def __repr__(self) -> str:
100
+ return f"{self.__class__.__name__}({self.width}×{self.height})"
101
+
102
+ class Image(ImageBase):
103
+
104
+ def __init__(self, width: int, height: int):
105
+ """
106
+ Creates a floating-point image with the given width and height.
107
+
108
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/image/#image-float32-precision
109
+ """
110
+ super().__init__(width, height, is_u8=False)
111
+
112
+
113
+ class ImageU8(ImageBase):
114
+
115
+ def __init__(self, width: int, height: int):
116
+ """
117
+ Creates an 8-bit unsigned integer image with the given width and height.
118
+
119
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/image/#imageu8-8-bit-precision
120
+ """
121
+ super().__init__(width, height, is_u8=True)