ImagePRO-Python 0.1.0__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.
- ImagePRO/__init__.py +0 -0
- ImagePRO/human_analysis/__init__.py +0 -0
- ImagePRO/human_analysis/body_analysis/__init__.py +0 -0
- ImagePRO/human_analysis/body_analysis/body_pose_estimation.py +129 -0
- ImagePRO/human_analysis/body_analysis/hand_tracking.py +157 -0
- ImagePRO/human_analysis/face_analysis/__init__.py +0 -0
- ImagePRO/human_analysis/face_analysis/eye_status_analysis.py +152 -0
- ImagePRO/human_analysis/face_analysis/face_detection.py +84 -0
- ImagePRO/human_analysis/face_analysis/face_mesh_analysis.py +172 -0
- ImagePRO/human_analysis/face_analysis/head_pose_estimation.py +148 -0
- ImagePRO/pre_processing/__init__.py +0 -0
- ImagePRO/pre_processing/blur.py +130 -0
- ImagePRO/pre_processing/contrast.py +105 -0
- ImagePRO/pre_processing/crop.py +51 -0
- ImagePRO/pre_processing/grayscale.py +27 -0
- ImagePRO/pre_processing/resize.py +43 -0
- ImagePRO/pre_processing/rotate.py +94 -0
- ImagePRO/pre_processing/sharpen.py +74 -0
- imagepro_python-0.1.0.dist-info/METADATA +17 -0
- imagepro_python-0.1.0.dist-info/RECORD +23 -0
- imagepro_python-0.1.0.dist-info/WHEEL +5 -0
- imagepro_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- imagepro_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
import mediapipe as mp
|
|
6
|
+
|
|
7
|
+
# Add parent directory to sys.path for custom module imports
|
|
8
|
+
parent_dir = Path(__file__).resolve().parent.parent.parent
|
|
9
|
+
sys.path.append(str(parent_dir))
|
|
10
|
+
|
|
11
|
+
from io_handler import IOHandler
|
|
12
|
+
from human_analysis.face_analysis.face_mesh_analysis import analyze_face_mesh
|
|
13
|
+
|
|
14
|
+
mp_face_mesh = mp.solutions.face_mesh
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def estimate_head_pose(
|
|
18
|
+
max_faces=1,
|
|
19
|
+
min_confidence=0.7,
|
|
20
|
+
image_path=None,
|
|
21
|
+
np_image=None,
|
|
22
|
+
result_path=None,
|
|
23
|
+
face_mesh_obj=None
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
Estimate head pose (yaw, pitch) from facial landmarks.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
max_faces (int): Number of faces to detect.
|
|
30
|
+
min_confidence (float): Confidence threshold (0–1).
|
|
31
|
+
image_path (str, optional): Path to input image.
|
|
32
|
+
np_image (np.ndarray, optional): Input image as array.
|
|
33
|
+
result_path (str, optional): If given, save results as CSV.
|
|
34
|
+
face_mesh_obj (FaceMesh, optional): Reusable MediaPipe instance.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
str or list[list]: CSV confirmation if saved, else list of [face_id, yaw, pitch].
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
ValueError: On invalid input or no detected face.
|
|
41
|
+
"""
|
|
42
|
+
if not isinstance(max_faces, int) or max_faces <= 0:
|
|
43
|
+
raise ValueError("'max_faces' must be a positive integer.")
|
|
44
|
+
if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
|
|
45
|
+
raise ValueError("'min_confidence' must be between 0 and 1.")
|
|
46
|
+
|
|
47
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
48
|
+
|
|
49
|
+
# Important landmark indices (MediaPipe 468 model)
|
|
50
|
+
indices = [1, 152, 33, 263, 168] # nose_tip, chin, left_eye, right_eye, nasion
|
|
51
|
+
|
|
52
|
+
_, landmarks = analyze_face_mesh(
|
|
53
|
+
max_faces=max_faces,
|
|
54
|
+
min_confidence=min_confidence,
|
|
55
|
+
landmarks_idx=indices,
|
|
56
|
+
np_image=np_image,
|
|
57
|
+
face_mesh_obj=face_mesh_obj
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if not landmarks:
|
|
61
|
+
raise ValueError("No face landmarks detected.")
|
|
62
|
+
|
|
63
|
+
results = []
|
|
64
|
+
for face in landmarks:
|
|
65
|
+
points = {lm[1]: lm for lm in face}
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
nose_x, nose_y = points[1][2:4]
|
|
69
|
+
chin_y = points[152][3]
|
|
70
|
+
left_x = points[33][2]
|
|
71
|
+
right_x = points[263][2]
|
|
72
|
+
nasion_x, nasion_y = points[168][2:4]
|
|
73
|
+
except KeyError:
|
|
74
|
+
continue # skip this face if any point is missing
|
|
75
|
+
|
|
76
|
+
# Simplified proportional estimation
|
|
77
|
+
yaw = 100 * ((right_x - nasion_x) - (nasion_x - left_x))
|
|
78
|
+
pitch = 100 * ((chin_y - nose_y) - (nose_y - nasion_y))
|
|
79
|
+
|
|
80
|
+
results.append([face[0][0], yaw, pitch]) # face_id, yaw, pitch
|
|
81
|
+
|
|
82
|
+
if result_path:
|
|
83
|
+
return IOHandler.save_csv(results, result_path)
|
|
84
|
+
|
|
85
|
+
return results
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def estimate_head_pose_live(max_faces=1, min_confidence=0.7):
|
|
89
|
+
"""
|
|
90
|
+
Live head pose estimation using webcam.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
max_faces (int): Number of faces to detect.
|
|
94
|
+
min_confidence (float): Confidence threshold (0–1).
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ValueError, RuntimeError: On invalid inputs or camera failure.
|
|
98
|
+
"""
|
|
99
|
+
if not isinstance(max_faces, int) or max_faces <= 0:
|
|
100
|
+
raise ValueError("'max_faces' must be a positive integer.")
|
|
101
|
+
if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
|
|
102
|
+
raise ValueError("'min_confidence' must be between 0 and 1.")
|
|
103
|
+
|
|
104
|
+
cap = cv2.VideoCapture(0)
|
|
105
|
+
if not cap.isOpened():
|
|
106
|
+
raise RuntimeError("Failed to access webcam.")
|
|
107
|
+
|
|
108
|
+
face_mesh = mp_face_mesh.FaceMesh(
|
|
109
|
+
max_num_faces=max_faces,
|
|
110
|
+
min_detection_confidence=min_confidence,
|
|
111
|
+
refine_landmarks=True,
|
|
112
|
+
static_image_mode=True
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
while True:
|
|
117
|
+
ret, frame = cap.read()
|
|
118
|
+
if not ret:
|
|
119
|
+
print("Skipping empty frame.")
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
face_angles = estimate_head_pose(
|
|
124
|
+
max_faces=max_faces,
|
|
125
|
+
min_confidence=min_confidence,
|
|
126
|
+
np_image=frame,
|
|
127
|
+
face_mesh_obj=face_mesh
|
|
128
|
+
)
|
|
129
|
+
except ValueError:
|
|
130
|
+
face_angles = []
|
|
131
|
+
|
|
132
|
+
for i, face in enumerate(face_angles):
|
|
133
|
+
face_id, yaw, pitch = face
|
|
134
|
+
text = f"Face {int(face_id)+1}: Yaw={yaw:.2f}, Pitch={pitch:.2f}"
|
|
135
|
+
cv2.putText(frame, text, (10, 30 + i * 25),
|
|
136
|
+
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
|
|
137
|
+
|
|
138
|
+
cv2.imshow("ImagePRO - Head Pose Estimation", frame)
|
|
139
|
+
|
|
140
|
+
if cv2.waitKey(5) & 0xFF == 27: # ESC
|
|
141
|
+
break
|
|
142
|
+
finally:
|
|
143
|
+
cap.release()
|
|
144
|
+
cv2.destroyAllWindows()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
estimate_head_pose_live()
|
|
File without changes
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
# Add parent directory to sys.path for custom module imports
|
|
8
|
+
parent_dir = Path(__file__).resolve().parent.parent
|
|
9
|
+
sys.path.append(str(parent_dir))
|
|
10
|
+
|
|
11
|
+
from io_handler import IOHandler
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def apply_average_blur(kernel_size=(5, 5), image_path=None, np_image=None, result_path=None):
|
|
15
|
+
"""
|
|
16
|
+
Apply average blur (box filter) to an image.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
kernel_size (tuple): (width, height) of the blur kernel.
|
|
20
|
+
image_path (str, optional): Path to input image.
|
|
21
|
+
np_image (np.ndarray, optional): Image array.
|
|
22
|
+
result_path (str, optional): Path to save output.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
str or np.ndarray: Saved message or blurred image.
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
TypeError, ValueError: On invalid inputs.
|
|
29
|
+
"""
|
|
30
|
+
if (
|
|
31
|
+
not isinstance(kernel_size, tuple)
|
|
32
|
+
or len(kernel_size) != 2
|
|
33
|
+
or not all(isinstance(k, int) and k > 0 for k in kernel_size)
|
|
34
|
+
):
|
|
35
|
+
raise ValueError("'kernel_size' must be a tuple of two positive integers.")
|
|
36
|
+
|
|
37
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
38
|
+
blurred = cv2.blur(np_image, kernel_size)
|
|
39
|
+
return IOHandler.save_image(blurred, result_path)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def apply_gaussian_blur(kernel_size=(5, 5), image_path=None, np_image=None, result_path=None):
|
|
43
|
+
"""
|
|
44
|
+
Apply Gaussian blur to an image.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
kernel_size (tuple): (width, height), both odd positive integers.
|
|
48
|
+
image_path (str, optional): Path to input image.
|
|
49
|
+
np_image (np.ndarray, optional): Image array.
|
|
50
|
+
result_path (str, optional): Path to save output.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
str or np.ndarray: Saved message or blurred image.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
TypeError, ValueError: On invalid inputs.
|
|
57
|
+
"""
|
|
58
|
+
if (
|
|
59
|
+
not isinstance(kernel_size, tuple)
|
|
60
|
+
or len(kernel_size) != 2
|
|
61
|
+
or not all(isinstance(k, int) and k > 0 and k % 2 == 1 for k in kernel_size)
|
|
62
|
+
):
|
|
63
|
+
raise ValueError("'kernel_size' must be a tuple of two odd positive integers.")
|
|
64
|
+
|
|
65
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
66
|
+
blurred = cv2.GaussianBlur(np_image, kernel_size, sigmaX=0)
|
|
67
|
+
return IOHandler.save_image(blurred, result_path)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def apply_median_blur(filter_size=5, image_path=None, np_image=None, result_path=None):
|
|
71
|
+
"""
|
|
72
|
+
Apply median blur to remove salt-and-pepper noise.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
filter_size (int): Must be an odd integer greater than 1.
|
|
76
|
+
image_path (str, optional): Path to input image.
|
|
77
|
+
np_image (np.ndarray, optional): Image array.
|
|
78
|
+
result_path (str, optional): Path to save output.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
str or np.ndarray: Saved message or blurred image.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
TypeError, ValueError: On invalid inputs.
|
|
85
|
+
"""
|
|
86
|
+
if not isinstance(filter_size, int):
|
|
87
|
+
raise TypeError("'filter_size' must be an integer.")
|
|
88
|
+
if filter_size <= 1 or filter_size % 2 == 0:
|
|
89
|
+
raise ValueError("'filter_size' must be an odd integer greater than 1.")
|
|
90
|
+
|
|
91
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
92
|
+
blurred = cv2.medianBlur(np_image, filter_size)
|
|
93
|
+
return IOHandler.save_image(blurred, result_path)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def apply_bilateral_blur(
|
|
97
|
+
filter_size=9,
|
|
98
|
+
sigma_color=75,
|
|
99
|
+
sigma_space=75,
|
|
100
|
+
image_path=None,
|
|
101
|
+
np_image=None,
|
|
102
|
+
result_path=None,
|
|
103
|
+
):
|
|
104
|
+
"""
|
|
105
|
+
Apply bilateral filter to smooth while preserving edges.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
filter_size (int): Diameter of pixel neighborhood.
|
|
109
|
+
sigma_color (float): Color-space standard deviation.
|
|
110
|
+
sigma_space (float): Coordinate-space standard deviation.
|
|
111
|
+
image_path (str, optional): Path to input image.
|
|
112
|
+
np_image (np.ndarray, optional): Image array.
|
|
113
|
+
result_path (str, optional): Path to save output.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
str or np.ndarray: Saved message or blurred image.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
TypeError, ValueError: On invalid inputs.
|
|
120
|
+
"""
|
|
121
|
+
if not isinstance(filter_size, int) or filter_size < 1:
|
|
122
|
+
raise ValueError("'filter_size' must be a positive integer.")
|
|
123
|
+
if not isinstance(sigma_color, (int, float)) or sigma_color <= 0:
|
|
124
|
+
raise ValueError("'sigma_color' must be a positive number.")
|
|
125
|
+
if not isinstance(sigma_space, (int, float)) or sigma_space <= 0:
|
|
126
|
+
raise ValueError("'sigma_space' must be a positive number.")
|
|
127
|
+
|
|
128
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
129
|
+
blurred = cv2.bilateralFilter(np_image, filter_size, sigma_color, sigma_space)
|
|
130
|
+
return IOHandler.save_image(blurred, result_path)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
# Add parent directory to sys.path for custom module imports
|
|
8
|
+
parent_dir = Path(__file__).resolve().parent.parent
|
|
9
|
+
sys.path.append(str(parent_dir))
|
|
10
|
+
|
|
11
|
+
from pre_processing.grayscale import convert_to_grayscale
|
|
12
|
+
from io_handler import IOHandler
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def apply_clahe_contrast(
|
|
16
|
+
clipLimit=2.0,
|
|
17
|
+
tileGridSize=(8, 8),
|
|
18
|
+
image_path=None,
|
|
19
|
+
np_image=None,
|
|
20
|
+
result_path=None,
|
|
21
|
+
):
|
|
22
|
+
"""
|
|
23
|
+
Enhance contrast using CLAHE (adaptive histogram equalization).
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
clipLimit (float): Contrast threshold (must be > 0).
|
|
27
|
+
tileGridSize (tuple): Grid size for local histogram (e.g., (8, 8)).
|
|
28
|
+
image_path (str, optional): Path to input image.
|
|
29
|
+
np_image (np.ndarray, optional): Image array.
|
|
30
|
+
result_path (str, optional): Path to save result.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
str or np.ndarray: Confirmation if saved, else enhanced image.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError, TypeError: On invalid parameter values.
|
|
37
|
+
"""
|
|
38
|
+
if not isinstance(clipLimit, (int, float)) or clipLimit <= 0:
|
|
39
|
+
raise ValueError("'clipLimit' must be a positive number.")
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
not isinstance(tileGridSize, tuple)
|
|
43
|
+
or len(tileGridSize) != 2
|
|
44
|
+
or not all(isinstance(i, int) and i > 0 for i in tileGridSize)
|
|
45
|
+
):
|
|
46
|
+
raise TypeError("'tileGridSize' must be a tuple of two positive integers.")
|
|
47
|
+
|
|
48
|
+
np_image = convert_to_grayscale(np_image=IOHandler.load_image(image_path=image_path, np_image=np_image))
|
|
49
|
+
|
|
50
|
+
clahe = cv2.createCLAHE(clipLimit=clipLimit, tileGridSize=tileGridSize)
|
|
51
|
+
enhanced = clahe.apply(np_image)
|
|
52
|
+
|
|
53
|
+
return IOHandler.save_image(enhanced, result_path)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def apply_histogram_equalization(image_path=None, np_image=None, result_path=None):
|
|
57
|
+
"""
|
|
58
|
+
Enhance contrast using global histogram equalization.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
image_path (str, optional): Path to input image.
|
|
62
|
+
np_image (np.ndarray, optional): Image array.
|
|
63
|
+
result_path (str, optional): Path to save result.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
str or np.ndarray: Confirmation if saved, else enhanced image.
|
|
67
|
+
"""
|
|
68
|
+
np_image = convert_to_grayscale(np_image=IOHandler.load_image(image_path=image_path, np_image=np_image))
|
|
69
|
+
enhanced = cv2.equalizeHist(np_image)
|
|
70
|
+
return IOHandler.save_image(enhanced, result_path)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def apply_contrast_stretching(
|
|
74
|
+
alpha,
|
|
75
|
+
beta,
|
|
76
|
+
image_path=None,
|
|
77
|
+
np_image=None,
|
|
78
|
+
result_path=None,
|
|
79
|
+
):
|
|
80
|
+
"""
|
|
81
|
+
Enhance contrast by linear contrast stretching (alpha × pixel + beta).
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
alpha (float): Contrast factor (>= 0).
|
|
85
|
+
beta (int): Brightness offset (0–255).
|
|
86
|
+
image_path (str, optional): Path to input image.
|
|
87
|
+
np_image (np.ndarray, optional): Image array.
|
|
88
|
+
result_path (str, optional): Path to save result.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
str or np.ndarray: Confirmation if saved, else enhanced image.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ValueError: If parameters are out of range.
|
|
95
|
+
"""
|
|
96
|
+
if not isinstance(alpha, (int, float)) or alpha < 0:
|
|
97
|
+
raise ValueError("'alpha' must be a non-negative number.")
|
|
98
|
+
|
|
99
|
+
if not isinstance(beta, int) or not (0 <= beta <= 255):
|
|
100
|
+
raise ValueError("'beta' must be an integer between 0 and 255.")
|
|
101
|
+
|
|
102
|
+
np_image = convert_to_grayscale(np_image=IOHandler.load_image(image_path=image_path, np_image=np_image))
|
|
103
|
+
enhanced = cv2.convertScaleAbs(np_image, alpha=alpha, beta=beta)
|
|
104
|
+
|
|
105
|
+
return IOHandler.save_image(enhanced, result_path)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
# Add parent directory to sys.path for importing custom modules
|
|
5
|
+
parent_dir = Path(__file__).resolve().parent.parent
|
|
6
|
+
sys.path.append(str(parent_dir))
|
|
7
|
+
|
|
8
|
+
from io_handler import IOHandler
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def crop_image(start_point, end_point, image_path=None, np_image=None, result_path=None):
|
|
12
|
+
"""
|
|
13
|
+
Crop image using top-left and bottom-right coordinates.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
start_point (tuple): (x1, y1) top-left corner.
|
|
17
|
+
end_point (tuple): (x2, y2) bottom-right corner.
|
|
18
|
+
image_path (str, optional): Path to input image. Overrides `np_image` if provided.
|
|
19
|
+
np_image (np.ndarray, optional): Image array. Used if `image_path` is None.
|
|
20
|
+
result_path (str, optional): Path to save cropped image.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
str or np.ndarray: Save message if saved, else cropped image array.
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
TypeError: If inputs are not proper tuples of integers.
|
|
27
|
+
ValueError: If coordinates are invalid or out of image bounds.
|
|
28
|
+
"""
|
|
29
|
+
# Validate coordinates
|
|
30
|
+
if (
|
|
31
|
+
not isinstance(start_point, tuple) or
|
|
32
|
+
not isinstance(end_point, tuple) or
|
|
33
|
+
len(start_point) != 2 or len(end_point) != 2 or
|
|
34
|
+
not all(isinstance(c, int) for c in start_point + end_point)
|
|
35
|
+
):
|
|
36
|
+
raise TypeError("Both 'start_point' and 'end_point' must be (x, y) tuples of integers.")
|
|
37
|
+
|
|
38
|
+
x1, y1 = start_point
|
|
39
|
+
x2, y2 = end_point
|
|
40
|
+
|
|
41
|
+
if x1 < 0 or y1 < 0 or x2 <= x1 or y2 <= y1:
|
|
42
|
+
raise ValueError("Invalid crop coordinates: ensure (x1, y1) is top-left and (x2, y2) is bottom-right.")
|
|
43
|
+
|
|
44
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
45
|
+
height, width = np_image.shape[:2]
|
|
46
|
+
|
|
47
|
+
if x2 > width or y2 > height:
|
|
48
|
+
raise ValueError(f"Crop area exceeds image bounds ({width}x{height}).")
|
|
49
|
+
|
|
50
|
+
cropped = np_image[y1:y2, x1:x2]
|
|
51
|
+
return IOHandler.save_image(cropped, result_path)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
|
|
6
|
+
# Add parent directory to sys.path for importing custom modules
|
|
7
|
+
parent_dir = Path(__file__).resolve().parent.parent
|
|
8
|
+
sys.path.append(str(parent_dir))
|
|
9
|
+
|
|
10
|
+
from io_handler import IOHandler
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def convert_to_grayscale(image_path=None, np_image=None, result_path=None):
|
|
14
|
+
"""
|
|
15
|
+
Convert a BGR image to single-channel grayscale.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
image_path (str, optional): Path to input image file.
|
|
19
|
+
np_image (np.ndarray, optional): Image array (used if image_path is None).
|
|
20
|
+
result_path (str, optional): Path to save grayscale image.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
str or np.ndarray: Confirmation message if saved, else grayscale image.
|
|
24
|
+
"""
|
|
25
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
26
|
+
grayscale = cv2.cvtColor(np_image, cv2.COLOR_BGR2GRAY)
|
|
27
|
+
return IOHandler.save_image(grayscale, result_path)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
|
|
6
|
+
# Add parent directory to sys.path for importing custom modules
|
|
7
|
+
parent_dir = Path(__file__).resolve().parent.parent
|
|
8
|
+
sys.path.append(str(parent_dir))
|
|
9
|
+
|
|
10
|
+
from io_handler import IOHandler
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def resize_image(new_size, image_path=None, np_image=None, result_path=None):
|
|
14
|
+
"""
|
|
15
|
+
Resize image to specified dimensions.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
new_size (tuple): (width, height), both positive integers.
|
|
19
|
+
image_path (str, optional): Path to input image. Overrides `np_image` if given.
|
|
20
|
+
np_image (np.ndarray, optional): Image array (used if image_path is None).
|
|
21
|
+
result_path (str, optional): Save path. If None, returns resized image.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
str or np.ndarray: Confirmation message if saved, else resized image.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
TypeError: If new_size is not a tuple of two integers.
|
|
28
|
+
ValueError: If width or height are not positive.
|
|
29
|
+
"""
|
|
30
|
+
if (
|
|
31
|
+
not isinstance(new_size, tuple) or
|
|
32
|
+
len(new_size) != 2 or
|
|
33
|
+
not all(isinstance(x, int) for x in new_size)
|
|
34
|
+
):
|
|
35
|
+
raise TypeError("'new_size' must be a tuple of two integers: (width, height).")
|
|
36
|
+
|
|
37
|
+
if new_size[0] <= 0 or new_size[1] <= 0:
|
|
38
|
+
raise ValueError("Both width and height must be positive integers.")
|
|
39
|
+
|
|
40
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
41
|
+
resized = cv2.resize(np_image, dsize=new_size)
|
|
42
|
+
|
|
43
|
+
return IOHandler.save_image(resized, result_path)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
|
|
6
|
+
# Add parent directory to sys.path for custom imports
|
|
7
|
+
parent_dir = Path(__file__).resolve().parent.parent
|
|
8
|
+
sys.path.append(str(parent_dir))
|
|
9
|
+
|
|
10
|
+
from io_handler import IOHandler
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def rotate_image_90(image_path=None, np_image=None, result_path=None):
|
|
14
|
+
"""
|
|
15
|
+
Rotate image 90 degrees clockwise.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
image_path (str, optional): Path to input image file.
|
|
19
|
+
np_image (np.ndarray, optional): Image as NumPy array.
|
|
20
|
+
result_path (str, optional): Path to save rotated image.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
str or np.ndarray: Save message if result_path is given, else rotated image.
|
|
24
|
+
"""
|
|
25
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
26
|
+
rotated = cv2.rotate(np_image, cv2.ROTATE_90_CLOCKWISE)
|
|
27
|
+
return IOHandler.save_image(rotated, result_path)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def rotate_image_180(image_path=None, np_image=None, result_path=None):
|
|
31
|
+
"""
|
|
32
|
+
Rotate image 180 degrees.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
image_path (str, optional): Path to input image file.
|
|
36
|
+
np_image (np.ndarray, optional): Image as NumPy array.
|
|
37
|
+
result_path (str, optional): Path to save rotated image.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
str or np.ndarray: Save message if result_path is given, else rotated image.
|
|
41
|
+
"""
|
|
42
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
43
|
+
rotated = cv2.rotate(np_image, cv2.ROTATE_180)
|
|
44
|
+
return IOHandler.save_image(rotated, result_path)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def rotate_image_270(image_path=None, np_image=None, result_path=None):
|
|
48
|
+
"""
|
|
49
|
+
Rotate image 270 degrees clockwise (same as 90 degrees counter-clockwise).
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
image_path (str, optional): Path to input image file.
|
|
53
|
+
np_image (np.ndarray, optional): Image as NumPy array.
|
|
54
|
+
result_path (str, optional): Path to save rotated image.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
str or np.ndarray: Save message if result_path is given, else rotated image.
|
|
58
|
+
"""
|
|
59
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
60
|
+
rotated = cv2.rotate(np_image, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
|
61
|
+
return IOHandler.save_image(rotated, result_path)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def rotate_image_custom(angle, scale=1.0, image_path=None, np_image=None, result_path=None):
|
|
65
|
+
"""
|
|
66
|
+
Rotate image by custom angle around its center, with optional scaling.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
angle (float): Rotation angle in degrees (positive = counter-clockwise).
|
|
70
|
+
scale (float): Scale factor (default = 1.0).
|
|
71
|
+
image_path (str, optional): Path to input image file.
|
|
72
|
+
np_image (np.ndarray, optional): Image as NumPy array.
|
|
73
|
+
result_path (str, optional): Path to save rotated image.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
str or np.ndarray: Save message if result_path is given, else rotated image.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
TypeError: If angle or scale are of incorrect type.
|
|
80
|
+
ValueError: If scale is non-positive.
|
|
81
|
+
"""
|
|
82
|
+
if not isinstance(angle, (int, float)):
|
|
83
|
+
raise TypeError("'angle' must be a number.")
|
|
84
|
+
if not isinstance(scale, (int, float)) or scale <= 0:
|
|
85
|
+
raise ValueError("'scale' must be a positive number.")
|
|
86
|
+
|
|
87
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
88
|
+
|
|
89
|
+
height, width = np_image.shape[:2]
|
|
90
|
+
center = (width / 2, height / 2)
|
|
91
|
+
matrix = cv2.getRotationMatrix2D(center=center, angle=angle, scale=scale)
|
|
92
|
+
rotated = cv2.warpAffine(np_image, matrix, (width, height))
|
|
93
|
+
|
|
94
|
+
return IOHandler.save_image(rotated, result_path)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
# Add parent directory to path for importing local modules
|
|
8
|
+
parent_dir = Path(__file__).resolve().parent.parent
|
|
9
|
+
sys.path.append(str(parent_dir))
|
|
10
|
+
|
|
11
|
+
from io_handler import IOHandler
|
|
12
|
+
from blur import apply_average_blur
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def apply_laplacian_sharpening(
|
|
16
|
+
laplacian_coefficient=3, image_path=None, np_image=None, result_path=None
|
|
17
|
+
):
|
|
18
|
+
"""
|
|
19
|
+
Apply Laplacian filter to enhance image sharpness.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
laplacian_coefficient (float): Intensity of sharpening effect (>= 0).
|
|
23
|
+
image_path (str, optional): Path to input image. Overrides `np_image` if given.
|
|
24
|
+
np_image (np.ndarray, optional): Image as NumPy array.
|
|
25
|
+
result_path (str, optional): Path to save the result.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
str or np.ndarray: Message if saved, else the sharpened image array.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ValueError: If `laplacian_coefficient` is negative or inputs are invalid.
|
|
32
|
+
"""
|
|
33
|
+
if not isinstance(laplacian_coefficient, (int, float)) or laplacian_coefficient < 0:
|
|
34
|
+
raise ValueError("'laplacian_coefficient' must be a non-negative number.")
|
|
35
|
+
|
|
36
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
37
|
+
|
|
38
|
+
laplacian = cv2.Laplacian(np_image, cv2.CV_64F)
|
|
39
|
+
laplacian = np.uint8(np.absolute(laplacian))
|
|
40
|
+
|
|
41
|
+
sharpened = np_image + laplacian_coefficient * laplacian
|
|
42
|
+
sharpened = np.uint8(np.clip(sharpened, 0, 255))
|
|
43
|
+
|
|
44
|
+
return IOHandler.save_image(sharpened, result_path)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def apply_unsharp_masking(
|
|
48
|
+
coefficient=1, image_path=None, np_image=None, result_path=None
|
|
49
|
+
):
|
|
50
|
+
"""
|
|
51
|
+
Apply Unsharp Masking to enhance image sharpness.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
coefficient (float): Intensity of sharpening effect (>= 0).
|
|
55
|
+
image_path (str, optional): Path to input image. Overrides `np_image` if given.
|
|
56
|
+
np_image (np.ndarray, optional): Image as NumPy array.
|
|
57
|
+
result_path (str, optional): Path to save the result.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
str or np.ndarray: Message if saved, else the sharpened image array.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ValueError: If `coefficient` is negative or inputs are invalid.
|
|
64
|
+
"""
|
|
65
|
+
if not isinstance(coefficient, (int, float)) or coefficient < 0:
|
|
66
|
+
raise ValueError("'coefficient' must be a non-negative number.")
|
|
67
|
+
|
|
68
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
69
|
+
blurred = apply_average_blur(np_image=np_image)
|
|
70
|
+
|
|
71
|
+
mask = cv2.subtract(np_image, blurred)
|
|
72
|
+
sharpened = cv2.addWeighted(np_image, 1 + coefficient, mask, -coefficient, 0)
|
|
73
|
+
|
|
74
|
+
return IOHandler.save_image(sharpened, result_path)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ImagePRO-Python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: clean, modular, and easy-to-use Python library for image processing tasks.
|
|
5
|
+
Author-email: Parsa Safaie <parsasafaie.2568@proton.me>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: opencv,mediapipe,computer-vision,face,pose
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
12
|
+
Requires-Python: ==3.10.*
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: numpy<3.0,>=1.24
|
|
16
|
+
Requires-Dist: opencv-python<5.0,>=4.8
|
|
17
|
+
Dynamic: license-file
|