imagekit-python 0.0.1a1__py3-none-any.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.
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import cv2
|
|
3
|
+
import base64
|
|
4
|
+
import numpy as np
|
|
5
|
+
from PIL import Image
|
|
6
|
+
from cv2.typing import MatLike
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import ClassVar, Literal, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Custom Errors
|
|
12
|
+
class ImageEmpty(Exception):
|
|
13
|
+
"""Raised when image is empty or has no data."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, message: str = "Image is empty"):
|
|
16
|
+
self.message = message
|
|
17
|
+
super().__init__(self.message)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InvalidImage(Exception):
|
|
21
|
+
"""Raised when image is invalid or corrupted."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, message: str = "Image is invalid or corrupted"):
|
|
24
|
+
self.message = message
|
|
25
|
+
super().__init__(self.message)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ImageTooLarge(Exception):
|
|
29
|
+
"""Raised when image size exceeds maximum allowed size."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, max_mb: float, message: str = None):
|
|
32
|
+
self.max_mb = max_mb
|
|
33
|
+
self.message = (
|
|
34
|
+
message or f"Image size exceeds maximum allowed size of {max_mb} MB"
|
|
35
|
+
)
|
|
36
|
+
super().__init__(self.message)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class InvalidAngle(Exception):
|
|
40
|
+
"""Raised when image size exceeds maximum allowed size."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, message: str = "Only 90, 180, 270 supported"):
|
|
43
|
+
self.message = message
|
|
44
|
+
super().__init__(self.message)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
ImageFormat = Literal["JPEG", "PNG"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class SmartImage:
|
|
52
|
+
_image: MatLike
|
|
53
|
+
_sharpening_kernel: ClassVar[np.ndarray] = np.array(
|
|
54
|
+
[[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# -----------------------------
|
|
58
|
+
# Constructors
|
|
59
|
+
# -----------------------------
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_bytes(cls, image_bytes: bytes):
|
|
62
|
+
if not image_bytes:
|
|
63
|
+
raise ImageEmpty()
|
|
64
|
+
|
|
65
|
+
cls.verify_bytes(image_bytes)
|
|
66
|
+
|
|
67
|
+
image = SmartImage.bytes_to_cv2(image_bytes)
|
|
68
|
+
|
|
69
|
+
if image is None:
|
|
70
|
+
raise InvalidImage()
|
|
71
|
+
|
|
72
|
+
return cls(_image=image)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_cv2(cls, image: MatLike) -> "SmartImage":
|
|
76
|
+
if image is None or image.size == 0:
|
|
77
|
+
raise ImageEmpty()
|
|
78
|
+
return cls(_image=image.copy())
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_pillow(cls, image: Image.Image) -> "SmartImage":
|
|
82
|
+
if image is None:
|
|
83
|
+
raise ImageEmpty()
|
|
84
|
+
return cls.from_cv2(SmartImage.pillow_to_cv2(image))
|
|
85
|
+
|
|
86
|
+
# -----------------------------
|
|
87
|
+
# Calculated Property
|
|
88
|
+
# -----------------------------
|
|
89
|
+
@property
|
|
90
|
+
def shape(self):
|
|
91
|
+
return self._image.shape
|
|
92
|
+
|
|
93
|
+
# -----------------------------
|
|
94
|
+
# Conversions (On Demand)
|
|
95
|
+
# -----------------------------
|
|
96
|
+
def to_cv2(self) -> MatLike:
|
|
97
|
+
return self._image.copy()
|
|
98
|
+
|
|
99
|
+
def to_pillow(self) -> Image.Image:
|
|
100
|
+
return SmartImage.cv2_to_pillow(self._image)
|
|
101
|
+
|
|
102
|
+
def to_bytes(self, format: ImageFormat = "JPEG", quality: int = 80) -> bytes:
|
|
103
|
+
return SmartImage.cv2_to_bytes(self._image, format, quality)
|
|
104
|
+
|
|
105
|
+
def to_base64(self, format: ImageFormat = "JPEG", quality: int = 80) -> str:
|
|
106
|
+
return base64.b64encode(self.to_bytes(format=format, quality=quality)).decode(
|
|
107
|
+
"utf-8"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# -----------------------------
|
|
111
|
+
# Transformations (Pure)
|
|
112
|
+
# -----------------------------
|
|
113
|
+
def resize(self, width: int = None, height: int = None):
|
|
114
|
+
return SmartImage.from_cv2(SmartImage.resize_image(self._image, width, height))
|
|
115
|
+
|
|
116
|
+
def crop(self, bbox: tuple[int, int, int, int], padding_factor: float = 0):
|
|
117
|
+
return SmartImage.from_cv2(
|
|
118
|
+
SmartImage.crop_image(self._image, bbox, padding_factor)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def rotate(self, angle: int):
|
|
122
|
+
return SmartImage.from_cv2(SmartImage.rotate_image(self._image, angle))
|
|
123
|
+
|
|
124
|
+
def sharpen_v1(self):
|
|
125
|
+
return SmartImage.from_cv2(SmartImage.sharpened_image_v1(self._image))
|
|
126
|
+
|
|
127
|
+
def sharpen_v2(self):
|
|
128
|
+
return SmartImage.from_cv2(SmartImage.sharpened_image_v2(self._image))
|
|
129
|
+
|
|
130
|
+
def adjust_quality(self, format: ImageFormat, quality: int = 80):
|
|
131
|
+
return SmartImage.from_cv2(
|
|
132
|
+
SmartImage.adjust_quality_image(self._image, format, quality),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def apply(self, fn):
|
|
136
|
+
new_img = fn(self._image.copy())
|
|
137
|
+
return SmartImage.from_cv2(new_img)
|
|
138
|
+
|
|
139
|
+
# -----------------------------
|
|
140
|
+
# Document Related
|
|
141
|
+
# -----------------------------
|
|
142
|
+
def ensure_document_upright(self, tolerance: float = 10):
|
|
143
|
+
return SmartImage.from_cv2(
|
|
144
|
+
SmartImage.ensure_document_upright_image(self._image, tolerance),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def ensure_document_portrait(self):
|
|
148
|
+
return SmartImage.from_cv2(
|
|
149
|
+
SmartImage.ensure_document_portrait_image(self._image)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def ensure_document_landscape(self):
|
|
153
|
+
return SmartImage.from_cv2(
|
|
154
|
+
SmartImage.ensure_document_landscape_image(self._image)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def unwarp_document(self, pts: list[tuple[int, int]], padding_ratio: float = 0.01):
|
|
158
|
+
return SmartImage.from_cv2(
|
|
159
|
+
SmartImage.unwarp_document_image(self._image, pts, padding_ratio)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# -----------------------------
|
|
163
|
+
# Validation Helpers
|
|
164
|
+
# -----------------------------
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def verify_bytes(image_bytes: bytes):
|
|
168
|
+
try:
|
|
169
|
+
Image.open(io.BytesIO(image_bytes)).verify()
|
|
170
|
+
except Exception:
|
|
171
|
+
raise InvalidImage()
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def validate_size(image_bytes: bytes, max_size_mb: Optional[float]):
|
|
175
|
+
if max_size_mb:
|
|
176
|
+
size_mb = len(image_bytes) / (1024 * 1024)
|
|
177
|
+
if size_mb > max_size_mb:
|
|
178
|
+
raise ImageTooLarge(max_size_mb)
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
# -----------------------------
|
|
182
|
+
# Static Methods
|
|
183
|
+
# -----------------------------
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def bytes_to_cv2(image: bytes):
|
|
187
|
+
np_arr = np.frombuffer(image, np.uint8)
|
|
188
|
+
return cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def bytes_to_base64(image: bytes):
|
|
192
|
+
return base64.b64encode(image).decode("utf-8")
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def bytes_to_pillow(image: bytes):
|
|
196
|
+
"""Converts raw bytes into a PIL Image object."""
|
|
197
|
+
return Image.open(io.BytesIO(image))
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def cv2_to_bytes(image: MatLike, format: ImageFormat = "JPEG", quality: int = 80):
|
|
201
|
+
# Set JPEG quality (0 to 100, higher = better quality, larger size)
|
|
202
|
+
if format.upper() == "PNG":
|
|
203
|
+
# PNG is lossless, so quality is ignored (uses compression level 0-9 instead)
|
|
204
|
+
success, image_bytes = cv2.imencode(".png", image)
|
|
205
|
+
else:
|
|
206
|
+
encode_params = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
|
|
207
|
+
success, image_bytes = cv2.imencode(".jpg", image, encode_params)
|
|
208
|
+
|
|
209
|
+
if not success:
|
|
210
|
+
raise InvalidImage()
|
|
211
|
+
return image_bytes.tobytes()
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def cv2_to_base64(image: MatLike, format: ImageFormat = "JPEG", quality: int = 80):
|
|
215
|
+
image_bytes = SmartImage.cv2_to_bytes(image, format, quality)
|
|
216
|
+
return SmartImage.bytes_to_base64(image_bytes)
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def cv2_to_pillow(image: MatLike):
|
|
220
|
+
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
|
221
|
+
return Image.fromarray(rgb_image)
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def pillow_to_bytes(
|
|
225
|
+
image: Image.Image, format: ImageFormat = "JPEG", quality: int = 80
|
|
226
|
+
):
|
|
227
|
+
buffer = io.BytesIO()
|
|
228
|
+
# PNG doesn't use the quality kwarg
|
|
229
|
+
if format.upper() == "PNG":
|
|
230
|
+
image.save(buffer, format="PNG")
|
|
231
|
+
else:
|
|
232
|
+
image.save(buffer, format="JPEG", quality=quality)
|
|
233
|
+
return buffer.getvalue()
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def pillow_to_cv2(image: Image.Image):
|
|
237
|
+
rgb_arr = np.array(image)
|
|
238
|
+
return cv2.cvtColor(rgb_arr, cv2.COLOR_RGB2BGR)
|
|
239
|
+
|
|
240
|
+
@staticmethod
|
|
241
|
+
def pillow_to_base64(
|
|
242
|
+
image: Image.Image, format: ImageFormat = "JPEG", quality: int = 80
|
|
243
|
+
) -> str:
|
|
244
|
+
image_bytes = SmartImage.pillow_to_bytes(image, format, quality)
|
|
245
|
+
return SmartImage.bytes_to_base64(image_bytes)
|
|
246
|
+
|
|
247
|
+
@staticmethod
|
|
248
|
+
def crop_image(
|
|
249
|
+
image: MatLike, bbox: tuple[int, int, int, int], padding_factor: float = 0
|
|
250
|
+
):
|
|
251
|
+
result_image = image.copy()
|
|
252
|
+
try:
|
|
253
|
+
x1, y1, x2, y2 = bbox
|
|
254
|
+
img_h, img_w = result_image.shape[:2]
|
|
255
|
+
|
|
256
|
+
# Calculate dynamic padding as a percentage of the image dimensions
|
|
257
|
+
padding = int(min(img_h, img_w) * padding_factor)
|
|
258
|
+
|
|
259
|
+
# Apply padding and clamp to image boundaries
|
|
260
|
+
x1 = max(0, x1 - padding)
|
|
261
|
+
y1 = max(0, y1 - padding)
|
|
262
|
+
x2 = min(img_w, x2 + padding)
|
|
263
|
+
y2 = min(img_h, y2 + padding)
|
|
264
|
+
|
|
265
|
+
# Validate the cropped region
|
|
266
|
+
if x1 >= x2 or y1 >= y2:
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
result_image = result_image[y1:y2, x1:x2]
|
|
270
|
+
if result_image.size == 0:
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
return result_image
|
|
274
|
+
|
|
275
|
+
except Exception:
|
|
276
|
+
return result_image
|
|
277
|
+
|
|
278
|
+
@staticmethod
|
|
279
|
+
def resize_image(image: MatLike, width: int = None, height: int = None):
|
|
280
|
+
|
|
281
|
+
if not width and not height:
|
|
282
|
+
raise ValueError("Either width or height must be provided")
|
|
283
|
+
|
|
284
|
+
h, w = image.shape[:2]
|
|
285
|
+
|
|
286
|
+
if width and height:
|
|
287
|
+
resized = cv2.resize(image, (width, height))
|
|
288
|
+
elif width:
|
|
289
|
+
ratio = width / w
|
|
290
|
+
resized = cv2.resize(image, (width, int(h * ratio)))
|
|
291
|
+
elif height:
|
|
292
|
+
ratio = height / h
|
|
293
|
+
resized = cv2.resize(image, (int(w * ratio), height))
|
|
294
|
+
else:
|
|
295
|
+
return image
|
|
296
|
+
|
|
297
|
+
return resized
|
|
298
|
+
|
|
299
|
+
@staticmethod
|
|
300
|
+
def adjust_quality_image(image: MatLike, format: ImageFormat, quality: int = 80):
|
|
301
|
+
return SmartImage.bytes_to_cv2(SmartImage.cv2_to_bytes(image, format, quality))
|
|
302
|
+
|
|
303
|
+
@staticmethod
|
|
304
|
+
def sharpened_image_v1(image: MatLike):
|
|
305
|
+
blurred_image = cv2.GaussianBlur(image, (5, 5), 0)
|
|
306
|
+
sharpened_image = cv2.filter2D(blurred_image, -1, SmartImage._sharpening_kernel)
|
|
307
|
+
return sharpened_image
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def sharpened_image_v2(image: MatLike):
|
|
311
|
+
kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])
|
|
312
|
+
return cv2.filter2D(image, -1, kernel)
|
|
313
|
+
|
|
314
|
+
@staticmethod
|
|
315
|
+
def rotate_image(image: MatLike, angle: Literal[90, 180, 270]):
|
|
316
|
+
if angle == 90:
|
|
317
|
+
rotated = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
|
|
318
|
+
elif angle == 180:
|
|
319
|
+
rotated = cv2.rotate(image, cv2.ROTATE_180)
|
|
320
|
+
elif angle == 270:
|
|
321
|
+
rotated = cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
|
322
|
+
else:
|
|
323
|
+
raise InvalidAngle()
|
|
324
|
+
|
|
325
|
+
return rotated
|
|
326
|
+
|
|
327
|
+
@staticmethod
|
|
328
|
+
def __order_points(pts: list[tuple[int, int]]):
|
|
329
|
+
# Convert to numpy array if not already
|
|
330
|
+
np_pts = np.array(pts, dtype=np.float32)
|
|
331
|
+
|
|
332
|
+
# Sum and difference to sort the points
|
|
333
|
+
s = np_pts.sum(axis=1)
|
|
334
|
+
diff = np.diff(pts, axis=1)
|
|
335
|
+
|
|
336
|
+
ordered = np.zeros((4, 2), dtype=np.float32)
|
|
337
|
+
ordered[0] = np_pts[np.argmin(s)] # Top-left
|
|
338
|
+
ordered[2] = np_pts[np.argmax(s)] # Bottom-right
|
|
339
|
+
ordered[1] = np_pts[np.argmin(diff)] # Top-right
|
|
340
|
+
ordered[3] = np_pts[np.argmax(diff)] # Bottom-left
|
|
341
|
+
|
|
342
|
+
return ordered
|
|
343
|
+
|
|
344
|
+
@staticmethod
|
|
345
|
+
def __get_max_dimensions(np_pts: np.ndarray[tuple[int, int], np.dtype[np.float32]]):
|
|
346
|
+
(tl, tr, br, bl) = np_pts
|
|
347
|
+
|
|
348
|
+
widthA = np.linalg.norm(br - bl)
|
|
349
|
+
widthB = np.linalg.norm(tr - tl)
|
|
350
|
+
maxWidth = max(int(widthA), int(widthB))
|
|
351
|
+
|
|
352
|
+
heightA = np.linalg.norm(tr - br)
|
|
353
|
+
heightB = np.linalg.norm(tl - bl)
|
|
354
|
+
maxHeight = max(int(heightA), int(heightB))
|
|
355
|
+
|
|
356
|
+
return maxWidth, maxHeight
|
|
357
|
+
|
|
358
|
+
# @staticmethod
|
|
359
|
+
# def unwarp_document(image: np.ndarray, pts: list[tuple[int, int]]):
|
|
360
|
+
# rect = ImageService.__order_points(pts)
|
|
361
|
+
# maxWidth, maxHeight = ImageService.__get_max_dimensions(rect)
|
|
362
|
+
|
|
363
|
+
# # Destination points for the "rectified" image
|
|
364
|
+
# dst = np.array([
|
|
365
|
+
# [0, 0],
|
|
366
|
+
# [maxWidth - 1, 0],
|
|
367
|
+
# [maxWidth - 1, maxHeight - 1],
|
|
368
|
+
# [0, maxHeight - 1]], dtype=np.float32)
|
|
369
|
+
|
|
370
|
+
# # Compute the perspective transform matrix and apply it
|
|
371
|
+
# M = cv2.getPerspectiveTransform(rect, dst)
|
|
372
|
+
# warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
|
|
373
|
+
|
|
374
|
+
# return warped
|
|
375
|
+
|
|
376
|
+
@staticmethod
|
|
377
|
+
def unwarp_document_image(
|
|
378
|
+
image: MatLike, pts: list[tuple[int, int]], padding_ratio: float = 0.01
|
|
379
|
+
):
|
|
380
|
+
rect = SmartImage.__order_points(pts)
|
|
381
|
+
maxWidth, maxHeight = SmartImage.__get_max_dimensions(rect)
|
|
382
|
+
|
|
383
|
+
# Calculate proportional padding in pixels
|
|
384
|
+
padding = int(max(maxWidth, maxHeight) * padding_ratio)
|
|
385
|
+
|
|
386
|
+
# Destination points for the "rectified" image
|
|
387
|
+
dst = np.array(
|
|
388
|
+
[
|
|
389
|
+
[padding, padding],
|
|
390
|
+
[maxWidth - 1 + padding, padding],
|
|
391
|
+
[maxWidth - 1 + padding, maxHeight - 1 + padding],
|
|
392
|
+
[padding, maxHeight - 1 + padding],
|
|
393
|
+
],
|
|
394
|
+
dtype=np.float32,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Compute the perspective transform matrix and apply it
|
|
398
|
+
M = cv2.getPerspectiveTransform(rect, dst)
|
|
399
|
+
warped = cv2.warpPerspective(
|
|
400
|
+
image, M, (maxWidth + padding * 2, maxHeight + padding * 2)
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
return warped
|
|
404
|
+
|
|
405
|
+
@staticmethod
|
|
406
|
+
def ensure_document_landscape_image(image: MatLike):
|
|
407
|
+
"""
|
|
408
|
+
Rotates the image 90 degrees counter-clockwise if it is in portrait orientation
|
|
409
|
+
(i.e., height > width), so that the output is in landscape orientation.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
image (MatLike): Input image.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
MatLike Rotated (if needed) image in landscape orientation.
|
|
416
|
+
"""
|
|
417
|
+
h, w = image.shape[:2]
|
|
418
|
+
result_image = image.copy()
|
|
419
|
+
if h > w:
|
|
420
|
+
result_image = cv2.rotate(result_image, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
|
421
|
+
return result_image
|
|
422
|
+
|
|
423
|
+
@staticmethod
|
|
424
|
+
def ensure_document_portrait_image(image: MatLike):
|
|
425
|
+
"""
|
|
426
|
+
Rotates the image 90 degrees counter-clockwise if it is in landscape orientation
|
|
427
|
+
(i.e., width > height), so that the output is in portrait orientation.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
image (MatLike): Input image.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
MatLike Rotated (if needed) image in landscape orientation.
|
|
434
|
+
"""
|
|
435
|
+
h, w = image.shape[:2]
|
|
436
|
+
result_image = image.copy()
|
|
437
|
+
if w > h:
|
|
438
|
+
result_image = cv2.rotate(result_image, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
|
439
|
+
return result_image
|
|
440
|
+
|
|
441
|
+
@staticmethod
|
|
442
|
+
def ensure_document_upright_image(image: MatLike, tolerance: float = 10) -> MatLike:
|
|
443
|
+
"""
|
|
444
|
+
Checks image orientation using pytesseract and rotates it by 180 degrees
|
|
445
|
+
if rotation angle is close to 180.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
img (MatLike): Input image (as a NumPy array).
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
MatLike: Rotated or original image.
|
|
452
|
+
"""
|
|
453
|
+
try:
|
|
454
|
+
import pytesseract
|
|
455
|
+
except ImportError as e:
|
|
456
|
+
raise RuntimeError(
|
|
457
|
+
"pytesseract is required for OCR. "
|
|
458
|
+
"Install with `pip install imagekit-python[pytesseract]`"
|
|
459
|
+
) from e
|
|
460
|
+
result_image = image.copy()
|
|
461
|
+
try:
|
|
462
|
+
orientation_data = pytesseract.image_to_osd(
|
|
463
|
+
result_image, output_type=pytesseract.Output.DICT
|
|
464
|
+
)
|
|
465
|
+
print(orientation_data)
|
|
466
|
+
angle = orientation_data["rotate"]
|
|
467
|
+
|
|
468
|
+
if abs(angle - 180) < tolerance:
|
|
469
|
+
result_image = cv2.rotate(result_image, cv2.ROTATE_180)
|
|
470
|
+
|
|
471
|
+
except pytesseract.pytesseract.TesseractError as e:
|
|
472
|
+
print("Tesseract OSD error:", e)
|
|
473
|
+
|
|
474
|
+
return result_image
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: imagekit-python
|
|
3
|
+
Version: 0.0.1a1
|
|
4
|
+
Summary: A Python library that provides a unified interface for handling images across multiple formats and performing common image processing tasks.
|
|
5
|
+
Project-URL: Repository, https://github.com/Hoopoes/imagekit
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 1 - Planning
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: numpy>=1.21.6
|
|
24
|
+
Requires-Dist: opencv-python-headless>=4.13.0.92
|
|
25
|
+
Requires-Dist: pillow>=9.5.0
|
|
26
|
+
Requires-Dist: pydantic>=2.5.3
|
|
27
|
+
Provides-Extra: all
|
|
28
|
+
Requires-Dist: pytesseract; extra == 'all'
|
|
29
|
+
Provides-Extra: pytesseract
|
|
30
|
+
Requires-Dist: pytesseract>=0.3.13; extra == 'pytesseract'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# Imagekit
|
|
34
|
+
|
|
35
|
+
`ImageKit` is a Python library that provides a unified interface for handling images across multiple formats (`bytes`, OpenCV `cv2`, Pillow `Image`) and performing common image processing tasks. It is designed for developers who want all-in-one image handling and processing, including conversion, cropping, resizing, rotation, and document unwarping.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
coming soon
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## Quick Start 🚀
|
|
47
|
+
|
|
48
|
+
### Basic Usage
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
coming soon
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## Parameters
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT License
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
imagekit_python/__init__.py,sha256=vekDkQ92P7wCwBEsISCufiwloXh50xNHNfriFrISefU,241
|
|
2
|
+
imagekit_python/smart_image.py,sha256=s1A5EEyE7vg6hVuCQOOmwsNw-C0W9GQ3EDq7MEmDIVM,15980
|
|
3
|
+
imagekit_python-0.0.1a1.dist-info/METADATA,sha256=zO2WuWnn3gSx8Z7YnodZrGiGHCoydTSNGRKjK5SbSKk,1880
|
|
4
|
+
imagekit_python-0.0.1a1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
5
|
+
imagekit_python-0.0.1a1.dist-info/licenses/LICENSE,sha256=vvtkYZ1iMd5ly1zrMSW1oX1iMgL4z1eVa9KT3ijlA5E,1085
|
|
6
|
+
imagekit_python-0.0.1a1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hoopoes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|