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/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 #type: ignore
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
- ensure_capacity(dst, src.width, src.height)
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
- ensure_capacity(dst, src.width, src.height)
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
- ensure_capacity(dst, src.width, src.height)
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
- ensure_capacity(f32_buffer, width, height)
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
- ensure_capacity(u8_buffer, width, height)
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 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)
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
- image.width,
139
- image.height,
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))
pyimagecuda/resize.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from .image import Image
2
- from .utils import ensure_capacity
3
- from .pyimagecuda_internal import resize_f32 #type: ignore
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
- ensure_capacity(dst_buffer, width, height)
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
- ensure_capacity(dst_buffer, image.width, image.height)
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: norm_angle += 360
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
- ensure_capacity(dst_buffer, image.width, image.height)
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: is_fixed = True; fixed_mode = 0
84
- elif abs(norm_angle - 180) < 0.01: is_fixed = True; fixed_mode = 1
85
- elif abs(norm_angle - 270) < 0.01: is_fixed = True; fixed_mode = 2
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, rot_h = image.width, image.height
108
+ rot_w = image.width
109
+ rot_h = image.height
90
110
  else:
91
- rot_w, rot_h = image.height, image.width
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
- ensure_capacity(dst_buffer, final_w, final_h)
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
- ensure_capacity(dst_buffer, width, height)
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, img_bottom = image.width, image.height
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.4
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.4
10
+ # PyImageCUDA 0.1.0
11
11
 
12
12
  [![PyPI version](https://img.shields.io/pypi/v/pyimagecuda.svg)](https://pypi.org/project/pyimagecuda/)
13
13
  [![Build Status](https://github.com/offerrall/pyimagecuda/actions/workflows/build.yml/badge.svg)](https://github.com/offerrall/pyimagecuda/actions)
14
14
  ![Python](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13-blue)
15
+ ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux-brightgreen)
16
+ ![Tests](https://img.shields.io/badge/tests-85%20passed-brightgreen)
17
+ [![NVIDIA](https://img.shields.io/badge/NVIDIA-CUDA-76B900?style=flat&logo=nvidia&logoColor=white)](https://developer.nvidia.com/cuda-zone)
15
18
 
16
19
  **GPU-accelerated image compositing for Python.**
17
20
 
18
- > PyImageCUDA focuses on creative image generation rather than computer vision. Expect GPU-accelerated effects for design workflows—blending modes, shadows, gradients, filters... not edge detection or object recognition.
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 **<0.5 MB**.
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:** Automatically installs `pyvips` binary dependencies for robust image format support (JPG, PNG, WEBP, HEIC).
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
- * [Adjust](https://offerrall.github.io/pyimagecuda/adjust/) (Brightness, Contrast, Saturation, Gamma, Opacity)
76
- * [Transform](https://offerrall.github.io/pyimagecuda/transform/) (Flip, Rotate, Crop)
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
- * [Fill](https://offerrall.github.io/pyimagecuda/fill/) (Solid colors, Gradients, Checkerboard, Grid, Stripes, Dots, Circle, Ngon, Noise, Perlin)
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:** Windows 10 or 11 (64-bit). *Linux support coming soon.*
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