393 lines
19 KiB
Python
393 lines
19 KiB
Python
import sqlite3
|
|
import pandas as pd
|
|
import openpyxl
|
|
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
|
from openpyxl.utils import get_column_letter
|
|
from openpyxl.chart import BarChart, Reference
|
|
|
|
def generate_excel_report(db_path, output_excel_path, target_date=None):
|
|
conn = sqlite3.connect(db_path)
|
|
if target_date:
|
|
df = pd.read_sql_query("SELECT * FROM batch_runs WHERE date = ? ORDER BY date ASC, camera_id ASC", conn, params=(target_date,))
|
|
else:
|
|
df = pd.read_sql_query("SELECT * FROM batch_runs ORDER BY date ASC, camera_id ASC", conn)
|
|
conn.close()
|
|
|
|
wb = openpyxl.Workbook()
|
|
# Remove default sheet
|
|
wb.remove(wb.active)
|
|
|
|
font_family = "Segoe UI"
|
|
if df.empty:
|
|
ws_empty = wb.create_sheet(title="Daily Summary")
|
|
ws_empty["A1"] = "Chicken Counter - Summary Report"
|
|
ws_empty["A1"].font = Font(name=font_family, size=16, bold=True, color="1F4E78")
|
|
target_info = f" for date {target_date}" if target_date else ""
|
|
ws_empty["A3"] = f"No batch runs found in database{target_info}."
|
|
ws_empty["A3"].font = Font(name=font_family, size=11, italic=True)
|
|
wb.save(output_excel_path)
|
|
print(f"No records found. Empty report saved to: {output_excel_path}")
|
|
return
|
|
|
|
# Styles
|
|
header_fill = PatternFill(start_color="1F4E78", end_color="1F4E78", fill_type="solid") # Dark Navy
|
|
header_font = Font(name=font_family, size=11, bold=True, color="FFFFFF")
|
|
|
|
accent_fill = PatternFill(start_color="D9E1F2", end_color="D9E1F2", fill_type="solid") # Light Soft Blue
|
|
total_fill = PatternFill(start_color="B4C6E7", end_color="B4C6E7", fill_type="solid")
|
|
total_font = Font(name=font_family, size=11, bold=True, color="000000")
|
|
|
|
kpi_title_font = Font(name=font_family, size=9, bold=False, color="595959")
|
|
kpi_value_font = Font(name=font_family, size=18, bold=True, color="1F4E78")
|
|
|
|
thin_border = Border(
|
|
left=Side(style='thin', color='D9D9D9'),
|
|
right=Side(style='thin', color='D9D9D9'),
|
|
top=Side(style='thin', color='D9D9D9'),
|
|
bottom=Side(style='thin', color='D9D9D9')
|
|
)
|
|
|
|
top_thick_bottom_double = Border(
|
|
top=Side(style='thin', color='000000'),
|
|
bottom=Side(style='double', color='000000')
|
|
)
|
|
|
|
# -------------------------------------------------------------
|
|
# SHEET 1: Daily Summary
|
|
# -------------------------------------------------------------
|
|
ws_summary = wb.create_sheet(title="Daily Summary")
|
|
ws_summary.views.sheetView[0].showGridLines = True
|
|
|
|
# Title Block
|
|
ws_summary["A1"] = "Chicken Counter - Cycle 7 Summary Report"
|
|
ws_summary["A1"].font = Font(name=font_family, size=16, bold=True, color="1F4E78")
|
|
ws_summary["A2"] = f"Location: {df['location'].iloc[0]} | Date Range: {df['date'].min()} to {df['date'].max()}"
|
|
ws_summary["A2"].font = Font(name=font_family, size=10, italic=True, color="595959")
|
|
|
|
# KPI Cards Block (Rows 4 to 6)
|
|
kpis = [
|
|
("TOTAL CHICKENS COUNTED", df['total_entered'].sum(), "#,##0"),
|
|
("TOTAL BATCH RUNS", len(df), "#,##0"),
|
|
("TOTAL FRAMES PROCESSED", df['frames_processed'].sum(), "#,##0"),
|
|
("TOTAL ELAPSED (MINUTES)", round(df['elapsed_seconds'].sum() / 60, 1), "#,##0.0")
|
|
]
|
|
|
|
col_starts = [1, 3, 5, 7]
|
|
for (title, val, num_fmt), col_idx in zip(kpis, col_starts):
|
|
c1 = ws_summary.cell(row=4, column=col_idx, value=title)
|
|
c1.font = kpi_title_font
|
|
c1.fill = accent_fill
|
|
ws_summary.merge_cells(start_row=4, start_column=col_idx, end_row=4, end_column=col_idx+1)
|
|
|
|
c2 = ws_summary.cell(row=5, column=col_idx, value=val)
|
|
c2.font = kpi_value_font
|
|
c2.number_format = num_fmt
|
|
c2.alignment = Alignment(horizontal='center', vertical='center')
|
|
ws_summary.merge_cells(start_row=5, start_column=col_idx, end_row=6, end_column=col_idx+1)
|
|
|
|
# Pivot Data: Date vs Camera
|
|
pivot = df.pivot_table(index='date', columns='camera_id', values='total_entered', aggfunc='sum', fill_value=0)
|
|
cameras = sorted(list(pivot.columns))
|
|
|
|
start_row = 9
|
|
ws_summary.cell(row=start_row, column=1, value="Date").font = header_font
|
|
ws_summary.cell(row=start_row, column=1).fill = header_fill
|
|
ws_summary.cell(row=start_row, column=1).alignment = Alignment(horizontal='center')
|
|
|
|
for idx, cam in enumerate(cameras):
|
|
cell = ws_summary.cell(row=start_row, column=idx+2, value=cam)
|
|
cell.font = header_font
|
|
cell.fill = header_fill
|
|
cell.alignment = Alignment(horizontal='center')
|
|
|
|
tot_header = ws_summary.cell(row=start_row, column=len(cameras)+2, value="Daily Total")
|
|
tot_header.font = header_font
|
|
tot_header.fill = header_fill
|
|
tot_header.alignment = Alignment(horizontal='center')
|
|
|
|
current_r = start_row + 1
|
|
for date_val, row_data in pivot.iterrows():
|
|
ws_summary.cell(row=current_r, column=1, value=str(date_val)).font = Font(name=font_family, size=11)
|
|
ws_summary.cell(row=current_r, column=1).alignment = Alignment(horizontal='center')
|
|
ws_summary.cell(row=current_r, column=1).border = thin_border
|
|
|
|
for idx, cam in enumerate(cameras):
|
|
c = ws_summary.cell(row=current_r, column=idx+2, value=int(row_data[cam]))
|
|
c.font = Font(name=font_family, size=11)
|
|
c.number_format = "#,##0"
|
|
c.alignment = Alignment(horizontal='right')
|
|
c.border = thin_border
|
|
|
|
# Excel SUM Formula for Daily Total
|
|
start_col_let = get_column_letter(2)
|
|
end_col_let = get_column_letter(len(cameras) + 1)
|
|
tot_c = ws_summary.cell(row=current_r, column=len(cameras)+2, value=f"=SUM({start_col_let}{current_r}:{end_col_let}{current_r})")
|
|
tot_c.font = Font(name=font_family, size=11, bold=True)
|
|
tot_c.number_format = "#,##0"
|
|
tot_c.alignment = Alignment(horizontal='right')
|
|
tot_c.border = thin_border
|
|
|
|
current_r += 1
|
|
|
|
# Total Row at Bottom
|
|
ws_summary.cell(row=current_r, column=1, value="Total").font = total_font
|
|
ws_summary.cell(row=current_r, column=1).fill = total_fill
|
|
ws_summary.cell(row=current_r, column=1).alignment = Alignment(horizontal='center')
|
|
ws_summary.cell(row=current_r, column=1).border = top_thick_bottom_double
|
|
|
|
for idx, cam in enumerate(cameras):
|
|
col_let = get_column_letter(idx + 2)
|
|
c = ws_summary.cell(row=current_r, column=idx+2, value=f"=SUM({col_let}{start_row+1}:{col_let}{current_r-1})")
|
|
c.font = total_font
|
|
c.fill = total_fill
|
|
c.number_format = "#,##0"
|
|
c.alignment = Alignment(horizontal='right')
|
|
c.border = top_thick_bottom_double
|
|
|
|
final_col_let = get_column_letter(len(cameras) + 2)
|
|
tot_final = ws_summary.cell(row=current_r, column=len(cameras)+2, value=f"=SUM({final_col_let}{start_row+1}:{final_col_let}{current_r-1})")
|
|
tot_final.font = total_font
|
|
tot_final.fill = total_fill
|
|
tot_final.number_format = "#,##0"
|
|
tot_final.alignment = Alignment(horizontal='right')
|
|
tot_final.border = top_thick_bottom_double
|
|
|
|
# Add Chart to Summary Sheet
|
|
chart = BarChart()
|
|
chart.type = "col"
|
|
chart.style = 10
|
|
chart.title = "Daily Chicken Counts by Camera"
|
|
chart.y_axis.title = "Chicken Count"
|
|
chart.x_axis.title = "Date"
|
|
chart.width = 16
|
|
chart.height = 10
|
|
|
|
data_ref = Reference(ws_summary, min_col=2, min_row=start_row, max_col=len(cameras)+1, max_row=current_r-1)
|
|
cats_ref = Reference(ws_summary, min_col=1, min_row=start_row+1, max_row=current_r-1)
|
|
chart.add_data(data_ref, titles_from_data=True)
|
|
chart.set_categories(cats_ref)
|
|
|
|
ws_summary.add_chart(chart, "I9")
|
|
|
|
# -------------------------------------------------------------
|
|
# SHEET 2: Camera Breakdown
|
|
# -------------------------------------------------------------
|
|
ws_cam = wb.create_sheet(title="Camera Summary")
|
|
ws_cam.views.sheetView[0].showGridLines = True
|
|
|
|
ws_cam["A1"] = "Camera Performance Summary"
|
|
ws_cam["A1"].font = Font(name=font_family, size=14, bold=True, color="1F4E78")
|
|
|
|
cam_pivot = df.groupby('camera_id').agg(
|
|
total_entered=('total_entered', 'sum'),
|
|
avg_entered=('total_entered', 'mean'),
|
|
total_frames=('frames_processed', 'sum'),
|
|
total_elapsed_sec=('elapsed_seconds', 'sum'),
|
|
total_runs=('id', 'count')
|
|
).reset_index()
|
|
|
|
cam_headers = ["Camera ID", "Total Chicken Count", "Average Count / Run", "Total Frames", "Total Time (Minutes)", "Total Runs"]
|
|
for c_idx, h_text in enumerate(cam_headers, 1):
|
|
cell = ws_cam.cell(row=3, column=c_idx, value=h_text)
|
|
cell.font = header_font
|
|
cell.fill = header_fill
|
|
cell.alignment = Alignment(horizontal='center')
|
|
|
|
for r_idx, row in cam_pivot.iterrows():
|
|
row_num = 4 + r_idx
|
|
ws_cam.cell(row=row_num, column=1, value=row['camera_id']).alignment = Alignment(horizontal='center')
|
|
|
|
ws_cam.cell(row=row_num, column=2, value=int(row['total_entered'])).number_format = "#,##0"
|
|
ws_cam.cell(row=row_num, column=3, value=round(row['avg_entered'], 1)).number_format = "#,##0.0"
|
|
ws_cam.cell(row=row_num, column=4, value=int(row['total_frames'])).number_format = "#,##0"
|
|
ws_cam.cell(row=row_num, column=5, value=round(row['total_elapsed_sec'] / 60, 2)).number_format = "#,##0.00"
|
|
ws_cam.cell(row=row_num, column=6, value=int(row['total_runs'])).number_format = "#,##0"
|
|
|
|
for c_idx in range(1, 7):
|
|
ws_cam.cell(row=row_num, column=c_idx).font = Font(name=font_family, size=11)
|
|
ws_cam.cell(row=row_num, column=c_idx).border = thin_border
|
|
|
|
# Total Row for Camera Summary
|
|
tot_row_cam = 4 + len(cam_pivot)
|
|
ws_cam.cell(row=tot_row_cam, column=1, value="Total").font = total_font
|
|
ws_cam.cell(row=tot_row_cam, column=1).fill = total_fill
|
|
ws_cam.cell(row=tot_row_cam, column=1).alignment = Alignment(horizontal='center')
|
|
ws_cam.cell(row=tot_row_cam, column=1).border = top_thick_bottom_double
|
|
|
|
for c_idx in [2, 4, 6]:
|
|
col_let = get_column_letter(c_idx)
|
|
c = ws_cam.cell(row=tot_row_cam, column=c_idx, value=f"=SUM({col_let}4:{col_let}{tot_row_cam-1})")
|
|
c.font = total_font
|
|
c.fill = total_fill
|
|
c.number_format = "#,##0"
|
|
c.border = top_thick_bottom_double
|
|
|
|
# Average for Avg column
|
|
c_avg = ws_cam.cell(row=tot_row_cam, column=3, value=f"=AVERAGE(C4:C{tot_row_cam-1})")
|
|
c_avg.font = total_font
|
|
c_avg.fill = total_fill
|
|
c_avg.number_format = "#,##0.0"
|
|
c_avg.border = top_thick_bottom_double
|
|
|
|
# Sum for elapsed
|
|
c_time = ws_cam.cell(row=tot_row_cam, column=5, value=f"=SUM(E4:E{tot_row_cam-1})")
|
|
c_time.font = total_font
|
|
c_time.fill = total_fill
|
|
c_time.number_format = "#,##0.00"
|
|
c_time.border = top_thick_bottom_double
|
|
|
|
# -------------------------------------------------------------
|
|
# SHEET 3: Raw Batch Runs
|
|
# -------------------------------------------------------------
|
|
ws_raw = wb.create_sheet(title="Raw Batch Runs")
|
|
ws_raw.views.sheetView[0].showGridLines = True
|
|
|
|
raw_headers = ["ID", "Date", "Location", "Camera ID", "Total Entered", "Frames Processed", "Elapsed (s)", "Stopped Reason", "Source Video", "Generated At"]
|
|
for c_idx, h_text in enumerate(raw_headers, 1):
|
|
cell = ws_raw.cell(row=1, column=c_idx, value=h_text)
|
|
cell.font = header_font
|
|
cell.fill = header_fill
|
|
cell.alignment = Alignment(horizontal='center')
|
|
|
|
for r_idx, row in df.iterrows():
|
|
row_num = 2 + r_idx
|
|
ws_raw.cell(row=row_num, column=1, value=int(row['id'])).alignment = Alignment(horizontal='center')
|
|
ws_raw.cell(row=row_num, column=2, value=str(row['date'])).alignment = Alignment(horizontal='center')
|
|
ws_raw.cell(row=row_num, column=3, value=str(row['location'])).alignment = Alignment(horizontal='center')
|
|
ws_raw.cell(row=row_num, column=4, value=str(row['camera_id'])).alignment = Alignment(horizontal='center')
|
|
|
|
ws_raw.cell(row=row_num, column=5, value=int(row['total_entered'])).number_format = "#,##0"
|
|
ws_raw.cell(row=row_num, column=6, value=int(row['frames_processed'])).number_format = "#,##0"
|
|
ws_raw.cell(row=row_num, column=7, value=float(row['elapsed_seconds'])).number_format = "#,##0.0"
|
|
ws_raw.cell(row=row_num, column=8, value=str(row['stopped_reason'])).alignment = Alignment(horizontal='center')
|
|
ws_raw.cell(row=row_num, column=9, value=str(row['source_video']))
|
|
ws_raw.cell(row=row_num, column=10, value=str(row['generated_at']))
|
|
|
|
for c_idx in range(1, 11):
|
|
ws_raw.cell(row=row_num, column=c_idx).font = Font(name=font_family, size=10)
|
|
ws_raw.cell(row=row_num, column=c_idx).border = thin_border
|
|
|
|
# -------------------------------------------------------------
|
|
# SHEET 4: Configurations
|
|
# -------------------------------------------------------------
|
|
ws_cfg = wb.create_sheet(title="Configurations")
|
|
ws_cfg.views.sheetView[0].showGridLines = True
|
|
|
|
ws_cfg["A1"] = "Pipeline & Model Configurations"
|
|
ws_cfg["A1"].font = Font(name=font_family, size=14, bold=True, color="1F4E78")
|
|
|
|
# Global Settings Table
|
|
ws_cfg["A3"] = "Global Pipeline & Detection Settings"
|
|
ws_cfg["A3"].font = Font(name=font_family, size=11, bold=True, color="1F4E78")
|
|
|
|
global_configs = [
|
|
("Hardware / Target Platform", "ASUS NUC (AMD Ryzen 9 9955HX + NVIDIA GeForce RTX 5070 8GB)"),
|
|
("Model Path (Engine)", "models/NUC5070_chicken-detection-model-v26n-300e-best-2026-05-02-NEW.engine"),
|
|
("Model Architecture", "YOLOv8/v26n TensorRT FP16 compiled engine"),
|
|
("Inference Image Size (imgsz)", "640 x 640"),
|
|
("IoU Threshold", "0.55"),
|
|
("Default Confidence Threshold (conf)", "0.40"),
|
|
("Target Classes", "[0] (Ignored: [1, 2])"),
|
|
("Default Min Box Area (px)", "3000"),
|
|
("Tracker Architecture", "BoT-SORT (persist=True, track_buffer=90)"),
|
|
("Gate Counting Mode", "two_line (lines_y: [420, 730], direction: bottom_to_up)"),
|
|
("Motion Flow Analysis", "vertical (forward_sign: 1.0, ema_alpha: 0.2, min_features: 40)"),
|
|
("Inference Stride", "2 frames (motion.stride_frames: 3)"),
|
|
("Execution Mode", "parallel_processes (MAX_JOBS=4)")
|
|
]
|
|
|
|
ws_cfg.cell(row=4, column=1, value="Configuration Parameter").font = header_font
|
|
ws_cfg.cell(row=4, column=1).fill = header_fill
|
|
ws_cfg.cell(row=4, column=2, value="Setting / Value").font = header_font
|
|
ws_cfg.cell(row=4, column=2).fill = header_fill
|
|
|
|
for idx, (param, val) in enumerate(global_configs, start=5):
|
|
c1 = ws_cfg.cell(row=idx, column=1, value=param)
|
|
c2 = ws_cfg.cell(row=idx, column=2, value=val)
|
|
c1.font = Font(name=font_family, size=10, bold=True)
|
|
c2.font = Font(name=font_family, size=10)
|
|
c1.border = thin_border
|
|
c2.border = thin_border
|
|
c1.fill = accent_fill
|
|
|
|
# Stages Table
|
|
stage_start_row = 5 + len(global_configs) + 2
|
|
ws_cfg.cell(row=stage_start_row-1, column=1, value="Cycle Stage Adaptation Rules").font = Font(name=font_family, size=11, bold=True, color="1F4E78")
|
|
|
|
stage_configs = [
|
|
("early_cycle (Day 0 - 15)", "conf: 0.10, min_box_area_px: 100, min_overlap_ratio: 0.10 (Chicks adaptation)"),
|
|
("mid_cycle (Day 16+)", "conf: 0.40, min_box_area_px: 3000, default filters (Grown chicken standard)")
|
|
]
|
|
|
|
ws_cfg.cell(row=stage_start_row, column=1, value="Cycle Stage").font = header_font
|
|
ws_cfg.cell(row=stage_start_row, column=1).fill = header_fill
|
|
ws_cfg.cell(row=stage_start_row, column=2, value="Applied Overrides").font = header_font
|
|
ws_cfg.cell(row=stage_start_row, column=2).fill = header_fill
|
|
|
|
for idx, (stg, desc) in enumerate(stage_configs, start=stage_start_row+1):
|
|
c1 = ws_cfg.cell(row=idx, column=1, value=stg)
|
|
c2 = ws_cfg.cell(row=idx, column=2, value=desc)
|
|
c1.font = Font(name=font_family, size=10, bold=True)
|
|
c2.font = Font(name=font_family, size=10)
|
|
c1.border = thin_border
|
|
c2.border = thin_border
|
|
c1.fill = accent_fill
|
|
|
|
# Per Camera Settings Table
|
|
cam_cfg_start = stage_start_row + len(stage_configs) + 3
|
|
ws_cfg.cell(row=cam_cfg_start-1, column=1, value="Per-Camera Specific Configurations").font = Font(name=font_family, size=11, bold=True, color="1F4E78")
|
|
|
|
cam_headers_cfg = ["Camera ID", "Count Anchor [X, Y]", "Confidence (conf)", "Min Box Area (px)", "Dedupe Settings", "ROI Polygon Points [X, Y]"]
|
|
for c_idx, h in enumerate(cam_headers_cfg, 1):
|
|
cell = ws_cfg.cell(row=cam_cfg_start, column=c_idx, value=h)
|
|
cell.font = header_font
|
|
cell.fill = header_fill
|
|
cell.alignment = Alignment(horizontal='center')
|
|
|
|
cam_details = [
|
|
("CC1", "[780, 120]", "0.35", "2500", "Disabled", "[(250, 330), (1650, 330), (1650, 720), (250, 720)]"),
|
|
("CC2", "[900, 120]", "0.50", "3000", "radius=32px, frames=24", "[(20, 380), (1880, 380), (1880, 720), (20, 720)]"),
|
|
("CC3", "[900, 120]", "0.40", "3000", "radius=24px, frames=16", "[(20, 330), (1880, 330), (1880, 720), (20, 720)]"),
|
|
("CC4", "[700, 120]", "0.40", "3000", "Disabled", "[(50, 330), (1450, 330), (1450, 720), (50, 720)]")
|
|
]
|
|
|
|
for r_offset, r_data in enumerate(cam_details, 1):
|
|
curr_row = cam_cfg_start + r_offset
|
|
for col_i, val in enumerate(r_data, 1):
|
|
cell = ws_cfg.cell(row=curr_row, column=col_i, value=val)
|
|
cell.font = Font(name=font_family, size=10)
|
|
cell.border = thin_border
|
|
if col_i in [1, 2, 3, 4]:
|
|
cell.alignment = Alignment(horizontal='center')
|
|
|
|
# Auto-adjust column widths across all sheets
|
|
for ws in wb.worksheets:
|
|
for col in ws.columns:
|
|
max_len = 0
|
|
col_letter = get_column_letter(col[0].column)
|
|
for cell in col:
|
|
# Avoid large width from title/merged cells
|
|
if cell.row < 3 and ws.title == "Daily Summary":
|
|
continue
|
|
val_str = str(cell.value or '')
|
|
if len(val_str) > max_len:
|
|
max_len = len(val_str)
|
|
ws.column_dimensions[col_letter].width = max(max_len + 4, 12)
|
|
|
|
ws_summary.column_dimensions['A'].width = 16
|
|
ws_summary.column_dimensions['B'].width = 14
|
|
ws_summary.column_dimensions['C'].width = 14
|
|
ws_summary.column_dimensions['D'].width = 14
|
|
ws_summary.column_dimensions['E'].width = 14
|
|
ws_summary.column_dimensions['F'].width = 16
|
|
|
|
wb.save(output_excel_path)
|
|
print(f"Excel report successfully generated: {output_excel_path}")
|
|
|
|
if __name__ == "__main__":
|
|
from pathlib import Path
|
|
BASE_DIR = Path(__file__).resolve().parent
|
|
db_file = str(BASE_DIR / "db" / "chicken_counts.db")
|
|
out_file = str(BASE_DIR / "db" / "chicken_counts_report.xlsx")
|
|
generate_excel_report(db_file, out_file)
|