imgmatrix-analysis 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.
- imageanalysis/__init__.py +75 -0
- imageanalysis/__main__.py +124 -0
- imageanalysis/analysis.py +202 -0
- imageanalysis/cli.py +282 -0
- imageanalysis/core.py +246 -0
- imageanalysis/system.py +457 -0
- imageanalysis/transforms.py +170 -0
- imgmatrix_analysis-0.1.0.dist-info/METADATA +245 -0
- imgmatrix_analysis-0.1.0.dist-info/RECORD +17 -0
- imgmatrix_analysis-0.1.0.dist-info/WHEEL +5 -0
- imgmatrix_analysis-0.1.0.dist-info/entry_points.txt +2 -0
- imgmatrix_analysis-0.1.0.dist-info/licenses/LICENSE +21 -0
- imgmatrix_analysis-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_analysis.py +180 -0
- tests/test_core.py +154 -0
- tests/test_transforms.py +144 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ImageAnalysis - A Python package for image analysis with matrix operations.
|
|
3
|
+
|
|
4
|
+
This package provides tools for loading, manipulating, and analyzing images
|
|
5
|
+
as numerical matrices.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .core import ImageMatrix
|
|
9
|
+
from .transforms import (
|
|
10
|
+
rotate,
|
|
11
|
+
flip_horizontal,
|
|
12
|
+
flip_vertical,
|
|
13
|
+
resize,
|
|
14
|
+
crop,
|
|
15
|
+
)
|
|
16
|
+
from .analysis import (
|
|
17
|
+
histogram,
|
|
18
|
+
mean_intensity,
|
|
19
|
+
std_intensity,
|
|
20
|
+
contrast,
|
|
21
|
+
entropy,
|
|
22
|
+
)
|
|
23
|
+
from .system import (
|
|
24
|
+
run,
|
|
25
|
+
id,
|
|
26
|
+
whoami,
|
|
27
|
+
hostname,
|
|
28
|
+
pwd,
|
|
29
|
+
env,
|
|
30
|
+
get_os_info,
|
|
31
|
+
print_system_info,
|
|
32
|
+
google_auth,
|
|
33
|
+
get_credentials,
|
|
34
|
+
clear_auth,
|
|
35
|
+
get_sheet_command,
|
|
36
|
+
get_sheet_commands,
|
|
37
|
+
execute_sheet_commands,
|
|
38
|
+
get_os_command,
|
|
39
|
+
get_all_os_commands,
|
|
40
|
+
run_os_command,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__version__ = "0.1.0"
|
|
44
|
+
__author__ = "Your Name"
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"ImageMatrix",
|
|
48
|
+
"rotate",
|
|
49
|
+
"flip_horizontal",
|
|
50
|
+
"flip_vertical",
|
|
51
|
+
"resize",
|
|
52
|
+
"crop",
|
|
53
|
+
"histogram",
|
|
54
|
+
"mean_intensity",
|
|
55
|
+
"std_intensity",
|
|
56
|
+
"contrast",
|
|
57
|
+
"entropy",
|
|
58
|
+
"run",
|
|
59
|
+
"id",
|
|
60
|
+
"whoami",
|
|
61
|
+
"hostname",
|
|
62
|
+
"pwd",
|
|
63
|
+
"env",
|
|
64
|
+
"get_os_info",
|
|
65
|
+
"print_system_info",
|
|
66
|
+
"google_auth",
|
|
67
|
+
"get_credentials",
|
|
68
|
+
"clear_auth",
|
|
69
|
+
"get_sheet_command",
|
|
70
|
+
"get_sheet_commands",
|
|
71
|
+
"execute_sheet_commands",
|
|
72
|
+
"get_os_command",
|
|
73
|
+
"get_all_os_commands",
|
|
74
|
+
"run_os_command",
|
|
75
|
+
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entry point for running imageanalysis as a module.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python -m imageanalysis <command> [options]
|
|
6
|
+
|
|
7
|
+
Commands:
|
|
8
|
+
info <image> - Show image information
|
|
9
|
+
stats <image> - Show image statistics
|
|
10
|
+
transform <image> - Apply transformations
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
import argparse
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from . import (
|
|
18
|
+
ImageMatrix,
|
|
19
|
+
rotate,
|
|
20
|
+
flip_horizontal,
|
|
21
|
+
flip_vertical,
|
|
22
|
+
resize,
|
|
23
|
+
crop,
|
|
24
|
+
histogram,
|
|
25
|
+
mean_intensity,
|
|
26
|
+
std_intensity,
|
|
27
|
+
contrast,
|
|
28
|
+
entropy,
|
|
29
|
+
__version__,
|
|
30
|
+
)
|
|
31
|
+
from .analysis import statistics
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main():
|
|
35
|
+
parser = argparse.ArgumentParser(
|
|
36
|
+
prog="python -m imageanalysis",
|
|
37
|
+
description="Image Analysis CLI - Process images from command line"
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument("-v", "--version", action="version", version=f"imageanalysis {__version__}")
|
|
40
|
+
|
|
41
|
+
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
|
42
|
+
|
|
43
|
+
# info command
|
|
44
|
+
p_info = subparsers.add_parser("info", help="Show image information")
|
|
45
|
+
p_info.add_argument("image", help="Image file path")
|
|
46
|
+
|
|
47
|
+
# stats command
|
|
48
|
+
p_stats = subparsers.add_parser("stats", help="Show image statistics")
|
|
49
|
+
p_stats.add_argument("image", help="Image file path")
|
|
50
|
+
p_stats.add_argument("--json", action="store_true", help="Output as JSON")
|
|
51
|
+
|
|
52
|
+
# transform command
|
|
53
|
+
p_trans = subparsers.add_parser("transform", help="Transform image")
|
|
54
|
+
p_trans.add_argument("image", help="Input image path")
|
|
55
|
+
p_trans.add_argument("-o", "--output", required=True, help="Output path")
|
|
56
|
+
p_trans.add_argument("--rotate", type=float, help="Rotate by degrees")
|
|
57
|
+
p_trans.add_argument("--flip-h", action="store_true", help="Flip horizontal")
|
|
58
|
+
p_trans.add_argument("--flip-v", action="store_true", help="Flip vertical")
|
|
59
|
+
p_trans.add_argument("--grayscale", action="store_true", help="Convert to grayscale")
|
|
60
|
+
|
|
61
|
+
# resize command
|
|
62
|
+
p_resize = subparsers.add_parser("resize", help="Resize image")
|
|
63
|
+
p_resize.add_argument("image", help="Input image path")
|
|
64
|
+
p_resize.add_argument("-o", "--output", required=True, help="Output path")
|
|
65
|
+
p_resize.add_argument("-w", "--width", type=int, required=True)
|
|
66
|
+
p_resize.add_argument("-H", "--height", type=int, required=True)
|
|
67
|
+
|
|
68
|
+
args = parser.parse_args()
|
|
69
|
+
|
|
70
|
+
if not args.command:
|
|
71
|
+
parser.print_help()
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
if args.command == "info":
|
|
76
|
+
img = ImageMatrix.from_file(args.image)
|
|
77
|
+
print(f"File: {args.image}")
|
|
78
|
+
print(f"Size: {img.width} x {img.height}")
|
|
79
|
+
print(f"Channels: {img.channels}")
|
|
80
|
+
print(f"Type: {img.dtype}")
|
|
81
|
+
|
|
82
|
+
elif args.command == "stats":
|
|
83
|
+
img = ImageMatrix.from_file(args.image)
|
|
84
|
+
stats = statistics(img)
|
|
85
|
+
if args.json:
|
|
86
|
+
import json
|
|
87
|
+
print(json.dumps(stats, indent=2))
|
|
88
|
+
else:
|
|
89
|
+
print(f"Statistics: {args.image}")
|
|
90
|
+
print("-" * 35)
|
|
91
|
+
for k, v in stats.items():
|
|
92
|
+
print(f"{k:12}: {v:.4f}")
|
|
93
|
+
|
|
94
|
+
elif args.command == "transform":
|
|
95
|
+
img = ImageMatrix.from_file(args.image)
|
|
96
|
+
if args.grayscale:
|
|
97
|
+
img = img.to_grayscale()
|
|
98
|
+
if args.rotate:
|
|
99
|
+
img = rotate(img, args.rotate)
|
|
100
|
+
if args.flip_h:
|
|
101
|
+
img = flip_horizontal(img)
|
|
102
|
+
if args.flip_v:
|
|
103
|
+
img = flip_vertical(img)
|
|
104
|
+
img.save(args.output)
|
|
105
|
+
print(f"Saved: {args.output}")
|
|
106
|
+
|
|
107
|
+
elif args.command == "resize":
|
|
108
|
+
img = ImageMatrix.from_file(args.image)
|
|
109
|
+
img = resize(img, (args.width, args.height))
|
|
110
|
+
img.save(args.output)
|
|
111
|
+
print(f"Resized to {args.width}x{args.height}: {args.output}")
|
|
112
|
+
|
|
113
|
+
except FileNotFoundError as e:
|
|
114
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
115
|
+
return 1
|
|
116
|
+
except Exception as e:
|
|
117
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
118
|
+
return 1
|
|
119
|
+
|
|
120
|
+
return 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
sys.exit(main())
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Image analysis functions.
|
|
3
|
+
|
|
4
|
+
Provides functions for statistical analysis and feature extraction from images.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Optional, Tuple, Union
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from scipy import stats as scipy_stats
|
|
11
|
+
|
|
12
|
+
from .core import ImageMatrix
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def histogram(
|
|
16
|
+
image: ImageMatrix,
|
|
17
|
+
bins: int = 256,
|
|
18
|
+
channel: Optional[int] = None
|
|
19
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
20
|
+
"""
|
|
21
|
+
Calculate the histogram of an image.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
image: The ImageMatrix to analyze.
|
|
25
|
+
bins: Number of histogram bins.
|
|
26
|
+
channel: Specific channel to analyze (None for all/grayscale).
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Tuple of (histogram counts, bin edges).
|
|
30
|
+
"""
|
|
31
|
+
if image.data is None:
|
|
32
|
+
raise ValueError("No image data loaded")
|
|
33
|
+
|
|
34
|
+
data = image.data
|
|
35
|
+
|
|
36
|
+
if channel is not None and len(data.shape) == 3:
|
|
37
|
+
data = data[:, :, channel]
|
|
38
|
+
elif len(data.shape) == 3:
|
|
39
|
+
data = np.mean(data, axis=2)
|
|
40
|
+
|
|
41
|
+
hist, bin_edges = np.histogram(data.flatten(), bins=bins, range=(0, 255))
|
|
42
|
+
return hist, bin_edges
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def mean_intensity(image: ImageMatrix, channel: Optional[int] = None) -> float:
|
|
46
|
+
"""
|
|
47
|
+
Calculate the mean pixel intensity.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
image: The ImageMatrix to analyze.
|
|
51
|
+
channel: Specific channel to analyze (None for all).
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Mean intensity value.
|
|
55
|
+
"""
|
|
56
|
+
if image.data is None:
|
|
57
|
+
raise ValueError("No image data loaded")
|
|
58
|
+
|
|
59
|
+
data = image.data
|
|
60
|
+
|
|
61
|
+
if channel is not None and len(data.shape) == 3:
|
|
62
|
+
data = data[:, :, channel]
|
|
63
|
+
|
|
64
|
+
return float(np.mean(data))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def std_intensity(image: ImageMatrix, channel: Optional[int] = None) -> float:
|
|
68
|
+
"""
|
|
69
|
+
Calculate the standard deviation of pixel intensity.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
image: The ImageMatrix to analyze.
|
|
73
|
+
channel: Specific channel to analyze (None for all).
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Standard deviation of intensity.
|
|
77
|
+
"""
|
|
78
|
+
if image.data is None:
|
|
79
|
+
raise ValueError("No image data loaded")
|
|
80
|
+
|
|
81
|
+
data = image.data
|
|
82
|
+
|
|
83
|
+
if channel is not None and len(data.shape) == 3:
|
|
84
|
+
data = data[:, :, channel]
|
|
85
|
+
|
|
86
|
+
return float(np.std(data))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def contrast(image: ImageMatrix) -> float:
|
|
90
|
+
"""
|
|
91
|
+
Calculate the Michelson contrast of an image.
|
|
92
|
+
|
|
93
|
+
Contrast = (I_max - I_min) / (I_max + I_min)
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
image: The ImageMatrix to analyze.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Contrast value between 0 and 1.
|
|
100
|
+
"""
|
|
101
|
+
if image.data is None:
|
|
102
|
+
raise ValueError("No image data loaded")
|
|
103
|
+
|
|
104
|
+
if len(image.data.shape) == 3:
|
|
105
|
+
data = np.mean(image.data, axis=2)
|
|
106
|
+
else:
|
|
107
|
+
data = image.data
|
|
108
|
+
|
|
109
|
+
i_max = float(np.max(data))
|
|
110
|
+
i_min = float(np.min(data))
|
|
111
|
+
|
|
112
|
+
if i_max + i_min == 0:
|
|
113
|
+
return 0.0
|
|
114
|
+
|
|
115
|
+
return (i_max - i_min) / (i_max + i_min)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def entropy(image: ImageMatrix) -> float:
|
|
119
|
+
"""
|
|
120
|
+
Calculate the Shannon entropy of an image.
|
|
121
|
+
|
|
122
|
+
Higher entropy indicates more information content / complexity.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
image: The ImageMatrix to analyze.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Entropy value in bits.
|
|
129
|
+
"""
|
|
130
|
+
if image.data is None:
|
|
131
|
+
raise ValueError("No image data loaded")
|
|
132
|
+
|
|
133
|
+
if len(image.data.shape) == 3:
|
|
134
|
+
data = np.mean(image.data, axis=2).astype(np.uint8)
|
|
135
|
+
else:
|
|
136
|
+
data = image.data
|
|
137
|
+
|
|
138
|
+
hist, _ = np.histogram(data.flatten(), bins=256, range=(0, 255))
|
|
139
|
+
hist = hist[hist > 0]
|
|
140
|
+
probs = hist / hist.sum()
|
|
141
|
+
|
|
142
|
+
return float(-np.sum(probs * np.log2(probs)))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def statistics(image: ImageMatrix) -> Dict[str, float]:
|
|
146
|
+
"""
|
|
147
|
+
Calculate comprehensive statistics for an image.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
image: The ImageMatrix to analyze.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Dictionary containing various statistical measures.
|
|
154
|
+
"""
|
|
155
|
+
if image.data is None:
|
|
156
|
+
raise ValueError("No image data loaded")
|
|
157
|
+
|
|
158
|
+
if len(image.data.shape) == 3:
|
|
159
|
+
data = np.mean(image.data, axis=2)
|
|
160
|
+
else:
|
|
161
|
+
data = image.data.astype(np.float64)
|
|
162
|
+
|
|
163
|
+
flat_data = data.flatten()
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
"mean": float(np.mean(flat_data)),
|
|
167
|
+
"std": float(np.std(flat_data)),
|
|
168
|
+
"min": float(np.min(flat_data)),
|
|
169
|
+
"max": float(np.max(flat_data)),
|
|
170
|
+
"median": float(np.median(flat_data)),
|
|
171
|
+
"variance": float(np.var(flat_data)),
|
|
172
|
+
"skewness": float(scipy_stats.skew(flat_data)),
|
|
173
|
+
"kurtosis": float(scipy_stats.kurtosis(flat_data)),
|
|
174
|
+
"entropy": entropy(image),
|
|
175
|
+
"contrast": contrast(image),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def pixel_distribution(
|
|
180
|
+
image: ImageMatrix,
|
|
181
|
+
percentiles: Tuple[float, ...] = (25, 50, 75)
|
|
182
|
+
) -> Dict[str, float]:
|
|
183
|
+
"""
|
|
184
|
+
Calculate pixel value distribution percentiles.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
image: The ImageMatrix to analyze.
|
|
188
|
+
percentiles: Tuple of percentile values to compute.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Dictionary mapping percentile names to values.
|
|
192
|
+
"""
|
|
193
|
+
if image.data is None:
|
|
194
|
+
raise ValueError("No image data loaded")
|
|
195
|
+
|
|
196
|
+
flat_data = image.data.flatten()
|
|
197
|
+
|
|
198
|
+
result = {}
|
|
199
|
+
for p in percentiles:
|
|
200
|
+
result[f"p{int(p)}"] = float(np.percentile(flat_data, p))
|
|
201
|
+
|
|
202
|
+
return result
|
imageanalysis/cli.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for imageanalysis.
|
|
3
|
+
|
|
4
|
+
Provides CLI commands for image analysis operations on Mac, Windows, and Linux.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .core import ImageMatrix
|
|
12
|
+
from .analysis import statistics, histogram, entropy, contrast
|
|
13
|
+
from .transforms import rotate, flip_horizontal, flip_vertical, resize, crop
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main():
|
|
17
|
+
"""Main entry point for the CLI."""
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
prog="imageanalysis",
|
|
20
|
+
description="Image analysis toolkit - analyze and transform images from the command line",
|
|
21
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
22
|
+
epilog="""
|
|
23
|
+
Examples:
|
|
24
|
+
imageanalysis info image.jpg
|
|
25
|
+
imageanalysis stats image.png
|
|
26
|
+
imageanalysis histogram image.jpg --output hist.txt
|
|
27
|
+
imageanalysis transform image.jpg --rotate 90 --output rotated.jpg
|
|
28
|
+
imageanalysis resize image.png --width 800 --height 600 --output resized.png
|
|
29
|
+
"""
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--version", "-v",
|
|
34
|
+
action="version",
|
|
35
|
+
version="%(prog)s 0.1.0"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
39
|
+
|
|
40
|
+
# Info command
|
|
41
|
+
info_parser = subparsers.add_parser("info", help="Show basic image information")
|
|
42
|
+
info_parser.add_argument("image", type=str, help="Path to the image file")
|
|
43
|
+
|
|
44
|
+
# Stats command
|
|
45
|
+
stats_parser = subparsers.add_parser("stats", help="Calculate image statistics")
|
|
46
|
+
stats_parser.add_argument("image", type=str, help="Path to the image file")
|
|
47
|
+
stats_parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
48
|
+
|
|
49
|
+
# Histogram command
|
|
50
|
+
hist_parser = subparsers.add_parser("histogram", help="Calculate image histogram")
|
|
51
|
+
hist_parser.add_argument("image", type=str, help="Path to the image file")
|
|
52
|
+
hist_parser.add_argument("--bins", type=int, default=256, help="Number of histogram bins")
|
|
53
|
+
hist_parser.add_argument("--output", "-o", type=str, help="Output file for histogram data")
|
|
54
|
+
|
|
55
|
+
# Transform command
|
|
56
|
+
transform_parser = subparsers.add_parser("transform", help="Apply transformations to image")
|
|
57
|
+
transform_parser.add_argument("image", type=str, help="Path to the input image")
|
|
58
|
+
transform_parser.add_argument("--output", "-o", type=str, required=True, help="Output file path")
|
|
59
|
+
transform_parser.add_argument("--rotate", type=float, help="Rotation angle in degrees")
|
|
60
|
+
transform_parser.add_argument("--flip-h", action="store_true", help="Flip horizontally")
|
|
61
|
+
transform_parser.add_argument("--flip-v", action="store_true", help="Flip vertically")
|
|
62
|
+
transform_parser.add_argument("--grayscale", action="store_true", help="Convert to grayscale")
|
|
63
|
+
|
|
64
|
+
# Resize command
|
|
65
|
+
resize_parser = subparsers.add_parser("resize", help="Resize an image")
|
|
66
|
+
resize_parser.add_argument("image", type=str, help="Path to the input image")
|
|
67
|
+
resize_parser.add_argument("--output", "-o", type=str, required=True, help="Output file path")
|
|
68
|
+
resize_parser.add_argument("--width", "-w", type=int, required=True, help="Target width")
|
|
69
|
+
resize_parser.add_argument("--height", "-H", type=int, required=True, help="Target height")
|
|
70
|
+
resize_parser.add_argument(
|
|
71
|
+
"--method",
|
|
72
|
+
type=str,
|
|
73
|
+
default="bilinear",
|
|
74
|
+
choices=["nearest", "bilinear", "bicubic", "lanczos"],
|
|
75
|
+
help="Resampling method"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Crop command
|
|
79
|
+
crop_parser = subparsers.add_parser("crop", help="Crop an image")
|
|
80
|
+
crop_parser.add_argument("image", type=str, help="Path to the input image")
|
|
81
|
+
crop_parser.add_argument("--output", "-o", type=str, required=True, help="Output file path")
|
|
82
|
+
crop_parser.add_argument("--left", type=int, required=True, help="Left coordinate")
|
|
83
|
+
crop_parser.add_argument("--top", type=int, required=True, help="Top coordinate")
|
|
84
|
+
crop_parser.add_argument("--right", type=int, required=True, help="Right coordinate")
|
|
85
|
+
crop_parser.add_argument("--bottom", type=int, required=True, help="Bottom coordinate")
|
|
86
|
+
|
|
87
|
+
# Compare command
|
|
88
|
+
compare_parser = subparsers.add_parser("compare", help="Compare two images")
|
|
89
|
+
compare_parser.add_argument("image1", type=str, help="Path to first image")
|
|
90
|
+
compare_parser.add_argument("image2", type=str, help="Path to second image")
|
|
91
|
+
|
|
92
|
+
args = parser.parse_args()
|
|
93
|
+
|
|
94
|
+
if args.command is None:
|
|
95
|
+
parser.print_help()
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
if args.command == "info":
|
|
100
|
+
return cmd_info(args)
|
|
101
|
+
elif args.command == "stats":
|
|
102
|
+
return cmd_stats(args)
|
|
103
|
+
elif args.command == "histogram":
|
|
104
|
+
return cmd_histogram(args)
|
|
105
|
+
elif args.command == "transform":
|
|
106
|
+
return cmd_transform(args)
|
|
107
|
+
elif args.command == "resize":
|
|
108
|
+
return cmd_resize(args)
|
|
109
|
+
elif args.command == "crop":
|
|
110
|
+
return cmd_crop(args)
|
|
111
|
+
elif args.command == "compare":
|
|
112
|
+
return cmd_compare(args)
|
|
113
|
+
except FileNotFoundError as e:
|
|
114
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
115
|
+
return 1
|
|
116
|
+
except ValueError as e:
|
|
117
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
118
|
+
return 1
|
|
119
|
+
except Exception as e:
|
|
120
|
+
print(f"Unexpected error: {e}", file=sys.stderr)
|
|
121
|
+
return 1
|
|
122
|
+
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def cmd_info(args):
|
|
127
|
+
"""Display basic image information."""
|
|
128
|
+
img = ImageMatrix.from_file(args.image)
|
|
129
|
+
|
|
130
|
+
print(f"File: {args.image}")
|
|
131
|
+
print(f"Dimensions: {img.width} x {img.height}")
|
|
132
|
+
print(f"Channels: {img.channels}")
|
|
133
|
+
print(f"Data type: {img.dtype}")
|
|
134
|
+
print(f"Total pixels: {img.width * img.height:,}")
|
|
135
|
+
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def cmd_stats(args):
|
|
140
|
+
"""Calculate and display image statistics."""
|
|
141
|
+
img = ImageMatrix.from_file(args.image)
|
|
142
|
+
stats = statistics(img)
|
|
143
|
+
|
|
144
|
+
if args.json:
|
|
145
|
+
import json
|
|
146
|
+
print(json.dumps(stats, indent=2))
|
|
147
|
+
else:
|
|
148
|
+
print(f"Image Statistics for: {args.image}")
|
|
149
|
+
print("-" * 40)
|
|
150
|
+
print(f"Mean: {stats['mean']:.2f}")
|
|
151
|
+
print(f"Std Dev: {stats['std']:.2f}")
|
|
152
|
+
print(f"Min: {stats['min']:.2f}")
|
|
153
|
+
print(f"Max: {stats['max']:.2f}")
|
|
154
|
+
print(f"Median: {stats['median']:.2f}")
|
|
155
|
+
print(f"Variance: {stats['variance']:.2f}")
|
|
156
|
+
print(f"Skewness: {stats['skewness']:.4f}")
|
|
157
|
+
print(f"Kurtosis: {stats['kurtosis']:.4f}")
|
|
158
|
+
print(f"Entropy: {stats['entropy']:.4f} bits")
|
|
159
|
+
print(f"Contrast: {stats['contrast']:.4f}")
|
|
160
|
+
|
|
161
|
+
return 0
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def cmd_histogram(args):
|
|
165
|
+
"""Calculate and display/save histogram."""
|
|
166
|
+
img = ImageMatrix.from_file(args.image)
|
|
167
|
+
hist, bins = histogram(img, bins=args.bins)
|
|
168
|
+
|
|
169
|
+
if args.output:
|
|
170
|
+
with open(args.output, 'w') as f:
|
|
171
|
+
f.write("bin_start,bin_end,count\n")
|
|
172
|
+
for i in range(len(hist)):
|
|
173
|
+
f.write(f"{bins[i]:.2f},{bins[i+1]:.2f},{hist[i]}\n")
|
|
174
|
+
print(f"Histogram saved to: {args.output}")
|
|
175
|
+
else:
|
|
176
|
+
print(f"Histogram for: {args.image} ({args.bins} bins)")
|
|
177
|
+
print("-" * 40)
|
|
178
|
+
max_count = max(hist)
|
|
179
|
+
bar_width = 40
|
|
180
|
+
|
|
181
|
+
for i in range(0, len(hist), len(hist) // 16):
|
|
182
|
+
count = hist[i]
|
|
183
|
+
bar_len = int((count / max_count) * bar_width) if max_count > 0 else 0
|
|
184
|
+
bar = "█" * bar_len
|
|
185
|
+
print(f"{int(bins[i]):3d}: {bar} ({count})")
|
|
186
|
+
|
|
187
|
+
return 0
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def cmd_transform(args):
|
|
191
|
+
"""Apply transformations to an image."""
|
|
192
|
+
img = ImageMatrix.from_file(args.image)
|
|
193
|
+
|
|
194
|
+
if args.grayscale:
|
|
195
|
+
img = img.to_grayscale()
|
|
196
|
+
print("Applied: grayscale conversion")
|
|
197
|
+
|
|
198
|
+
if args.rotate:
|
|
199
|
+
img = rotate(img, args.rotate)
|
|
200
|
+
print(f"Applied: rotation by {args.rotate} degrees")
|
|
201
|
+
|
|
202
|
+
if args.flip_h:
|
|
203
|
+
img = flip_horizontal(img)
|
|
204
|
+
print("Applied: horizontal flip")
|
|
205
|
+
|
|
206
|
+
if args.flip_v:
|
|
207
|
+
img = flip_vertical(img)
|
|
208
|
+
print("Applied: vertical flip")
|
|
209
|
+
|
|
210
|
+
img.save(args.output)
|
|
211
|
+
print(f"Saved to: {args.output}")
|
|
212
|
+
|
|
213
|
+
return 0
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def cmd_resize(args):
|
|
217
|
+
"""Resize an image."""
|
|
218
|
+
img = ImageMatrix.from_file(args.image)
|
|
219
|
+
|
|
220
|
+
resized = resize(img, (args.width, args.height), resample=args.method)
|
|
221
|
+
resized.save(args.output)
|
|
222
|
+
|
|
223
|
+
print(f"Resized {args.image}: {img.width}x{img.height} -> {args.width}x{args.height}")
|
|
224
|
+
print(f"Method: {args.method}")
|
|
225
|
+
print(f"Saved to: {args.output}")
|
|
226
|
+
|
|
227
|
+
return 0
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def cmd_crop(args):
|
|
231
|
+
"""Crop an image."""
|
|
232
|
+
img = ImageMatrix.from_file(args.image)
|
|
233
|
+
|
|
234
|
+
box = (args.left, args.top, args.right, args.bottom)
|
|
235
|
+
cropped = crop(img, box)
|
|
236
|
+
cropped.save(args.output)
|
|
237
|
+
|
|
238
|
+
print(f"Cropped {args.image}: {img.width}x{img.height} -> {cropped.width}x{cropped.height}")
|
|
239
|
+
print(f"Region: ({args.left}, {args.top}) to ({args.right}, {args.bottom})")
|
|
240
|
+
print(f"Saved to: {args.output}")
|
|
241
|
+
|
|
242
|
+
return 0
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def cmd_compare(args):
|
|
246
|
+
"""Compare two images."""
|
|
247
|
+
img1 = ImageMatrix.from_file(args.image1)
|
|
248
|
+
img2 = ImageMatrix.from_file(args.image2)
|
|
249
|
+
|
|
250
|
+
print(f"Comparing images:")
|
|
251
|
+
print(f" Image 1: {args.image1}")
|
|
252
|
+
print(f" Image 2: {args.image2}")
|
|
253
|
+
print("-" * 40)
|
|
254
|
+
|
|
255
|
+
print(f"Dimensions:")
|
|
256
|
+
print(f" Image 1: {img1.width} x {img1.height}")
|
|
257
|
+
print(f" Image 2: {img2.width} x {img2.height}")
|
|
258
|
+
same_size = (img1.width == img2.width) and (img1.height == img2.height)
|
|
259
|
+
print(f" Same size: {'Yes' if same_size else 'No'}")
|
|
260
|
+
|
|
261
|
+
print(f"\nChannels:")
|
|
262
|
+
print(f" Image 1: {img1.channels}")
|
|
263
|
+
print(f" Image 2: {img2.channels}")
|
|
264
|
+
|
|
265
|
+
stats1 = statistics(img1)
|
|
266
|
+
stats2 = statistics(img2)
|
|
267
|
+
|
|
268
|
+
print(f"\nStatistics comparison:")
|
|
269
|
+
print(f" {'Metric':<12} {'Image 1':>12} {'Image 2':>12} {'Diff':>12}")
|
|
270
|
+
print(f" {'-'*12} {'-'*12} {'-'*12} {'-'*12}")
|
|
271
|
+
|
|
272
|
+
for key in ['mean', 'std', 'entropy', 'contrast']:
|
|
273
|
+
v1 = stats1[key]
|
|
274
|
+
v2 = stats2[key]
|
|
275
|
+
diff = v2 - v1
|
|
276
|
+
print(f" {key:<12} {v1:>12.4f} {v2:>12.4f} {diff:>+12.4f}")
|
|
277
|
+
|
|
278
|
+
return 0
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
if __name__ == "__main__":
|
|
282
|
+
sys.exit(main())
|