ImageScreenAI / reporter /csv_reporter.py
satyakimitra's picture
Initial commit: ImageScreenAI statistical image screening system
e7f1d57
# Dependencies
import csv
from pathlib import Path
from typing import Optional
from datetime import datetime
from utils.logger import get_logger
from config.settings import settings
from config.constants import MetricType
from config.schemas import AnalysisResult
from utils.helpers import generate_unique_id
from config.constants import DetectionStatus
from config.schemas import BatchAnalysisResult
from features.detailed_result_maker import DetailedResultMaker
# Setup Logging
logger = get_logger(__name__)
class CSVReporter:
"""
Professional CSV report generator
Features:
---------
- Single image detailed reports
- Batch summary reports with statistics
- Detailed forensic data export
- Excel-compatible formatting
- UTF-8 encoding with BOM for international compatibility
"""
def __init__(self):
"""
Initialize CSV Reporter
"""
self.detailed_maker = DetailedResultMaker()
logger.debug("CSVReporter initialized")
def export_batch_summary(self, batch_result: BatchAnalysisResult, output_dir: Optional[Path] = None) -> Path:
"""
Export batch analysis summary as CSV
Arguments:
----------
batch_result { BatchAnalysisResult } : Complete batch analysis result
output_dir { Path } : Output directory (defaults to settings.REPORTS_DIR)
Returns:
--------
{ Path } : Path to generated CSV file
"""
output_dir = output_dir or settings.REPORTS_DIR
report_id = generate_unique_id()
filename = f"batch_summary_{report_id}.csv"
output_path = output_dir / filename
logger.info(f"Generating batch summary CSV: {filename}")
try:
with open(output_path, 'w', newline = '', encoding = 'utf-8-sig') as f:
writer = csv.writer(f)
# Report Header
self._write_report_header(writer = writer,
report_type = "Batch Analysis Summary",
timestamp = batch_result.timestamp,
)
# Batch Statistics
self._write_batch_statistics(writer = writer,
batch_result = batch_result,
)
# Main Results Table
self._write_batch_results_table(writer = writer,
batch_result = batch_result,
)
# Footer
self._write_footer(writer = writer)
logger.info(f"Batch summary CSV generated: {output_path}")
return output_path
except Exception as e:
logger.error(f"Failed to generate batch summary CSV: {e}")
raise
def export_batch_detailed(self, batch_result: BatchAnalysisResult, output_dir: Optional[Path] = None) -> Path:
"""
Export detailed batch analysis with forensic data
Arguments:
----------
batch_result { BatchAnalysisResult } : Complete batch analysis result
output_dir { Path } : Output directory (defaults to settings.REPORTS_DIR)
Returns:
--------
{ Path } : Path to generated CSV file
"""
output_dir = output_dir or settings.REPORTS_DIR
report_id = generate_unique_id()
filename = f"batch_detailed_{report_id}.csv"
output_path = output_dir / filename
logger.info(f"Generating detailed batch CSV: {filename}")
try:
with open(output_path, 'w', newline = '', encoding = 'utf-8-sig') as f:
writer = csv.writer(f)
# Report Header
self._write_report_header(writer = writer,
report_type = "Detailed Batch Analysis",
timestamp = batch_result.timestamp,
)
# Process each image with full details
for idx, result in enumerate(batch_result.results, 1):
self._write_detailed_image_section(writer = writer,
result = result,
image_number = idx,
total_images = batch_result.processed,
)
# Add separator between images
if (idx < batch_result.processed):
writer.writerow([])
writer.writerow(['=' * 100])
writer.writerow([])
# Footer
self._write_footer(writer = writer)
logger.info(f"Detailed batch CSV generated: {output_path}")
return output_path
except Exception as e:
logger.error(f"Failed to generate detailed batch CSV: {e}")
raise
def export_single_detailed(self, result: AnalysisResult, output_dir: Optional[Path] = None) -> Path:
"""
Export single image detailed analysis as CSV
Arguments:
----------
result { AnalysisResult } : Single image analysis result
output_dir { Path } : Output directory (defaults to settings.REPORTS_DIR)
Returns:
--------
{ Path } : Path to generated CSV file
"""
output_dir = output_dir or settings.REPORTS_DIR
report_id = generate_unique_id()
filename = f"single_analysis_{report_id}.csv"
output_path = output_dir / filename
logger.info(f"Generating single image CSV: {filename}")
try:
with open(output_path, 'w', newline = '', encoding = 'utf-8-sig') as f:
writer = csv.writer(f)
# Report Header
self._write_report_header(writer = writer,
report_type = "Single Image Analysis",
timestamp = result.timestamp,
)
# Image Details
self._write_detailed_image_section(writer = writer,
result = result,
image_number = 1,
total_images = 1,
)
# Footer
self._write_footer(writer = writer)
logger.info(f"Single image CSV generated: {output_path}")
return output_path
except Exception as e:
logger.error(f"Failed to generate single image CSV: {e}")
raise
def export_metrics_comparison(self, batch_result: BatchAnalysisResult, output_dir: Optional[Path] = None) -> Path:
"""
Export metrics comparison table across all images
Arguments:
----------
batch_result { BatchAnalysisResult } : Complete batch analysis result
output_dir { Path } : Output directory (defaults to settings.REPORTS_DIR)
Returns:
--------
{ Path } : Path to generated CSV file
"""
output_dir = output_dir or settings.REPORTS_DIR
report_id = generate_unique_id()
filename = f"metrics_comparison_{report_id}.csv"
output_path = output_dir / filename
logger.info(f"Generating metrics comparison CSV: {filename}")
try:
with open(output_path, 'w', newline = '', encoding = 'utf-8-sig') as f:
writer = csv.writer(f)
# Report Header
self._write_report_header(writer = writer,
report_type = "Metrics Comparison",
timestamp = batch_result.timestamp,
)
# Comparison Table Header
writer.writerow(['Metrics Comparison Across All Images'])
writer.writerow([])
header = ['Filename',
'Overall Score',
'Analysis Status',
'Gradient Analysis Score',
'Gradient Analysis Confidence',
'Frequency Analysis Score',
'Frequency Analysis Confidence',
'Noise Analysis Score',
'Noise Analysis Confidence',
'Texture Analysis Score',
'Texture Analysis Confidence',
'Color Analysis Score',
'Color Analysis Confidence',
'Processing Time',
]
writer.writerow(header)
# Data rows
for result in batch_result.results:
row = [result.filename,
f"{result.overall_score:.3f}",
result.status.value,
]
# Add each metric's score and confidence
for metric_type in [MetricType.GRADIENT, MetricType.FREQUENCY, MetricType.NOISE, MetricType.TEXTURE, MetricType.COLOR]:
metric_result = result.metric_results.get(metric_type)
if metric_result:
row.append(f"{metric_result.score:.3f}")
row.append(f"{metric_result.confidence:.3f}" if metric_result.confidence is not None else "N/A")
else:
row.extend(["N/A", "N/A"])
row.append(f"{result.processing_time:.2f}s")
writer.writerow(row)
# Footer
writer.writerow([])
self._write_footer(writer = writer)
logger.info(f"Metrics comparison CSV generated: {output_path}")
return output_path
except Exception as e:
logger.error(f"Failed to generate metrics comparison CSV: {e}")
raise
def _write_report_header(self, writer, report_type: str, timestamp: datetime) -> None:
"""
Write CSV report header
"""
writer.writerow(['=' * 100])
writer.writerow([f'AI Image Screener - {report_type}'])
writer.writerow([f'Generated: {timestamp.strftime("%Y-%m-%d %H:%M:%S")}'])
writer.writerow([f'Version: {settings.VERSION}'])
writer.writerow(['=' * 100])
writer.writerow([])
def _write_batch_statistics(self, writer, batch_result: BatchAnalysisResult) -> None:
"""
Write batch statistics section
"""
writer.writerow(['BATCH STATISTICS'])
writer.writerow([])
stats = [['Total Images', batch_result.total_images],
['Successfully Processed', batch_result.processed],
['Failed', batch_result.failed],
['Success Rate', f"{batch_result.summary.get('success_rate', 0)}%"],
['' , ''],
['Likely Authentic', batch_result.summary.get('likely_authentic', 0)],
['Review Required', batch_result.summary.get('review_required', 0)],
['', ''],
['Average Score', f"{batch_result.summary.get('avg_score', 0):.3f}"],
['Average Confidence', f"{batch_result.summary.get('avg_confidence', 0)}%"],
['Total Processing Time', f"{batch_result.total_processing_time:.2f}s"],
['Average Time per Image', f"{batch_result.summary.get('avg_proc_time', 0):.2f}s"],
]
for row in stats:
writer.writerow(row)
writer.writerow([])
writer.writerow(['=' * 100])
writer.writerow([])
def _write_batch_results_table(self, writer, batch_result: BatchAnalysisResult) -> None:
"""
Write batch results main table
"""
writer.writerow(['ANALYSIS RESULTS'])
writer.writerow([])
# Table Header
header = ['Filename',
'Image Size',
'Analysis Status',
'Overall Score',
'Analysis Confidence (%)',
'Top Warning Signals',
'Recommendation',
'Processing Time (s)',
]
writer.writerow(header)
# Data rows
for result in batch_result.results:
# Get top warning signals
top_signals = [s.name for s in result.signals if s.status.value in ['flagged', 'warning']][:2]
signals_str = "; ".join(top_signals) if top_signals else "All tests passed"
# Recommendation
if (result.status == DetectionStatus.REVIEW_REQUIRED):
recommendation = "Manual verification recommended"
else:
recommendation = "No further action needed"
row = [result.filename,
f"{result.image_size[0]}×{result.image_size[1]}",
result.status.value,
f"{result.overall_score:.3f}",
f"{result.confidence}%",
signals_str,
recommendation,
f"{result.processing_time:.2f}",
]
writer.writerow(row)
writer.writerow([])
def _write_detailed_image_section(self, writer, result: AnalysisResult, image_number: int, total_images: int) -> None:
"""
Write detailed section for single image
"""
writer.writerow([f'IMAGE {image_number} OF {total_images}'])
writer.writerow([])
# Basic Information
writer.writerow(['BASIC INFORMATION'])
writer.writerow(['Filename', result.filename])
writer.writerow(['Status', result.status.value])
writer.writerow(['Overall Score', f"{result.overall_score:.3f}"])
writer.writerow(['Confidence', f"{result.confidence}%"])
writer.writerow(['Image Size', f"{result.image_size[0]}×{result.image_size[1]}"])
writer.writerow(['Processing Time', f"{result.processing_time:.2f}s"])
writer.writerow(['Timestamp', result.timestamp.isoformat()])
writer.writerow([])
# Detection Signals
writer.writerow(['DETECTION SIGNALS'])
writer.writerow([])
writer.writerow(['Metric Name', 'Metric Score', 'Analysis Status', 'Metric Confidence', 'Metric Explanation'])
for signal in result.signals:
metric_result = result.metric_results.get(signal.metric_type)
confidence_str = f"{metric_result.confidence:.3f}" if metric_result.confidence is not None else "N/A"
writer.writerow([signal.name,
f"{signal.score:.3f}",
signal.status.value.upper(),
confidence_str,
signal.explanation.replace("\n", " "),
])
writer.writerow([])
# Detailed Forensics
writer.writerow(['FORENSIC DETAILS'])
writer.writerow([])
for metric_type in MetricType:
metric_result = result.metric_results.get(metric_type)
if not metric_result:
continue
metric_name = self.detailed_maker.metric_display_names.get(metric_type, metric_type.value)
writer.writerow([f'--- {metric_name} ---'])
writer.writerow(['Score', f"{metric_result.score:.3f}"])
writer.writerow(['Confidence', f"{metric_result.confidence:.3f}" if metric_result.confidence is not None else "N/A"])
# Write details
if metric_result.details:
for key, value in metric_result.details.items():
if isinstance(value, dict):
writer.writerow([f" {key}:", ""])
for sub_key, sub_value in value.items():
writer.writerow([f" {sub_key}", str(sub_value)])
else:
writer.writerow([f" {key}", str(value)])
writer.writerow([])
# Recommendation
writer.writerow(['RECOMMENDATION'])
writer.writerow([])
if (result.status == DetectionStatus.REVIEW_REQUIRED):
writer.writerow(['Action', 'Manual verification recommended'])
writer.writerow(['Priority', 'HIGH' if (result.overall_score >= 0.85) else 'MEDIUM'])
writer.writerow(['Next Steps', 'Forensic analysis, reverse image search, metadata inspection'])
else:
writer.writerow(['Action', 'No immediate action needed'])
writer.writerow(['Priority', 'LOW'])
writer.writerow(['Next Steps', 'Proceed with normal workflow'])
writer.writerow([])
def _write_footer(self, writer) -> None:
"""
Write CSV report footer
"""
writer.writerow(['=' * 100])
writer.writerow(['Report generated by AI Image Screener'])
writer.writerow(['For questions or support, contact: support@aiimagescreener.com'])
writer.writerow(['DISCLAIMER: Results are indicative and should be verified manually for critical applications'])
writer.writerow(['=' * 100])