pyimagecuda 0.0.1__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyimagecuda might be problematic. Click here for more details.
- pyimagecuda/__init__.py +60 -0
- pyimagecuda/blend.py +77 -0
- pyimagecuda/effect.py +90 -0
- pyimagecuda/fill.py +36 -0
- pyimagecuda/filter.py +71 -0
- pyimagecuda/image.py +78 -0
- pyimagecuda/io.py +130 -0
- pyimagecuda/pyimagecuda_internal.cp313-win_amd64.pyd +0 -0
- pyimagecuda/resize.py +72 -0
- pyimagecuda/utils.py +9 -0
- pyimagecuda-0.0.1.dist-info/METADATA +29 -0
- pyimagecuda-0.0.1.dist-info/RECORD +13 -0
- pyimagecuda-0.0.1.dist-info/WHEEL +5 -0
pyimagecuda/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
def _check_nvidia_driver():
|
|
5
|
+
try:
|
|
6
|
+
ctypes.windll.LoadLibrary("nvcuda.dll")
|
|
7
|
+
return True
|
|
8
|
+
except OSError:
|
|
9
|
+
return False
|
|
10
|
+
|
|
11
|
+
_INTERNAL_LOADED = False
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from . import pyimagecuda_internal
|
|
15
|
+
_INTERNAL_LOADED = True
|
|
16
|
+
except ImportError as e:
|
|
17
|
+
_INTERNAL_LOADED = False
|
|
18
|
+
error_msg = str(e).lower()
|
|
19
|
+
|
|
20
|
+
if "dll load failed" in error_msg:
|
|
21
|
+
print("\n" + "!" * 75)
|
|
22
|
+
print(" [CRITICAL ERROR] Failed to load pyimagecuda backend.")
|
|
23
|
+
|
|
24
|
+
if not _check_nvidia_driver():
|
|
25
|
+
print(" CAUSE: NVIDIA Drivers are missing or not detected.")
|
|
26
|
+
print(" SOLUTION: Install latest drivers from: https://www.nvidia.com/Download/index.aspx")
|
|
27
|
+
else:
|
|
28
|
+
print(" CAUSE: Microsoft Visual C++ Redistributable is missing.")
|
|
29
|
+
print(" SOLUTION: Install it from: https://aka.ms/vs/17/release/vc_redist.x64.exe")
|
|
30
|
+
|
|
31
|
+
print("!" * 75 + "\n")
|
|
32
|
+
else:
|
|
33
|
+
print(f"Error loading internal module: {e}")
|
|
34
|
+
|
|
35
|
+
if _INTERNAL_LOADED:
|
|
36
|
+
try:
|
|
37
|
+
from .image import Image, ImageU8
|
|
38
|
+
from .io import upload, download, copy, save, load, convert_float_to_u8, convert_u8_to_float
|
|
39
|
+
from .fill import Fill
|
|
40
|
+
from .resize import Resize
|
|
41
|
+
from .blend import Blend
|
|
42
|
+
from .filter import Filter
|
|
43
|
+
from .effect import Effect
|
|
44
|
+
except ImportError as e:
|
|
45
|
+
print(f"Warning: Error importing Python wrappers: {e}")
|
|
46
|
+
|
|
47
|
+
def check_system():
|
|
48
|
+
print("--- PYIMAGECUDA DIAGNOSTIC ---")
|
|
49
|
+
|
|
50
|
+
if not _INTERNAL_LOADED:
|
|
51
|
+
print("❌ Backend C++ NOT loaded. See errors above.")
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
pyimagecuda_internal.cuda_sync()
|
|
56
|
+
print("✅ SYSTEM OK. GPU Ready & Libraries Loaded.")
|
|
57
|
+
return True
|
|
58
|
+
except Exception as e:
|
|
59
|
+
print(f"❌ Backend loaded but GPU runtime failed: {e}")
|
|
60
|
+
return False
|
pyimagecuda/blend.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from .image import Image
|
|
2
|
+
from .pyimagecuda_internal import blend_f32 #type: ignore
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Blend:
|
|
6
|
+
|
|
7
|
+
@staticmethod
|
|
8
|
+
def normal(
|
|
9
|
+
base: Image,
|
|
10
|
+
overlay: Image,
|
|
11
|
+
pos_x: int = 0,
|
|
12
|
+
pos_y: int = 0,
|
|
13
|
+
opacity: float = 1.0
|
|
14
|
+
) -> None:
|
|
15
|
+
blend_f32(
|
|
16
|
+
base._buffer._handle,
|
|
17
|
+
overlay._buffer._handle,
|
|
18
|
+
base.width, base.height,
|
|
19
|
+
overlay.width, overlay.height,
|
|
20
|
+
pos_x, pos_y,
|
|
21
|
+
0,
|
|
22
|
+
opacity
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def multiply(
|
|
27
|
+
base: Image,
|
|
28
|
+
overlay: Image,
|
|
29
|
+
pos_x: int = 0,
|
|
30
|
+
pos_y: int = 0,
|
|
31
|
+
opacity: float = 1.0
|
|
32
|
+
) -> None:
|
|
33
|
+
blend_f32(
|
|
34
|
+
base._buffer._handle,
|
|
35
|
+
overlay._buffer._handle,
|
|
36
|
+
base.width, base.height,
|
|
37
|
+
overlay.width, overlay.height,
|
|
38
|
+
pos_x, pos_y,
|
|
39
|
+
1,
|
|
40
|
+
opacity
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def screen(
|
|
45
|
+
base: Image,
|
|
46
|
+
overlay: Image,
|
|
47
|
+
pos_x: int = 0,
|
|
48
|
+
pos_y: int = 0,
|
|
49
|
+
opacity: float = 1.0
|
|
50
|
+
) -> None:
|
|
51
|
+
blend_f32(
|
|
52
|
+
base._buffer._handle,
|
|
53
|
+
overlay._buffer._handle,
|
|
54
|
+
base.width, base.height,
|
|
55
|
+
overlay.width, overlay.height,
|
|
56
|
+
pos_x, pos_y,
|
|
57
|
+
2,
|
|
58
|
+
opacity
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def add(
|
|
63
|
+
base: Image,
|
|
64
|
+
overlay: Image,
|
|
65
|
+
pos_x: int = 0,
|
|
66
|
+
pos_y: int = 0,
|
|
67
|
+
opacity: float = 1.0
|
|
68
|
+
) -> None:
|
|
69
|
+
blend_f32(
|
|
70
|
+
base._buffer._handle,
|
|
71
|
+
overlay._buffer._handle,
|
|
72
|
+
base.width, base.height,
|
|
73
|
+
overlay.width, overlay.height,
|
|
74
|
+
pos_x, pos_y,
|
|
75
|
+
3,
|
|
76
|
+
opacity
|
|
77
|
+
)
|
pyimagecuda/effect.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from .image import Image
|
|
2
|
+
from .filter import Filter
|
|
3
|
+
from .blend import Blend
|
|
4
|
+
from .fill import Fill
|
|
5
|
+
from .utils import check_dimensions_match
|
|
6
|
+
from .pyimagecuda_internal import ( #type: ignore
|
|
7
|
+
rounded_corners_f32,
|
|
8
|
+
extract_alpha_f32,
|
|
9
|
+
colorize_shadow_f32
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Effect:
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def rounded_corners(image: Image, radius: float) -> None:
|
|
17
|
+
max_radius = min(image.width, image.height) / 2.0
|
|
18
|
+
|
|
19
|
+
if radius < 0:
|
|
20
|
+
raise ValueError("Radius must be non-negative")
|
|
21
|
+
|
|
22
|
+
if radius > max_radius:
|
|
23
|
+
radius = max_radius
|
|
24
|
+
|
|
25
|
+
rounded_corners_f32(
|
|
26
|
+
image._buffer._handle,
|
|
27
|
+
image.width,
|
|
28
|
+
image.height,
|
|
29
|
+
float(radius)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def drop_shadow(
|
|
34
|
+
image: Image,
|
|
35
|
+
offset_x: int = 10,
|
|
36
|
+
offset_y: int = 10,
|
|
37
|
+
blur: int = 20,
|
|
38
|
+
color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.5),
|
|
39
|
+
dst_buffer: Image | None = None,
|
|
40
|
+
shadow_buffer: Image | None = None,
|
|
41
|
+
temp_buffer: Image | None = None
|
|
42
|
+
) -> Image | None:
|
|
43
|
+
|
|
44
|
+
if dst_buffer is None:
|
|
45
|
+
result = Image(image.width, image.height)
|
|
46
|
+
return_result = True
|
|
47
|
+
else:
|
|
48
|
+
check_dimensions_match(dst_buffer, image)
|
|
49
|
+
result = dst_buffer
|
|
50
|
+
return_result = False
|
|
51
|
+
|
|
52
|
+
if shadow_buffer is None:
|
|
53
|
+
shadow = Image(image.width, image.height)
|
|
54
|
+
owns_shadow = True
|
|
55
|
+
else:
|
|
56
|
+
check_dimensions_match(shadow_buffer, image)
|
|
57
|
+
shadow = shadow_buffer
|
|
58
|
+
owns_shadow = False
|
|
59
|
+
|
|
60
|
+
extract_alpha_f32(
|
|
61
|
+
image._buffer._handle,
|
|
62
|
+
shadow._buffer._handle,
|
|
63
|
+
image.width,
|
|
64
|
+
image.height
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if blur > 0:
|
|
68
|
+
Filter.gaussian_blur(
|
|
69
|
+
shadow,
|
|
70
|
+
radius=blur,
|
|
71
|
+
sigma=blur / 3.0,
|
|
72
|
+
dst_buffer=shadow,
|
|
73
|
+
temp_buffer=temp_buffer
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
colorize_shadow_f32(
|
|
77
|
+
shadow._buffer._handle,
|
|
78
|
+
shadow.width,
|
|
79
|
+
shadow.height,
|
|
80
|
+
color
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
Fill.color(result, (0.0, 0.0, 0.0, 0.0))
|
|
84
|
+
Blend.normal(result, shadow, pos_x=offset_x, pos_y=offset_y)
|
|
85
|
+
Blend.normal(result, image, pos_x=0, pos_y=0)
|
|
86
|
+
|
|
87
|
+
if owns_shadow:
|
|
88
|
+
shadow.free()
|
|
89
|
+
|
|
90
|
+
return result if return_result else None
|
pyimagecuda/fill.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
from .image import Image
|
|
3
|
+
from .pyimagecuda_internal import fill_color_f32, fill_gradient_f32 #type: ignore
|
|
4
|
+
|
|
5
|
+
class Fill:
|
|
6
|
+
|
|
7
|
+
@staticmethod
|
|
8
|
+
def color(image: Image, rgba: tuple[float, float, float, float]) -> None:
|
|
9
|
+
fill_color_f32(image._buffer._handle, rgba, image.width, image.height)
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def gradient(image: Image,
|
|
13
|
+
rgba1: tuple[float, float, float, float],
|
|
14
|
+
rgba2: tuple[float, float, float, float],
|
|
15
|
+
direction: Literal['horizontal', 'vertical', 'diagonal', 'radial'] = 'horizontal',
|
|
16
|
+
seamless: bool = False) -> None:
|
|
17
|
+
direction_map = {
|
|
18
|
+
'horizontal': 0,
|
|
19
|
+
'vertical': 1,
|
|
20
|
+
'diagonal': 2,
|
|
21
|
+
'radial': 3
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
dir_int = direction_map.get(direction)
|
|
25
|
+
if dir_int is None:
|
|
26
|
+
raise ValueError(f"Invalid direction: {direction}. Must be one of {list(direction_map.keys())}")
|
|
27
|
+
|
|
28
|
+
fill_gradient_f32(
|
|
29
|
+
image._buffer._handle,
|
|
30
|
+
rgba1,
|
|
31
|
+
rgba2,
|
|
32
|
+
image.width,
|
|
33
|
+
image.height,
|
|
34
|
+
dir_int,
|
|
35
|
+
seamless
|
|
36
|
+
)
|
pyimagecuda/filter.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from .image import Image
|
|
2
|
+
from .utils import check_dimensions_match
|
|
3
|
+
from .pyimagecuda_internal import gaussian_blur_separable_f32, sharpen_f32 #type: ignore
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Filter:
|
|
7
|
+
|
|
8
|
+
@staticmethod
|
|
9
|
+
def gaussian_blur(
|
|
10
|
+
src: Image,
|
|
11
|
+
radius: int = 3,
|
|
12
|
+
sigma: float | None = None,
|
|
13
|
+
dst_buffer: Image | None = None,
|
|
14
|
+
temp_buffer: Image | None = None
|
|
15
|
+
) -> Image | None:
|
|
16
|
+
|
|
17
|
+
if sigma is None:
|
|
18
|
+
sigma = radius / 3.0
|
|
19
|
+
|
|
20
|
+
if dst_buffer is None:
|
|
21
|
+
dst_buffer = Image(src.width, src.height)
|
|
22
|
+
return_dst = True
|
|
23
|
+
else:
|
|
24
|
+
check_dimensions_match(dst_buffer, src)
|
|
25
|
+
return_dst = False
|
|
26
|
+
|
|
27
|
+
if temp_buffer is None:
|
|
28
|
+
temp_buffer = Image(src.width, src.height)
|
|
29
|
+
owns_temp = True
|
|
30
|
+
else:
|
|
31
|
+
check_dimensions_match(temp_buffer, src)
|
|
32
|
+
owns_temp = False
|
|
33
|
+
|
|
34
|
+
gaussian_blur_separable_f32(
|
|
35
|
+
src._buffer._handle,
|
|
36
|
+
temp_buffer._buffer._handle,
|
|
37
|
+
dst_buffer._buffer._handle,
|
|
38
|
+
src.width,
|
|
39
|
+
src.height,
|
|
40
|
+
int(radius),
|
|
41
|
+
float(sigma)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if owns_temp:
|
|
45
|
+
temp_buffer.free()
|
|
46
|
+
|
|
47
|
+
return dst_buffer if return_dst else None
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def sharpen(
|
|
51
|
+
src: Image,
|
|
52
|
+
strength: float = 1.0,
|
|
53
|
+
dst_buffer: Image | None = None
|
|
54
|
+
) -> Image | None:
|
|
55
|
+
|
|
56
|
+
if dst_buffer is None:
|
|
57
|
+
dst_buffer = Image(src.width, src.height)
|
|
58
|
+
return_buffer = True
|
|
59
|
+
else:
|
|
60
|
+
check_dimensions_match(dst_buffer, src)
|
|
61
|
+
return_buffer = False
|
|
62
|
+
|
|
63
|
+
sharpen_f32(
|
|
64
|
+
src._buffer._handle,
|
|
65
|
+
dst_buffer._buffer._handle,
|
|
66
|
+
src.width,
|
|
67
|
+
src.height,
|
|
68
|
+
float(strength)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return dst_buffer if return_buffer else None
|
pyimagecuda/image.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
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 get_max_capacity(self) -> tuple[int, int]:
|
|
63
|
+
return (self._buffer.capacity_width, self._buffer.capacity_height)
|
|
64
|
+
|
|
65
|
+
def __repr__(self) -> str:
|
|
66
|
+
return f"{self.__class__.__name__}({self.width}×{self.height})"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Image(ImageBase):
|
|
70
|
+
|
|
71
|
+
def __init__(self, width: int, height: int):
|
|
72
|
+
super().__init__(width, height, is_u8=False)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ImageU8(ImageBase):
|
|
76
|
+
|
|
77
|
+
def __init__(self, width: int, height: int):
|
|
78
|
+
super().__init__(width, height, is_u8=True)
|
pyimagecuda/io.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
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 check_dimensions_match
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def upload(image: ImageBase, data: bytes | bytearray | memoryview) -> None:
|
|
10
|
+
bytes_per_pixel = 4 if isinstance(image, ImageU8) else 16
|
|
11
|
+
expected = image.width * image.height * bytes_per_pixel
|
|
12
|
+
actual = data.nbytes if isinstance(data, memoryview) else len(data)
|
|
13
|
+
|
|
14
|
+
if actual != expected:
|
|
15
|
+
raise ValueError(f"Expected {expected} bytes, got {actual}")
|
|
16
|
+
|
|
17
|
+
upload_to_buffer(image._buffer._handle, data, image.width, image.height)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def download(image: ImageBase) -> bytes:
|
|
21
|
+
return download_from_buffer(image._buffer._handle, image.width, image.height)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def copy(dst: ImageBase, src: ImageBase) -> None:
|
|
25
|
+
check_dimensions_match(dst, src)
|
|
26
|
+
copy_buffer(dst._buffer._handle, src._buffer._handle, src.width, src.height)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def convert_float_to_u8(dst: ImageU8, src: Image) -> None:
|
|
30
|
+
check_dimensions_match(dst, src)
|
|
31
|
+
convert_f32_to_u8(dst._buffer._handle, src._buffer._handle, src.width, src.height)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def convert_u8_to_float(dst: Image, src: ImageU8) -> None:
|
|
35
|
+
check_dimensions_match(dst, src)
|
|
36
|
+
convert_u8_to_f32(dst._buffer._handle, src._buffer._handle, src.width, src.height)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load(filepath: str, f32_buffer: Image | None = None, u8_buffer: ImageU8 | None = None) -> Image:
|
|
40
|
+
vips_img = pyvips.Image.new_from_file(filepath, access='sequential')
|
|
41
|
+
|
|
42
|
+
if vips_img.bands == 1:
|
|
43
|
+
vips_img = vips_img.bandjoin([vips_img, vips_img, vips_img])
|
|
44
|
+
vips_img = vips_img.bandjoin(255)
|
|
45
|
+
elif vips_img.bands == 3:
|
|
46
|
+
vips_img = vips_img.bandjoin(255)
|
|
47
|
+
elif vips_img.bands == 4:
|
|
48
|
+
pass
|
|
49
|
+
else:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"Unsupported image format: {vips_img.bands} channels. "
|
|
52
|
+
f"Only grayscale (1), RGB (3), and RGBA (4) are supported."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
width = vips_img.width
|
|
56
|
+
height = vips_img.height
|
|
57
|
+
|
|
58
|
+
if f32_buffer is None:
|
|
59
|
+
f32_buffer = Image(width, height)
|
|
60
|
+
else:
|
|
61
|
+
max_w, max_h = f32_buffer.get_max_capacity()
|
|
62
|
+
if width > max_w or height > max_h:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Image {width}×{height} exceeds f32_buffer capacity {max_w}×{max_h}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
f32_buffer.width = width
|
|
68
|
+
f32_buffer.height = height
|
|
69
|
+
|
|
70
|
+
if u8_buffer is None:
|
|
71
|
+
u8_buffer = ImageU8(width, height)
|
|
72
|
+
owns_u8 = True
|
|
73
|
+
else:
|
|
74
|
+
max_w, max_h = u8_buffer.get_max_capacity()
|
|
75
|
+
if width > max_w or height > max_h:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"Image {width}×{height} exceeds u8_buffer capacity {max_w}×{max_h}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
u8_buffer.width = width
|
|
81
|
+
u8_buffer.height = height
|
|
82
|
+
owns_u8 = False
|
|
83
|
+
|
|
84
|
+
vips_img = vips_img.cast('uchar')
|
|
85
|
+
pixel_data = vips_img.write_to_memory()
|
|
86
|
+
|
|
87
|
+
upload(u8_buffer, pixel_data)
|
|
88
|
+
|
|
89
|
+
convert_u8_to_float(f32_buffer, u8_buffer)
|
|
90
|
+
|
|
91
|
+
if owns_u8:
|
|
92
|
+
u8_buffer.free()
|
|
93
|
+
|
|
94
|
+
return f32_buffer
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def save(image: Image, filepath: str, u8_buffer: ImageU8 | None = None, quality: int | None = None) -> None:
|
|
98
|
+
if u8_buffer is None:
|
|
99
|
+
u8_buffer = ImageU8(image.width, image.height)
|
|
100
|
+
owns_buffer = True
|
|
101
|
+
else:
|
|
102
|
+
check_dimensions_match(u8_buffer, image)
|
|
103
|
+
owns_buffer = False
|
|
104
|
+
|
|
105
|
+
convert_float_to_u8(u8_buffer, image)
|
|
106
|
+
pixel_data = download(u8_buffer)
|
|
107
|
+
|
|
108
|
+
vips_img = pyvips.Image.new_from_memory(
|
|
109
|
+
pixel_data,
|
|
110
|
+
image.width,
|
|
111
|
+
image.height,
|
|
112
|
+
bands=4,
|
|
113
|
+
format='uchar'
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
vips_img = vips_img.copy(interpretation='srgb')
|
|
117
|
+
|
|
118
|
+
save_kwargs = {}
|
|
119
|
+
if quality is not None:
|
|
120
|
+
if filepath.lower().endswith(('.jpg', '.jpeg')):
|
|
121
|
+
save_kwargs['Q'] = quality
|
|
122
|
+
elif filepath.lower().endswith('.webp'):
|
|
123
|
+
save_kwargs['Q'] = quality
|
|
124
|
+
elif filepath.lower().endswith(('.heic', '.heif')):
|
|
125
|
+
save_kwargs['Q'] = quality
|
|
126
|
+
|
|
127
|
+
vips_img.write_to_file(filepath, **save_kwargs)
|
|
128
|
+
|
|
129
|
+
if owns_buffer:
|
|
130
|
+
u8_buffer.free()
|
|
Binary file
|
pyimagecuda/resize.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from .image import Image
|
|
2
|
+
from .pyimagecuda_internal import resize_f32 #type: ignore
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _resize_internal(
|
|
6
|
+
src: Image,
|
|
7
|
+
width: int,
|
|
8
|
+
height: int,
|
|
9
|
+
method: int,
|
|
10
|
+
dst_buffer: Image | None = None
|
|
11
|
+
) -> Image | None:
|
|
12
|
+
|
|
13
|
+
if dst_buffer is None:
|
|
14
|
+
dst_buffer = Image(width, height)
|
|
15
|
+
return_buffer = True
|
|
16
|
+
else:
|
|
17
|
+
if dst_buffer.width != width or dst_buffer.height != height:
|
|
18
|
+
raise ValueError(
|
|
19
|
+
f"dst_buffer size mismatch: expected {width}×{height}, "
|
|
20
|
+
f"got {dst_buffer.width}×{dst_buffer.height}"
|
|
21
|
+
)
|
|
22
|
+
return_buffer = False
|
|
23
|
+
|
|
24
|
+
resize_f32(
|
|
25
|
+
src._buffer._handle,
|
|
26
|
+
dst_buffer._buffer._handle,
|
|
27
|
+
src.width,
|
|
28
|
+
src.height,
|
|
29
|
+
width,
|
|
30
|
+
height,
|
|
31
|
+
method
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return dst_buffer if return_buffer else None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Resize:
|
|
38
|
+
@staticmethod
|
|
39
|
+
def nearest(
|
|
40
|
+
src: Image,
|
|
41
|
+
width: int,
|
|
42
|
+
height: int,
|
|
43
|
+
dst_buffer: Image | None = None
|
|
44
|
+
) -> Image | None:
|
|
45
|
+
return _resize_internal(src, width, height, 0, dst_buffer)
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def bilinear(
|
|
49
|
+
src: Image,
|
|
50
|
+
width: int,
|
|
51
|
+
height: int,
|
|
52
|
+
dst_buffer: Image | None = None
|
|
53
|
+
) -> Image | None:
|
|
54
|
+
return _resize_internal(src, width, height, 1, dst_buffer)
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def bicubic(
|
|
58
|
+
src: Image,
|
|
59
|
+
width: int,
|
|
60
|
+
height: int,
|
|
61
|
+
dst_buffer: Image | None = None
|
|
62
|
+
) -> Image | None:
|
|
63
|
+
return _resize_internal(src, width, height, 2, dst_buffer)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def lanczos(
|
|
67
|
+
src: Image,
|
|
68
|
+
width: int,
|
|
69
|
+
height: int,
|
|
70
|
+
dst_buffer: Image | None = None
|
|
71
|
+
) -> Image | None:
|
|
72
|
+
return _resize_internal(src, width, height, 3, dst_buffer)
|
pyimagecuda/utils.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .image import ImageBase
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def check_dimensions_match(img1: ImageBase, img2: ImageBase) -> None:
|
|
6
|
+
if img1.width != img2.width or img1.height != img2.height:
|
|
7
|
+
raise ValueError(
|
|
8
|
+
f"Dimension mismatch: {img1.width}×{img1.height} vs {img2.width}×{img2.height}"
|
|
9
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: pyimagecuda
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: GPU-accelerated image processing library for Python
|
|
5
|
+
Author: Beltrán Offerrall
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: pyvips[binary]
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# PyImageCUDA
|
|
11
|
+
|
|
12
|
+
[](https://github.com/offerrall/pyimagecuda/actions)
|
|
13
|
+
|
|
14
|
+
> ⚠️ **STATUS: PRE-ALPHA / INFRASTRUCTURE TESTING**
|
|
15
|
+
>
|
|
16
|
+
> This repository is currently validating the build and distribution system.
|
|
17
|
+
> **It is NOT ready for production use yet.**
|
|
18
|
+
|
|
19
|
+
### Goal
|
|
20
|
+
GPU-accelerated (CUDA) image processing library for Python.
|
|
21
|
+
Designed to be installed via a simple `pip install` without requiring the user to have the CUDA Toolkit or Visual Studio installed.
|
|
22
|
+
|
|
23
|
+
### Verification (Testers Only)
|
|
24
|
+
|
|
25
|
+
If you have installed a test build, you can verify that hardware acceleration is working correctly by running:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
python -c "import pyimagecuda; pyimagecuda.check_system()"
|
|
29
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
pyimagecuda/__init__.py,sha256=YNyiiMnR1XgqMqdhXbbPG2r-w8VUPetHZN-la2sJUAU,1941
|
|
2
|
+
pyimagecuda/blend.py,sha256=_WgLwveDHJ_-OpqpcRf7FVZIF_k0SoPhA_RwRjgqV1M,1824
|
|
3
|
+
pyimagecuda/effect.py,sha256=BPnu53goEMZGWWF0_9_tPfKXEF5eLkMtfM99H_KN3GE,2585
|
|
4
|
+
pyimagecuda/fill.py,sha256=ZBzN1L8iXsymd7aixovkc397osrD8AQCiytOWiYRwac,1238
|
|
5
|
+
pyimagecuda/filter.py,sha256=GdGIiadYuqtY8k7kFISR8GMTHZ1QYtvtLWFxDMO9_qY,1956
|
|
6
|
+
pyimagecuda/image.py,sha256=K3d13hhUb1y4DKu8zlA81JFgQ81_MWZeMKHc4OOUvFA,2304
|
|
7
|
+
pyimagecuda/io.py,sha256=sATHtf0tHH-4luZ9kmVF1EQ7vhyoB5iIhLK7bnj9x5E,4285
|
|
8
|
+
pyimagecuda/pyimagecuda_internal.cp313-win_amd64.pyd,sha256=iaNyBlVlySdiHYQtCUH13YeaIXmokMByNbjmYvfvKQA,336896
|
|
9
|
+
pyimagecuda/resize.py,sha256=WZAf85t1BNxF4THbPR6_dcj44xZroLKmTR5T90pw6nk,1848
|
|
10
|
+
pyimagecuda/utils.py,sha256=9QDAFFnb2uJYwXup3hHHUm7uOjT7e5omfDEpwRx_F_w,302
|
|
11
|
+
pyimagecuda-0.0.1.dist-info/METADATA,sha256=nhfRJoEYkBlNw3d8pgQ89LCx1rB73exmg02Gc27fwGI,1001
|
|
12
|
+
pyimagecuda-0.0.1.dist-info/WHEEL,sha256=vkL3wTIkhjZa3RmEXX20hldNp6Q8qtwRjrXW6K5sw_Q,106
|
|
13
|
+
pyimagecuda-0.0.1.dist-info/RECORD,,
|