viseda 1.0.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.
- viseda/__init__.py +20 -0
- viseda/cli.py +144 -0
- viseda/core/__init__.py +3 -0
- viseda/core/base.py +69 -0
- viseda/hyperspectral/__init__.py +1 -0
- viseda/hyperspectral/eda.py +1849 -0
- viseda/image/__init__.py +1 -0
- viseda/image/eda.py +1840 -0
- viseda/pointcloud/__init__.py +3 -0
- viseda/pointcloud/eda.py +1167 -0
- viseda/report/__init__.py +3 -0
- viseda/report/html_report.py +213 -0
- viseda/text/__init__.py +3 -0
- viseda/text/eda.py +1613 -0
- viseda/utils/__init__.py +27 -0
- viseda/utils/helpers.py +120 -0
- viseda/video/__init__.py +3 -0
- viseda/video/eda.py +587 -0
- viseda-1.0.0.dist-info/METADATA +266 -0
- viseda-1.0.0.dist-info/RECORD +24 -0
- viseda-1.0.0.dist-info/WHEEL +5 -0
- viseda-1.0.0.dist-info/entry_points.txt +2 -0
- viseda-1.0.0.dist-info/licenses/LICENSE +21 -0
- viseda-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
viseda.report.html_report
|
|
3
|
+
-------------------------
|
|
4
|
+
Generates a self-contained HTML EDA report from a summary dict.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_TEMPLATE = """<!DOCTYPE html>
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="UTF-8"/>
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
19
|
+
<title>{title}</title>
|
|
20
|
+
<style>
|
|
21
|
+
:root {{
|
|
22
|
+
--bg: #0d1117; --surface: #161b22; --border: #30363d;
|
|
23
|
+
--text: #e6edf3; --muted: #8b949e; --accent: #58a6ff;
|
|
24
|
+
--green: #3fb950; --red: #f78166; --yellow: #e3b341;
|
|
25
|
+
}}
|
|
26
|
+
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
|
27
|
+
body {{ background: var(--bg); color: var(--text);
|
|
28
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
29
|
+
padding: 2rem; }}
|
|
30
|
+
h1 {{ font-size: 1.8rem; margin-bottom: 0.25rem; }}
|
|
31
|
+
h2 {{ font-size: 1.1rem; color: var(--accent); margin: 1.5rem 0 0.5rem; }}
|
|
32
|
+
h3 {{ font-size: 0.9rem; color: var(--muted); margin: 1rem 0 0.3rem; }}
|
|
33
|
+
.subtitle {{ color: var(--muted); margin-bottom: 2rem; font-size: 0.9rem; }}
|
|
34
|
+
.grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
35
|
+
gap: 1rem; }}
|
|
36
|
+
.card {{ background: var(--surface); border: 1px solid var(--border);
|
|
37
|
+
border-radius: 8px; padding: 1rem; }}
|
|
38
|
+
.card h3 {{ font-size: 0.8rem; color: var(--muted); margin-bottom: 0.5rem;
|
|
39
|
+
text-transform: uppercase; letter-spacing: 0.05em; }}
|
|
40
|
+
.stat {{ display: flex; justify-content: space-between; font-size: 0.85rem;
|
|
41
|
+
padding: 0.2rem 0; border-bottom: 1px solid var(--border); }}
|
|
42
|
+
.stat:last-child {{ border-bottom: none; }}
|
|
43
|
+
.val {{ font-variant-numeric: tabular-nums; color: var(--accent); }}
|
|
44
|
+
.badge {{ display: inline-block; padding: 0.15rem 0.5rem; border-radius: 12px;
|
|
45
|
+
font-size: 0.75rem; font-weight: 600; margin: 0.15rem; }}
|
|
46
|
+
.badge-blue {{ background: rgba(88,166,255,0.15); color: var(--accent); }}
|
|
47
|
+
.badge-green {{ background: rgba(63,185,80,0.15); color: var(--green); }}
|
|
48
|
+
.badge-red {{ background: rgba(247,129,102,0.15); color: var(--red); }}
|
|
49
|
+
.badge-yellow {{ background: rgba(227,179,65,0.15); color: var(--yellow); }}
|
|
50
|
+
.bar-wrap {{ margin-top: 0.5rem; }}
|
|
51
|
+
.bar-row {{ display: flex; align-items: center; gap: 0.5rem; margin: 0.2rem 0; font-size: 0.78rem; }}
|
|
52
|
+
.bar-label {{ width: 110px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--muted); }}
|
|
53
|
+
.bar {{ flex: 1; background: var(--border); border-radius: 4px; height: 10px; }}
|
|
54
|
+
.bar-fill {{ height: 100%; border-radius: 4px; background: var(--accent); }}
|
|
55
|
+
.bar-count {{ width: 60px; text-align: right; color: var(--accent); }}
|
|
56
|
+
.corrupt {{ color: var(--red); font-size: 0.8rem; margin-top: 0.5rem;
|
|
57
|
+
max-height: 200px; overflow-y: auto; }}
|
|
58
|
+
footer {{ margin-top: 3rem; color: var(--muted); font-size: 0.75rem;
|
|
59
|
+
border-top: 1px solid var(--border); padding-top: 1rem; }}
|
|
60
|
+
</style>
|
|
61
|
+
</head>
|
|
62
|
+
<body>
|
|
63
|
+
<h1>π¬ {title}</h1>
|
|
64
|
+
<p class="subtitle">Generated by <strong>VisEDA</strong></p>
|
|
65
|
+
|
|
66
|
+
{body}
|
|
67
|
+
|
|
68
|
+
<footer>Generated by VisEDA — Visual Exploratory Data Analysis</footer>
|
|
69
|
+
</body>
|
|
70
|
+
</html>
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _stat_card(title: str, stats: Dict) -> str:
|
|
75
|
+
if not stats:
|
|
76
|
+
return ""
|
|
77
|
+
rows = "".join(
|
|
78
|
+
f'<div class="stat"><span>{k}</span>'
|
|
79
|
+
f'<span class="val">{_fmt(v)}</span></div>'
|
|
80
|
+
for k, v in stats.items()
|
|
81
|
+
)
|
|
82
|
+
return f'<div class="card"><h3>{title}</h3>{rows}</div>'
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _fmt(v) -> str:
|
|
86
|
+
if isinstance(v, float):
|
|
87
|
+
return f"{v:,.3f}"
|
|
88
|
+
if isinstance(v, int):
|
|
89
|
+
return f"{v:,}"
|
|
90
|
+
return str(v)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _bar_chart(title: str, dist: Dict, color_class: str = "badge-blue") -> str:
|
|
94
|
+
if not dist:
|
|
95
|
+
return ""
|
|
96
|
+
total = sum(dist.values()) or 1
|
|
97
|
+
max_val = max(dist.values()) or 1
|
|
98
|
+
bars = ""
|
|
99
|
+
for label, count in sorted(dist.items(), key=lambda x: -x[1])[:30]:
|
|
100
|
+
pct = count / max_val * 100
|
|
101
|
+
bars += (
|
|
102
|
+
f'<div class="bar-row">'
|
|
103
|
+
f'<span class="bar-label" title="{label}">{label}</span>'
|
|
104
|
+
f'<div class="bar"><div class="bar-fill" style="width:{pct:.1f}%"></div></div>'
|
|
105
|
+
f'<span class="bar-count">{count:,}</span>'
|
|
106
|
+
f"</div>"
|
|
107
|
+
)
|
|
108
|
+
return f'<div class="card" style="grid-column: span 2;"><h3>{title}</h3><div class="bar-wrap">{bars}</div></div>'
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def generate_html_report(
|
|
112
|
+
summary: Dict[str, Any],
|
|
113
|
+
output_path: str,
|
|
114
|
+
title: str = "VisEDA Report",
|
|
115
|
+
) -> None:
|
|
116
|
+
sections = []
|
|
117
|
+
|
|
118
|
+
# ββ Overview badges ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
119
|
+
badges = ""
|
|
120
|
+
if "total_images" in summary:
|
|
121
|
+
badges += f'<span class="badge badge-blue">{summary["total_images"]:,} images</span>'
|
|
122
|
+
if "valid_images" in summary:
|
|
123
|
+
badges += f'<span class="badge badge-green">{summary["valid_images"]:,} valid</span>'
|
|
124
|
+
if summary.get("corrupt_images"):
|
|
125
|
+
badges += f'<span class="badge badge-red">{summary["corrupt_images"]:,} corrupt</span>'
|
|
126
|
+
if "n_points" in summary:
|
|
127
|
+
badges += f'<span class="badge badge-blue">{summary["n_points"]:,} points</span>'
|
|
128
|
+
if summary.get("n_duplicate_groups"):
|
|
129
|
+
badges += (
|
|
130
|
+
f'<span class="badge badge-yellow">'
|
|
131
|
+
f'{summary["n_duplicate_groups"]:,} duplicate groups</span>'
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if badges:
|
|
135
|
+
sections.append(f"<h2>Overview</h2><p>{badges}</p>")
|
|
136
|
+
|
|
137
|
+
# ββ Grid of stat cards βββββββββββββββββββββββββββββββββββββββββββ
|
|
138
|
+
stat_keys = [
|
|
139
|
+
"height", "width", "aspect_ratio", "file_size_kb",
|
|
140
|
+
"brightness", "contrast", "sharpness",
|
|
141
|
+
"x", "y", "z",
|
|
142
|
+
"intensity",
|
|
143
|
+
]
|
|
144
|
+
cards = ""
|
|
145
|
+
for key in stat_keys:
|
|
146
|
+
if key in summary and summary[key]:
|
|
147
|
+
cards += _stat_card(key.replace("_", " ").title(), summary[key])
|
|
148
|
+
|
|
149
|
+
if "bounding_box" in summary:
|
|
150
|
+
bb = summary["bounding_box"]
|
|
151
|
+
cards += _stat_card("Bounding Box", {
|
|
152
|
+
"X extent": bb["extent"][0],
|
|
153
|
+
"Y extent": bb["extent"][1],
|
|
154
|
+
"Z extent": bb["extent"][2],
|
|
155
|
+
"Volume": summary.get("volume_bounding_box", 0),
|
|
156
|
+
"Density (pts/unitΒ³)": summary.get("density_pts_per_unit3", 0),
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if cards:
|
|
160
|
+
sections.append(f'<h2>Statistics</h2><div class="grid">{cards}</div>')
|
|
161
|
+
|
|
162
|
+
# ββ Channel / RGB βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
163
|
+
if "mean_pixel_per_channel" in summary and summary["mean_pixel_per_channel"]:
|
|
164
|
+
ch_stats = {}
|
|
165
|
+
for i, (mn, st) in enumerate(zip(
|
|
166
|
+
summary["mean_pixel_per_channel"],
|
|
167
|
+
summary.get("std_pixel_per_channel", [0] * 3),
|
|
168
|
+
)):
|
|
169
|
+
ch_stats[f"Channel {i} mean"] = mn
|
|
170
|
+
ch_stats[f"Channel {i} std"] = st
|
|
171
|
+
cards2 = _stat_card("Channel Statistics", ch_stats)
|
|
172
|
+
sections.append(f'<h2>Pixel Statistics</h2><div class="grid">{cards2}</div>')
|
|
173
|
+
|
|
174
|
+
# ββ Label distribution ββββββββββββββββββββββββββββββββββββββββββββ
|
|
175
|
+
if summary.get("label_distribution"):
|
|
176
|
+
sections.append(
|
|
177
|
+
f'<h2>Label Distribution</h2>'
|
|
178
|
+
f'<div class="grid">'
|
|
179
|
+
f'{_bar_chart("Labels", summary["label_distribution"])}'
|
|
180
|
+
f'</div>'
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# ββ Channel distribution ββββββββββββββββββββββββββββββββββββββββββ
|
|
184
|
+
if summary.get("channel_distribution"):
|
|
185
|
+
sections.append(
|
|
186
|
+
f'<div class="grid">'
|
|
187
|
+
f'{_bar_chart("Channel Distribution", {str(k)+"-ch": v for k, v in summary["channel_distribution"].items()})}'
|
|
188
|
+
f'</div>'
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# ββ Corrupt files βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
192
|
+
if summary.get("corrupt_paths"):
|
|
193
|
+
items = "".join(f"<li>{p}</li>" for p in summary["corrupt_paths"][:50])
|
|
194
|
+
sections.append(
|
|
195
|
+
f'<h2>Corrupt Files ({len(summary["corrupt_paths"])})</h2>'
|
|
196
|
+
f'<div class="corrupt"><ul>{items}</ul></div>'
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# ββ Duplicate groups ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
200
|
+
if summary.get("duplicate_groups"):
|
|
201
|
+
groups_html = ""
|
|
202
|
+
for grp in summary["duplicate_groups"][:10]:
|
|
203
|
+
items = "".join(f"<li>{p}</li>" for p in grp)
|
|
204
|
+
groups_html += f"<ul>{items}</ul><hr/>"
|
|
205
|
+
sections.append(
|
|
206
|
+
f'<h2>Duplicate Groups (showing first 10)</h2>'
|
|
207
|
+
f'<div class="card">{groups_html}</div>'
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
body = "\n".join(sections)
|
|
211
|
+
html = _TEMPLATE.format(title=title, body=body)
|
|
212
|
+
|
|
213
|
+
Path(output_path).write_text(html, encoding="utf-8")
|
viseda/text/__init__.py
ADDED