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) # Styles font_family = "Segoe UI" 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)