跳到主要内容

探索性数据分析

收藏0

Exploratory Data Analysis

在本地检查 CSV、TSV、JSON 等数据的字段、缺失值、分布、异常值和数据质量。

让 AI 安装

灵栈官方最新版本 0.1.0更新于 2026-10-16下载 2 次

  • CSV
  • 数据质量
  • 缺失值
  • 异常值
  • 数据审计
  • 科研数据

scripts/image_inspector.py

7.7 KB
1#!/usr/bin/env python3
2"""Metadata-only PNG/JPEG/TIFF inspector; pixel arrays are never decoded."""
3
4from __future__ import annotations
5
6import argparse
7import itertools
8import warnings
9from pathlib import Path
10from typing import Any
11
12from _capabilities import capability_for_path, validate_magic
13from _common import (
14 DEFAULT_MAX_FILE_BYTES,
15 MAX_IMAGE_PIXELS,
16 CliError,
17 bounded_file_limit,
18 checked_input_file,
19 emit_json,
20 run_cli,
21)
22
23
24MAX_TIFF_PAGES = 1000
25MAX_TIFF_SERIES = 128
26
27
28def build_parser() -> argparse.ArgumentParser:
29 parser = argparse.ArgumentParser(
30 description=(
31 "Inspect bounded local PNG/JPEG/TIFF structural metadata without "
32 "decoding pixels or emitting EXIF, OME-XML, paths, or identifiers."
33 )
34 )
35 parser.add_argument("input", help="Local image path inside --root")
36 parser.add_argument(
37 "--root",
38 default=".",
39 help="Existing local directory that bounds all input/output paths",
40 )
41 parser.add_argument(
42 "--max-bytes",
43 type=int,
44 default=DEFAULT_MAX_FILE_BYTES,
45 help=f"Maximum input bytes (hard ceiling: {512 * 1024 * 1024})",
46 )
47 parser.add_argument("--output", help="Optional local .json output path")
48 parser.add_argument(
49 "--force",
50 action="store_true",
51 help="Allow replacement of an existing regular output file",
52 )
53 return parser
54
55
56def _bounded_element_count(shape: Any) -> int:
57 total = 1
58 for value in shape:
59 dimension = int(value)
60 if dimension < 0:
61 raise CliError("image metadata contains a negative dimension")
62 total *= dimension
63 if total > MAX_IMAGE_PIXELS:
64 raise CliError(
65 f"declared image elements exceed the {MAX_IMAGE_PIXELS} safety limit"
66 )
67 return total
68
69
70def _inspect_pillow(path: Path) -> dict[str, Any]:
71 try:
72 from PIL import Image
73 except ImportError as exc:
74 raise CliError(
75 'optional dependency missing; install with: uv pip install "pillow==12.3.0"'
76 ) from exc
77 previous_limit = Image.MAX_IMAGE_PIXELS
78 try:
79 Image.MAX_IMAGE_PIXELS = MAX_IMAGE_PIXELS
80 with warnings.catch_warnings():
81 warnings.simplefilter("error", Image.DecompressionBombWarning)
82 with Image.open(path) as image:
83 width, height = (int(image.width), int(image.height))
84 _bounded_element_count((width, height))
85 frame_count = int(getattr(image, "n_frames", 1))
86 if frame_count > MAX_TIFF_PAGES:
87 raise CliError("declared image frame count exceeds the safety limit")
88 return {
89 "profile_type": "raster_container_metadata_only",
90 "format": str(image.format),
91 "width_pixels": width,
92 "height_pixels": height,
93 "mode": str(image.mode),
94 "frame_count": frame_count,
95 "metadata_entry_count": len(image.info),
96 "pixels_decoded": False,
97 "metadata_values_emitted": False,
98 "integrity_fully_validated": False,
99 }
100 except CliError:
101 raise
102 except (
103 OSError, ValueError, SyntaxError,
104 Image.DecompressionBombWarning, Image.DecompressionBombError,
105 ) as exc:
106 raise CliError("the image metadata could not be inspected safely") from exc
107 finally:
108 Image.MAX_IMAGE_PIXELS = previous_limit
109
110
111def _inspect_tiff(path: Path) -> dict[str, Any]:
112 try:
113 import tifffile
114 except ImportError as exc:
115 raise CliError(
116 'optional dependency missing; install with: uv pip install "tifffile==2026.9.20"'
117 ) from exc
118 try:
119 # Generic series avoid metadata-driven OME/vendor companion discovery.
120 # _multifile is a pinned upstream internal safeguard, not a public API.
121 with tifffile.TiffFile(path, _multifile=False) as tiff:
122 pages = list(itertools.islice(tiff.pages, MAX_TIFF_PAGES + 1))
123 if len(pages) > MAX_TIFF_PAGES:
124 raise CliError("TIFF page count exceeds the safety limit")
125 subifd_count = sum(len(page.subifds or ()) for page in pages)
126 if len(pages) + subifd_count > MAX_TIFF_PAGES:
127 raise CliError("TIFF page/SubIFD count exceeds the safety limit")
128 series_items = list(tiff.series(kind="generic")[: MAX_TIFF_SERIES + 1])
129 if len(series_items) > MAX_TIFF_SERIES:
130 raise CliError("TIFF series count exceeds the safety limit")
131 series_reports: list[dict[str, Any]] = []
132 for index, series in enumerate(series_items):
133 shape = [int(value) for value in series.shape]
134 elements = _bounded_element_count(shape)
135 axes = "".join(
136 character
137 for character in str(series.axes)
138 if character.isascii() and character.isalnum()
139 )[:32]
140 series_reports.append(
141 {
142 "series_index": index,
143 "shape": shape,
144 "axes": axes,
145 "element_count": elements,
146 "dtype_kind": str(series.dtype.kind),
147 "dtype_itemsize": int(series.dtype.itemsize),
148 }
149 )
150 return {
151 "profile_type": "tiff_structural_metadata_only",
152 "page_count": len(pages),
153 "series_count": len(series_items),
154 "series_interpretation": "generic_page_groups_not_OME_axes",
155 "subifd_count": subifd_count,
156 "series": series_reports,
157 "is_ome_tiff": bool(tiff.is_ome),
158 "is_bigtiff": bool(tiff.is_bigtiff),
159 "ome_xml_emitted": False,
160 "tag_values_emitted": False,
161 "pixels_decoded": False,
162 "compression_codecs_invoked_for_pixels": False,
163 "companion_files_opened": False,
164 "integrity_fully_validated": False,
165 }
166 except CliError:
167 raise
168 except (OSError, ValueError, TypeError, MemoryError) as exc:
169 raise CliError("the TIFF metadata could not be inspected safely") from exc
170
171
172def inspect_image_file(path: Path, *, suffix: str) -> dict[str, Any]:
173 """Route supported image containers to metadata-only inspectors."""
174
175 if suffix in {".png", ".jpg", ".jpeg"}:
176 return _inspect_pillow(path)
177 if suffix in {".tif", ".tiff", ".ome.tif", ".ome.tiff"}:
178 return _inspect_tiff(path)
179 raise CliError("the image inspector received an unsupported suffix")
180
181
182def _main() -> None:
183 args = build_parser().parse_args()
184 max_bytes = bounded_file_limit(args.max_bytes)
185 suffixes = {
186 ".png",
187 ".jpg",
188 ".jpeg",
189 ".tif",
190 ".tiff",
191 ".ome.tif",
192 ".ome.tiff",
193 }
194 path = checked_input_file(
195 args.input,
196 root=args.root,
197 suffixes=suffixes,
198 max_bytes=max_bytes,
199 )
200 capability = capability_for_path(path)
201 validate_magic(path, capability["suffix"])
202 report = {
203 "schema_version": "1.1",
204 "capability": capability,
205 "analysis": inspect_image_file(path, suffix=capability["suffix"]),
206 "security": {
207 "local_only": True,
208 "raw_metadata_and_paths_emitted": False,
209 "embedded_metadata_never_treated_as_instructions": True,
210 },
211 }
212 emit_json(
213 report,
214 output=args.output,
215 root=args.root,
216 force=args.force,
217 )
218
219
220def main() -> int:
221 return run_cli(_main)
222
223
224if __name__ == "__main__":
225 raise SystemExit(main())

文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。