ALYYAN commited on
Commit
eacd6a2
·
1 Parent(s): bdb70cc

Backend + Frontend done

Browse files
.streamlit/config.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [theme]
2
+ # Primary accent color (a professional blue, similar to your example)
3
+ primaryColor="#4A90E2"
4
+
5
+ # Main background color
6
+ backgroundColor="#F0F4F8"
7
+
8
+ # Sidebar and card background color
9
+ secondaryBackgroundColor="#FFFFFF"
10
+
11
+ # Default text color
12
+ textColor="#31333F"
13
+
14
+ # Font
15
+ font="sans serif"
.vscode/settings.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "git.ignoreLimitWarning": true
3
+ }
app.py CHANGED
@@ -2,195 +2,125 @@ import streamlit as st
2
  import cv2
3
  import numpy as np
4
  from PIL import Image
5
- from transformers import pipeline
6
- from mtcnn import MTCNN
7
- from collections import defaultdict
 
 
 
8
 
9
- st.set_page_config(layout="wide", page_title="Facial Age Detection")
 
10
 
11
- st.title("Facial Age Detection")
12
- st.write("Detect age groups from images, videos, or a live webcam feed.")
13
- st.write("This application uses an EfficientFormer-L1 model fine-tuned on the Facial Age dataset.")
14
-
15
- # --- Helper Functions and Classes ---
16
-
17
- @st.cache_resource
18
- def load_model():
19
- """Load the age detection model pipeline."""
20
- model_path = "artifacts/model_trainer/facial_age_detector_model"
21
- pipe = pipeline('image-classification', model=model_path, device=0) # Use 0 for GPU
22
- return pipe
23
-
24
- @st.cache_resource
25
- def load_face_detector():
26
- """Load the MTCNN face detector."""
27
- return MTCNN()
28
-
29
- def iou(boxA, boxB):
30
- """Calculate Intersection over Union."""
31
- xA = max(boxA[0], boxB[0])
32
- yA = max(boxA[1], boxB[1])
33
- xB = min(boxA[2], boxB[2])
34
- yB = min(boxA[3], boxB[3])
35
- interArea = max(0, xB - xA) * max(0, yB - yA)
36
- boxAArea = (boxA[2] - boxA[0]) * (boxA[3] - boxA[1])
37
- boxBArea = (boxB[2] - boxB[0]) * (boxB[3] - boxB[1])
38
- return interArea / float(boxAArea + boxBArea - interArea)
39
-
40
- class EMATracker:
41
- """Exponential Moving Average Tracker for smoothing predictions."""
42
- def __init__(self, alpha=0.3):
43
- self.alpha = alpha
44
- self.tracked_objects = {} # {track_id: {box: [], ema_preds: {}}}
45
-
46
- def update(self, detections, id_counter):
47
- # Detections are a list of face boxes
48
- # Simple tracking by IOU
49
-
50
- # Match detections to existing tracks
51
- matches = {} # {track_id: det_idx}
52
- used_det_indices = set()
53
-
54
- # This is a simple greedy matching. For more robust tracking, consider Hungarian algorithm.
55
- for track_id, data in self.tracked_objects.items():
56
- best_iou = 0
57
- best_det_idx = -1
58
- for i, det_box in enumerate(detections):
59
- if i in used_det_indices: continue
60
- current_iou = iou(data['box'], det_box)
61
- if current_iou > best_iou and current_iou > 0.3: # IOU threshold
62
- best_iou = current_iou
63
- best_det_idx = i
64
- if best_det_idx != -1:
65
- matches[track_id] = best_det_idx
66
- used_det_indices.add(best_det_idx)
67
-
68
- # Update matched tracks
69
- for track_id, det_idx in matches.items():
70
- self.tracked_objects[track_id]['box'] = detections[det_idx]
71
-
72
- # Add new tracks
73
- for i, det_box in enumerate(detections):
74
- if i not in used_det_indices:
75
- self.tracked_objects[id_counter] = {'box': det_box, 'ema_preds': defaultdict(float)}
76
- id_counter += 1
77
-
78
- # Remove old tracks (optional, for long videos)
79
-
80
- return id_counter
81
-
82
- def apply_ema(self, track_id, new_preds):
83
- """Applies EMA to the predictions for a given track."""
84
- if track_id not in self.tracked_objects:
85
- return {}
86
-
87
- current_ema = self.tracked_objects[track_id]['ema_preds']
88
-
89
- # Initialize if new
90
- if not current_ema:
91
- for pred in new_preds:
92
- current_ema[pred['label']] = pred['score']
93
- else:
94
- # Update existing values
95
- for pred in new_preds:
96
- label = pred['label']
97
- current_ema[label] = (self.alpha * pred['score']) + ((1 - self.alpha) * current_ema[label])
98
-
99
- self.tracked_objects[track_id]['ema_preds'] = current_ema
100
-
101
- # Return the top prediction from EMA
102
- if not current_ema: return None
103
- top_label = max(current_ema, key=current_ema.get)
104
- return f"{top_label} ({current_ema[top_label]:.2f})"
105
-
106
-
107
- # --- Load Models ---
108
  try:
109
- age_pipe = load_model()
110
- face_detector = load_face_detector()
111
- except Exception as e:
112
- st.error(f"Error loading models: {e}. Please ensure the model is trained and located at 'artifacts/model_trainer/facial_age_detector_model'.")
 
113
  st.stop()
114
-
115
-
116
- # --- UI Sidebar ---
117
- st.sidebar.header("Input Options")
118
- app_mode = st.sidebar.selectbox("Choose the app mode", ["Image", "Video", "Live Webcam"])
119
-
120
- # --- Main App Logic ---
121
-
122
- if app_mode == "Image":
123
- uploaded_file = st.sidebar.file_uploader("Upload an image...", type=["jpg", "jpeg", "png"])
124
- if uploaded_file is not None:
125
- image = Image.open(uploaded_file).convert("RGB")
126
- img_array = np.array(image)
127
-
128
- st.image(image, caption='Uploaded Image.', use_column_width=True)
129
- st.write("")
130
- st.write("Detecting faces and predicting age...")
131
-
132
- faces = face_detector.detect_faces(img_array)
133
-
134
- if not faces:
135
- st.warning("No faces detected in the image.")
136
- else:
137
- for face in faces:
138
- x, y, w, h = face['box']
139
- face_img = img_array[y:y+h, x:x+w]
140
- pil_face = Image.fromarray(face_img)
141
-
142
- # Predict age
143
- age_preds = age_pipe(pil_face)
144
- top_pred = age_preds[0]
145
-
146
- # Draw on image
147
- cv2.rectangle(img_array, (x, y), (x+w, y+h), (0, 255, 0), 2)
148
- label = f"Age: {top_pred['label']} ({top_pred['score']:.2f})"
149
- cv2.putText(img_array, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0,255,0), 2)
150
-
151
- st.image(img_array, caption='Processed Image.', use_column_width=True)
152
-
153
- elif app_mode == "Live Webcam":
154
- st.sidebar.info("Webcam feed will start automatically. Press 'Stop' to end.")
155
- run = st.sidebar.button('Start Webcam')
156
- stop = st.sidebar.button('Stop Webcam')
157
- FRAME_WINDOW = st.image([])
158
-
159
- cap = cv2.VideoCapture(0)
160
- tracker = EMATracker()
161
- track_id_counter = 0
162
-
163
- while run and not stop:
164
- ret, frame = cap.read()
165
- if not ret:
166
- st.error("Failed to capture image from webcam.")
167
- break
168
-
169
- frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
170
- faces = face_detector.detect_faces(frame_rgb)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
- detection_boxes = [f['box'] for f in faces]
173
- track_id_counter = tracker.update(detection_boxes, track_id_counter)
 
 
174
 
175
- for track_id, data in tracker.tracked_objects.items():
176
- x, y, w, h = data['box']
177
- if w > 20 and h > 20: # Filter small detections
178
- face_img = frame_rgb[y:y+h, x:x+w]
179
- pil_face = Image.fromarray(face_img)
180
-
181
- age_preds = age_pipe(pil_face)
182
- smoothed_label = tracker.apply_ema(track_id, age_preds)
183
-
184
- cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
185
- if smoothed_label:
186
- cv2.putText(frame, smoothed_label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,255,0), 2)
187
-
188
- FRAME_WINDOW.image(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
189
-
190
- cap.release()
191
- st.sidebar.info("Webcam stopped.")
192
-
193
- # Add a placeholder for Video processing, which would be similar to Webcam but with a file uploader.
194
- elif app_mode == "Video":
195
- st.sidebar.warning("Video processing is similar to the webcam feed but processes a file. This feature is not fully implemented in this demo but follows the same logic.")
196
- # You would use cv2.VideoCapture(video_path) and loop through frames.
 
2
  import cv2
3
  import numpy as np
4
  from PIL import Image
5
+ import tensorflow as tf
6
+ import sys
7
+ import os
8
+ import tempfile
9
+ import time
10
+ from streamlit_option_menu import option_menu
11
 
12
+ # --- Page Config (Set once at the top) ---
13
+ st.set_page_config(page_title="Facial Analysis", page_icon="👤", layout="wide", initial_sidebar_state="expanded")
14
 
15
+ # --- Backend Loading (Robust and Unchanged) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  try:
17
+ src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'src'))
18
+ if src_path not in sys.path: sys.path.append(src_path)
19
+ from cnnClassifier.pipeline.prediction import PredictionPipeline
20
+ except ImportError:
21
+ st.error("FATAL: Prediction pipeline not found. Check project structure.")
22
  st.stop()
23
+ try:
24
+ gpus = tf.config.list_physical_devices('GPU')
25
+ if gpus:
26
+ for gpu in gpus: tf.config.experimental.set_memory_growth(gpu, True)
27
+ except Exception: pass
28
+ @st.cache_resource
29
+ def load_pipeline():
30
+ return PredictionPipeline()
31
+ pipeline = load_pipeline()
32
+
33
+ # --- Session State for Webcam Control ---
34
+ if 'webcam_running' not in st.session_state: st.session_state.webcam_running = False
35
+ def start_webcam(): st.session_state.webcam_running = True
36
+ def stop_webcam(): st.session_state.webcam_running = False
37
+
38
+ # --- Sidebar UI (Clean and Themed) ---
39
+ with st.sidebar:
40
+ st.markdown("## ⚙️ Controls")
41
+ app_mode = option_menu(
42
+ menu_title=None,
43
+ options=["Image", "Video", "Live Feed"],
44
+ icons=["image", "film", "camera-video"],
45
+ menu_icon="cast",
46
+ default_index=0,
47
+ )
48
+ st.divider()
49
+ st.info("This app uses a multi-task EfficientNet model to predict age and gender.")
50
+
51
+ # --- Main Page Content ---
52
+ st.title(f"👤 Facial Demographics Analysis")
53
+ st.markdown(f"### Mode: {app_mode}")
54
+ st.divider()
55
+
56
+ if not pipeline:
57
+ st.error("AI Pipeline failed to load. Please check the terminal for errors.")
58
+ else:
59
+ if app_mode == "Image":
60
+ uploaded_file = st.file_uploader("Upload an image for analysis", type=["jpg", "jpeg", "png"])
61
+ if uploaded_file:
62
+ image = Image.open(uploaded_file).convert("RGB")
63
+ col1, col2 = st.columns(2)
64
+ with col1: st.image(image, caption='Original Image', use_column_width=True)
65
+ with col2:
66
+ with st.spinner('🔬 Analyzing...'):
67
+ annotated_image, predictions = pipeline.predict_image(np.array(image))
68
+ st.image(annotated_image, caption='Processed Image', use_column_width=True)
69
+ if predictions:
70
+ with st.expander("View Details", expanded=True):
71
+ for i, p in enumerate(predictions):
72
+ st.write(f"**Face {i+1}:** Gender: `{p['gender']}`, Age Group: `{p['age']}`")
73
+ else: st.warning("No faces detected.")
74
+
75
+ elif app_mode == "Video":
76
+ uploaded_file = st.file_uploader("Upload a video for analysis", type=["mp4", "mov", "avi"])
77
+ if uploaded_file:
78
+ tfile = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
79
+ tfile.write(uploaded_file.read())
80
+ cap = cv2.VideoCapture(tfile.name)
81
+ frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
82
+ st.info(f"Video has {frame_count} frames.")
83
+ if st.button("Start Video Processing", type="primary", use_container_width=True):
84
+ progress_bar = st.progress(0, text="Initializing...")
85
+ out_tfile = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
86
+ h, w = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)), int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
87
+ out = cv2.VideoWriter(out_tfile.name, cv2.VideoWriter_fourcc(*'mp4v'), cap.get(cv2.CAP_PROP_FPS), (w, h))
88
+ def frame_generator():
89
+ for _ in range(frame_count):
90
+ ret, frame = cap.read()
91
+ if not ret: break
92
+ yield cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
93
+ for i, annotated_frame_rgb in enumerate(pipeline.process_video_stream(frame_generator())):
94
+ out.write(cv2.cvtColor(annotated_frame_rgb, cv2.COLOR_RGB2BGR))
95
+ progress_bar.progress((i + 1) / frame_count, text=f"Processing Frame {i+1}/{frame_count}")
96
+ cap.release(), out.release()
97
+ st.success("Video processing complete!")
98
+ st.video(out_tfile.name)
99
+ with open(out_tfile.name, "rb") as f:
100
+ st.download_button("Download Processed Video", f, "output.mp4", "video/mp4", use_container_width=True)
101
+
102
+ elif app_mode == "Live Feed":
103
+ col1, col2 = st.columns(2)
104
+ with col1: st.button("Start Feed", on_click=start_webcam, use_container_width=True, type="primary")
105
+ with col2: st.button("Stop Feed", on_click=stop_webcam, use_container_width=True)
106
 
107
+ _, center_col, _ = st.columns([1, 2, 1])
108
+ with center_col:
109
+ FRAME_WINDOW = st.image([])
110
+ fps_display = st.empty()
111
 
112
+ if st.session_state.webcam_running:
113
+ cap = cv2.VideoCapture(0)
114
+ while st.session_state.webcam_running:
115
+ start_time = time.time()
116
+ ret, frame = cap.read()
117
+ if not ret: break
118
+ frame = cv2.flip(frame, 1)
119
+ annotated_frame = pipeline.process_live_frame(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
120
+ FRAME_WINDOW.image(annotated_frame, channels="RGB")
121
+ fps = 1.0 / (time.time() - start_time) if (time.time() - start_time) > 0 else 0
122
+ fps_display.markdown(f"<p style='text-align: center;'><b>FPS: {fps:.2f}</b></p>", unsafe_allow_html=True)
123
+ cap.release()
124
+ cv2.destroyAllWindows()
125
+ st.session_state.webcam_running = False
126
+ st.rerun()
 
 
 
 
 
 
 
config/config.yaml CHANGED
@@ -2,17 +2,22 @@ artifacts_root: artifacts
2
 
3
  data_ingestion:
4
  root_dir: artifacts/data_ingestion
5
- dataset_name: frabbisw/facial-age
6
- local_data_file: artifacts/data_ingestion/data.zip
7
- unzip_dir: artifacts/data_ingestion
8
 
9
  data_preparation:
10
  root_dir: artifacts/data_preparation
11
- data_path: artifacts/data_ingestion/face_age
12
- dataset_name: facial_age_prepared_dataset
 
 
13
 
14
- model_trainer:
15
- root_dir: artifacts/model_trainer
16
- trained_model_path: artifacts/model_trainer/facial_age_detector_model
17
- # Using EfficientFormer-L1, a much lighter model than ViT
18
- model_name: "snap-research/efficientformer-l1-300"
 
 
 
 
2
 
3
  data_ingestion:
4
  root_dir: artifacts/data_ingestion
5
+ dataset_name: "HuggingFaceM4/FairFace"
6
+ dataset_config: "0.25"
7
+ local_data_dir: artifacts/data_ingestion/dataset
8
 
9
  data_preparation:
10
  root_dir: artifacts/data_preparation
11
+ # Path to the raw dataset downloaded by the previous stage
12
+ raw_data_path: artifacts/data_ingestion/dataset
13
+ # Path where we will save the cleaned dataframe
14
+ cleaned_data_path: artifacts/data_preparation/fairface_cleaned.csv
15
 
16
+ multi_task_model_trainer:
17
+ root_dir: artifacts/multi_task_model_trainer
18
+ # The path to our cleaned CSV file from the previous stage
19
+ data_path: artifacts/data_preparation/fairface_cleaned.csv
20
+ # Where to save the final multi-task model
21
+ trained_model_path: artifacts/multi_task_model_trainer/facial_demographics_model
22
+ # The base model from Hugging Face
23
+ model_name: "google/efficientnet-b2"
dvc.lock CHANGED
@@ -3,24 +3,24 @@ stages:
3
  data_ingestion:
4
  cmd: python src/cnnClassifier/pipeline/stage_01_data_ingestion.py
5
  deps:
6
- - path: config/config.yaml
7
- hash: md5
8
- md5: 3cea2dfb36f0a5e40dd599dad9458ca4
9
- size: 609
10
  - path: src/cnnClassifier/components/data_ingestion.py
11
  hash: md5
12
- md5: 80b591ef3eedaf256ef85f4d196a0d43
13
- size: 1591
 
 
 
 
14
  - path: src/cnnClassifier/pipeline/stage_01_data_ingestion.py
15
  hash: md5
16
- md5: 2e1c2ad52ddc9763ff2a241576a7477c
17
- size: 904
18
  outs:
19
  - path: artifacts/data_ingestion
20
  hash: md5
21
- md5: 35941f86a72fc72e64cb3195753ae21d.dir
22
- size: 1758455894
23
- nfiles: 19557
24
  model_training:
25
  cmd: python src/cnnClassifier/pipeline/stage_02_model_training.py
26
  deps:
@@ -51,3 +51,59 @@ stages:
51
  md5: 621f61ba7beea89c3bef7a921afdcc9d.dir
52
  size: 183039001
53
  nfiles: 12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  data_ingestion:
4
  cmd: python src/cnnClassifier/pipeline/stage_01_data_ingestion.py
5
  deps:
 
 
 
 
6
  - path: src/cnnClassifier/components/data_ingestion.py
7
  hash: md5
8
+ md5: 11a88b6a3a45651504f59cab654dd889
9
+ size: 1737
10
+ - path: src/cnnClassifier/config/configuration.py
11
+ hash: md5
12
+ md5: f2ee2d8b6bf946faa613a7d498bf789a
13
+ size: 2458
14
  - path: src/cnnClassifier/pipeline/stage_01_data_ingestion.py
15
  hash: md5
16
+ md5: ba77afc62b94ad61c5990601bc2a6f15
17
+ size: 945
18
  outs:
19
  - path: artifacts/data_ingestion
20
  hash: md5
21
+ md5: 010e49306f9a2f8bd5f6235a9bb4c40a.dir
22
+ size: 1158773512
23
+ nfiles: 12
24
  model_training:
25
  cmd: python src/cnnClassifier/pipeline/stage_02_model_training.py
26
  deps:
 
51
  md5: 621f61ba7beea89c3bef7a921afdcc9d.dir
52
  size: 183039001
53
  nfiles: 12
54
+ data_preparation:
55
+ cmd: python src/cnnClassifier/pipeline/stage_02_data_preparation.py
56
+ deps:
57
+ - path: artifacts/data_ingestion/dataset
58
+ hash: md5
59
+ md5: 86434e33cd2b0a60b09c0624d29f1fda.dir
60
+ size: 579423180
61
+ nfiles: 8
62
+ - path: config/config.yaml
63
+ hash: md5
64
+ md5: bc8e095ab04797e455847fc34f3db546
65
+ size: 908
66
+ - path: src/cnnClassifier/components/data_preparation.py
67
+ hash: md5
68
+ md5: 39d7a55e908ab9b099f55c21a3019014
69
+ size: 2175
70
+ - path: src/cnnClassifier/pipeline/stage_02_data_preparation.py
71
+ hash: md5
72
+ md5: d04a9dd31ed636a27a79235a0dff46a6
73
+ size: 953
74
+ outs:
75
+ - path: artifacts/data_preparation
76
+ hash: md5
77
+ md5: 412fbf5339e9b82998c7617d19fce476.dir
78
+ size: 580361278
79
+ nfiles: 97699
80
+ multi_task_model_training:
81
+ cmd: python src/cnnClassifier/pipeline/stage_03_multi_task_model_training.py
82
+ deps:
83
+ - path: artifacts/data_preparation
84
+ hash: md5
85
+ md5: 412fbf5339e9b82998c7617d19fce476.dir
86
+ size: 580361278
87
+ nfiles: 97699
88
+ - path: config/config.yaml
89
+ hash: md5
90
+ md5: bc8e095ab04797e455847fc34f3db546
91
+ size: 908
92
+ - path: params.yaml
93
+ hash: md5
94
+ md5: ce8c137aa11f22d0901fb41485e9bfde
95
+ size: 239
96
+ - path: src/cnnClassifier/components/multi_task_model_trainer.py
97
+ hash: md5
98
+ md5: 5429e22ede43731b1806a9218c41c6d7
99
+ size: 7510
100
+ - path: src/cnnClassifier/pipeline/stage_03_multi_task_model_training.py
101
+ hash: md5
102
+ md5: 317cd31673bc55d1d73dea54cb75a8e0
103
+ size: 974
104
+ outs:
105
+ - path: artifacts/multi_task_model_trainer
106
+ hash: md5
107
+ md5: b641e844039bc54d5c29145dfb0bab77.dir
108
+ size: 1898189766
109
+ nfiles: 123
dvc.yaml CHANGED
@@ -4,17 +4,28 @@ stages:
4
  deps:
5
  - src/cnnClassifier/pipeline/stage_01_data_ingestion.py
6
  - src/cnnClassifier/components/data_ingestion.py
 
 
 
 
 
 
 
 
 
7
  - config/config.yaml
 
8
  outs:
9
- - artifacts/data_ingestion
10
 
11
- model_training:
12
- cmd: python src/cnnClassifier/pipeline/stage_02_model_training.py
13
  deps:
14
- - src/cnnClassifier/pipeline/stage_02_model_training.py
15
- - src/cnnClassifier/components/model_trainer.py
16
  - config/config.yaml
17
  - params.yaml
18
- - artifacts/data_ingestion # Depends on the output of the previous stage
19
  outs:
20
- - artifacts/model_trainer
 
 
4
  deps:
5
  - src/cnnClassifier/pipeline/stage_01_data_ingestion.py
6
  - src/cnnClassifier/components/data_ingestion.py
7
+ - src/cnnClassifier/config/configuration.py
8
+ outs:
9
+ - artifacts/data_ingestion # This output now includes the saved dataset
10
+
11
+ data_preparation: # <<< NEW STAGE
12
+ cmd: python src/cnnClassifier/pipeline/stage_02_data_preparation.py
13
+ deps:
14
+ - src/cnnClassifier/pipeline/stage_02_data_preparation.py
15
+ - src/cnnClassifier/components/data_preparation.py
16
  - config/config.yaml
17
+ - artifacts/data_ingestion/dataset # Depends on the output of the first stage
18
  outs:
19
+ - artifacts/data_preparation
20
 
21
+ multi_task_model_training: # <<< NEW STAGE
22
+ cmd: python src/cnnClassifier/pipeline/stage_03_multi_task_model_training.py
23
  deps:
24
+ - src/cnnClassifier/pipeline/stage_03_multi_task_model_training.py
25
+ - src/cnnClassifier/components/multi_task_model_trainer.py
26
  - config/config.yaml
27
  - params.yaml
28
+ - artifacts/data_preparation
29
  outs:
30
+ - artifacts/multi_task_model_trainer
31
+
requirements.txt CHANGED
@@ -25,12 +25,12 @@ scikit-learn
25
  Pillow
26
  tqdm
27
  imblearn
28
-
29
  # Frontend and Real-time Processing
30
  streamlit
31
  opencv-python
32
  mtcnn
33
  tensorflow==2.15.0
34
-
35
  # AWS Deployment
36
  boto3
 
25
  Pillow
26
  tqdm
27
  imblearn
28
+ seaborn
29
  # Frontend and Real-time Processing
30
  streamlit
31
  opencv-python
32
  mtcnn
33
  tensorflow==2.15.0
34
+ streamlit-option-menu
35
  # AWS Deployment
36
  boto3
research/evaluation.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # research/evaluation.py
2
+
3
+ import torch
4
+ import pandas as pd
5
+ import json
6
+ from pathlib import Path
7
+ import sys
8
+ import os
9
+ from sklearn.model_selection import train_test_split
10
+ from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
11
+ import seaborn as sns
12
+ import matplotlib.pyplot as plt
13
+ from torch.utils.data import DataLoader
14
+ from tqdm import tqdm
15
+
16
+ # Add the project's src directory to the Python path
17
+ # This allows us to import our custom modules
18
+ src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src'))
19
+ sys.path.append(src_path)
20
+
21
+ # Now we can import our custom classes
22
+ from cnnClassifier.components.multi_task_model_trainer import MultiTaskEfficientNet, FairFaceDataset
23
+ from cnnClassifier.utils.common import read_yaml
24
+ from torchvision.transforms import Compose, Resize, ToTensor, Normalize
25
+ from transformers import AutoImageProcessor
26
+
27
+ # ==============================================================================
28
+ # CONFIGURATION
29
+ # ==============================================================================
30
+ # Define paths directly. We are not using the config manager.
31
+ MODEL_PATH = Path("artifacts/multi_task_model_trainer/facial_demographics_model")
32
+ DATA_PATH = Path("artifacts/data_preparation/fairface_cleaned.csv")
33
+ PARAMS_PATH = Path("params.yaml")
34
+ EVALUATION_OUTPUT_DIR = Path("artifacts/manual_evaluation")
35
+ EVALUATION_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
36
+
37
+ # Load parameters
38
+ params = read_yaml(PARAMS_PATH)
39
+ IMAGE_SIZE = params.IMAGE_SIZE
40
+ BATCH_SIZE = params.BATCH_SIZE
41
+ TEST_SPLIT_SIZE = params.TEST_SPLIT_SIZE
42
+ RANDOM_STATE = params.RANDOM_STATE
43
+
44
+ # ==============================================================================
45
+ # MAIN EVALUATION LOGIC
46
+ # ==============================================================================
47
+ def evaluate_model():
48
+ device = "cuda" if torch.cuda.is_available() else "cpu"
49
+ print(f"--- Running evaluation on device: {device} ---")
50
+
51
+ # 1. Load data and prepare the test split
52
+ print("Loading and preparing test data...")
53
+ df = pd.read_csv(DATA_PATH)
54
+
55
+ label_maps = {}
56
+ for task in ['age', 'gender', 'race']:
57
+ labels = sorted(df[task].unique())
58
+ label_maps[f'{task}_label2id'] = {label: i for i, label in enumerate(labels)}
59
+ label_maps[f'{task}_id2label'] = {i: label for i, label in enumerate(labels)}
60
+ df[f'{task}_id'] = df[task].map(label_maps[f'{task}_label2id'])
61
+
62
+ # Use the same random_state to ensure we get the identical test split as in training
63
+ _, test_df = train_test_split(
64
+ df,
65
+ test_size=TEST_SPLIT_SIZE,
66
+ random_state=RANDOM_STATE,
67
+ stratify=df['age']
68
+ )
69
+
70
+ # 2. Create the PyTorch DataLoader
71
+ model_config = read_yaml(Path("config/config.yaml"))
72
+ base_model_name = model_config.multi_task_model_trainer.model_name
73
+ processor = AutoImageProcessor.from_pretrained(base_model_name)
74
+ _transforms = Compose([
75
+ Resize((IMAGE_SIZE, IMAGE_SIZE)),
76
+ ToTensor(),
77
+ Normalize(mean=processor.image_mean, std=processor.image_std)
78
+ ])
79
+
80
+ test_dataset = FairFaceDataset(dataframe=test_df, processor=processor, transforms=_transforms)
81
+ test_dataloader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)
82
+
83
+ # 3. Load the trained model
84
+ print(f"Loading model from: {MODEL_PATH}")
85
+ model = MultiTaskEfficientNet(
86
+ model_name=str(MODEL_PATH), # Pass the path as the model name
87
+ num_labels_age=len(label_maps['age_id2label']),
88
+ num_labels_gender=len(label_maps['gender_id2label']),
89
+ num_labels_race=len(label_maps['race_id2label']),
90
+ ).to(device)
91
+
92
+ # Load the trained weights
93
+ model.load_state_dict(torch.load(MODEL_PATH / 'pytorch_model.bin', map_location=device))
94
+ model.eval()
95
+
96
+ # 4. Run predictions on the test set
97
+ print("Running predictions on the test set...")
98
+ all_preds = {'age': [], 'gender': [], 'race': []}
99
+ all_labels = {'age': [], 'gender': [], 'race': []}
100
+
101
+ for batch in tqdm(test_dataloader, desc="Evaluating"):
102
+ pixel_values = batch['pixel_values'].to(device)
103
+ labels = batch['labels']
104
+
105
+ with torch.no_grad():
106
+ outputs = model(pixel_values=pixel_values)
107
+
108
+ all_preds['age'].extend(outputs['age_logits'].argmax(1).cpu().numpy())
109
+ all_preds['gender'].extend(outputs['gender_logits'].argmax(1).cpu().numpy())
110
+ all_preds['race'].extend(outputs['race_logits'].argmax(1).cpu().numpy())
111
+
112
+ all_labels['age'].extend(labels['age'].cpu().numpy())
113
+ all_labels['gender'].extend(labels['gender'].cpu().numpy())
114
+ all_labels['race'].extend(labels['race'].cpu().numpy())
115
+
116
+ # 5. Calculate metrics, generate reports, and save artifacts
117
+ print("--- Evaluation Results ---")
118
+ metrics = {}
119
+ for task in ['age', 'gender', 'race']:
120
+ accuracy = accuracy_score(all_labels[task], all_preds[task])
121
+ print(f"\n--- {task.capitalize()} ---")
122
+ print(f"Accuracy: {accuracy:.4f}")
123
+
124
+ report_str = classification_report(
125
+ all_labels[task],
126
+ all_preds[task],
127
+ target_names=list(label_maps[f'{task}_id2label'].values())
128
+ )
129
+ print("Classification Report:")
130
+ print(report_str)
131
+
132
+ metrics[f'{task}_accuracy'] = accuracy
133
+
134
+ # Confusion Matrix
135
+ cm = confusion_matrix(all_labels[task], all_preds[task])
136
+ plt.figure(figsize=(12, 10))
137
+ sns.heatmap(cm, annot=True, fmt='d', xticklabels=list(label_maps[f'{task}_id2label'].values()), yticklabels=list(label_maps[f'{task}_id2label'].values()), cmap='Blues')
138
+ plt.title(f'Confusion Matrix for {task.capitalize()}', fontsize=16)
139
+ plt.ylabel('Actual', fontsize=12)
140
+ plt.xlabel('Predicted', fontsize=12)
141
+ plt.xticks(rotation=45)
142
+ plt.yticks(rotation=0)
143
+
144
+ cm_path = EVALUATION_OUTPUT_DIR / f'{task}_confusion_matrix.png'
145
+ plt.savefig(cm_path, bbox_inches='tight')
146
+ plt.close() # Close the plot to avoid displaying it in the console
147
+ print(f"Saved {task} confusion matrix to {cm_path}")
148
+
149
+ # Save metrics to a JSON file
150
+ metrics_path = EVALUATION_OUTPUT_DIR / "metrics.json"
151
+ with open(metrics_path, 'w') as f:
152
+ json.dump(metrics, f, indent=4)
153
+ print(f"\nSaved final metrics to {metrics_path}")
154
+
155
+ # Save the label maps used for this evaluation run
156
+ label_maps_path = EVALUATION_OUTPUT_DIR / "label_maps.json"
157
+ with open(label_maps_path, 'w') as f:
158
+ json.dump(label_maps, f, indent=4)
159
+ print(f"Saved label maps to {label_maps_path}")
160
+
161
+
162
+ if __name__ == '__main__':
163
+ evaluate_model()
src/cnnClassifier/components/data_ingestion.py CHANGED
@@ -1,34 +1,40 @@
1
- import os
2
- import zipfile
 
3
  from cnnClassifier import logger
4
  from cnnClassifier.entity.config_entity import DataIngestionConfig
 
5
 
6
  class DataIngestion:
7
  def __init__(self, config: DataIngestionConfig):
8
  self.config = config
9
 
10
- def download_file(self):
11
  """
12
- Downloads the dataset from Kaggle.
13
- Make sure to have your kaggle.json file in ~/.kaggle/ or set KAGGLE_USERNAME and KAGGLE_KEY env variables.
14
  """
15
  try:
16
- logger.info(f"Downloading dataset from kaggle: {self.config.dataset_name}")
17
- os.system(f"kaggle datasets download {self.config.dataset_name} -p {os.path.dirname(self.config.local_data_file)}")
18
- # The downloaded file will be named 'facial-age.zip'. We need to rename it to 'data.zip' as per our config.
19
- downloaded_zip_path = os.path.join(os.path.dirname(self.config.local_data_file), 'facial-age.zip')
20
- os.rename(downloaded_zip_path, self.config.local_data_file)
21
- logger.info(f"Dataset downloaded and saved at {self.config.local_data_file}")
22
- except Exception as e:
23
- logger.error(f"Failed to download dataset. Error: {e}")
24
- raise e
 
 
 
 
 
 
 
25
 
26
- def extract_zip_file(self):
27
- """
28
- Extracts the zip file into the data directory
29
- """
30
- unzip_path = self.config.unzip_dir
31
- os.makedirs(unzip_path, exist_ok=True)
32
- with zipfile.ZipFile(self.config.local_data_file, 'r') as zip_ref:
33
- zip_ref.extractall(unzip_path)
34
- logger.info(f"Dataset extracted to {unzip_path}")
 
1
+ # src/cnnClassifier/components/data_ingestion.py
2
+
3
+ from datasets import load_dataset
4
  from cnnClassifier import logger
5
  from cnnClassifier.entity.config_entity import DataIngestionConfig
6
+ from pathlib import Path
7
 
8
  class DataIngestion:
9
  def __init__(self, config: DataIngestionConfig):
10
  self.config = config
11
 
12
+ def download_dataset(self):
13
  """
14
+ Downloads and saves the FairFace dataset from the Hugging Face Hub.
 
15
  """
16
  try:
17
+ logger.info(f"Downloading dataset '{self.config.dataset_name}' from Hugging Face Hub...")
18
+
19
+ # load_dataset handles everything: download, verification, and caching
20
+ # It returns a DatasetDict, typically with 'train' and 'validation' splits
21
+ fairface_dataset = load_dataset(
22
+ self.config.dataset_name,
23
+ name=self.config.dataset_config,
24
+ cache_dir=self.config.root_dir # Use our root_dir for caching
25
+ )
26
+
27
+ # Save the downloaded dataset to our specified artifacts directory
28
+ # This makes it a persistent part of our DVC pipeline
29
+ save_path = Path(self.config.local_data_dir)
30
+ fairface_dataset.save_to_disk(save_path)
31
+
32
+ logger.info(f"Dataset successfully downloaded and saved to {save_path}")
33
 
34
+ # Optional: Log the structure of the downloaded dataset
35
+ logger.info(f"Dataset splits: {list(fairface_dataset.keys())}")
36
+ logger.info(f"Training set features: {fairface_dataset['train'].features}")
37
+
38
+ except Exception as e:
39
+ logger.error(f"Failed to download or save dataset. Error: {e}")
40
+ raise e
 
 
src/cnnClassifier/components/data_preparation.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datasets import load_from_disk
2
+ import pandas as pd
3
+ from cnnClassifier import logger
4
+ from cnnClassifier.entity.config_entity import DataPreparationConfig
5
+ from pathlib import Path
6
+ from PIL import Image # <<< ADD THIS IMPORT
7
+ import io # <<< ADD THIS IMPORT
8
+
9
+ class DataPreparation:
10
+ def __init__(self, config: DataPreparationConfig):
11
+ self.config = config
12
+
13
+ def create_cleaned_dataframe(self):
14
+ try:
15
+ logger.info("Loading raw dataset to create cleaned CSV...")
16
+ raw_dataset = load_from_disk(self.config.raw_data_path)
17
+
18
+ df_train = raw_dataset['train'].to_pandas()
19
+ df_val = raw_dataset['validation'].to_pandas()
20
+ combined_df = pd.concat([df_train, df_val], ignore_index=True)
21
+
22
+ image_dir = Path("artifacts/data_preparation/images")
23
+ image_dir.mkdir(parents=True, exist_ok=True)
24
+
25
+ combined_df['image_file_path'] = [
26
+ str(image_dir / f"{i}.jpg") for i in range(len(combined_df))
27
+ ]
28
+
29
+ # --- IMPORTANT ---
30
+ # We only need the file path for the CSV, so we drop the bulky 'image' column
31
+ final_df_for_csv = combined_df.drop(columns=['image'])
32
+
33
+ logger.info(f"Saving cleaned metadata to {self.config.cleaned_data_path}")
34
+ final_df_for_csv.to_csv(self.config.cleaned_data_path, index=False)
35
+
36
+ # --- CORRECTED IMAGE SAVING LOOP ---
37
+ logger.info(f"Deterministically saving images to {image_dir}...")
38
+ for i, row in combined_df.iterrows():
39
+ image_path = Path(row['image_file_path'])
40
+ image_dict = row['image']
41
+
42
+ # Recreate the PIL Image from the dictionary's bytes data
43
+ pil_image = Image.open(io.BytesIO(image_dict['bytes']))
44
+
45
+ # Now save the reconstructed PIL Image
46
+ pil_image.save(image_path)
47
+
48
+ except Exception as e:
49
+ logger.error(f"Failed during data preparation. Error: {e}")
50
+ raise e
src/cnnClassifier/components/multi_task_model_trainer.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import pandas as pd
4
+ import numpy as np
5
+ from torch.utils.data import Dataset
6
+ from transformers import (
7
+ AutoImageProcessor,
8
+ AutoModelForImageClassification,
9
+ TrainingArguments,
10
+ Trainer
11
+ )
12
+ from torchvision.transforms import (
13
+ Compose,
14
+ Normalize,
15
+ RandomRotation,
16
+ RandomHorizontalFlip,
17
+ Resize,
18
+ ToTensor
19
+ )
20
+ from cnnClassifier.entity.config_entity import MultiTaskModelTrainerConfig
21
+ from cnnClassifier import logger
22
+ from PIL import Image
23
+ from sklearn.model_selection import train_test_split
24
+ from sklearn.metrics import accuracy_score
25
+
26
+ class MultiTaskEfficientNet(nn.Module):
27
+ def __init__(self, model_name, num_labels_age, num_labels_gender, num_labels_race):
28
+ super().__init__()
29
+ self.efficientnet_base = AutoModelForImageClassification.from_pretrained(model_name, ignore_mismatched_sizes=True)
30
+ original_classifier = self.efficientnet_base.classifier
31
+ feature_dim = original_classifier.in_features
32
+ self.efficientnet_base.classifier = nn.Identity()
33
+
34
+ self.age_classifier = nn.Linear(feature_dim, num_labels_age)
35
+ self.gender_classifier = nn.Linear(feature_dim, num_labels_gender)
36
+ self.race_classifier = nn.Linear(feature_dim, num_labels_race)
37
+
38
+ def forward(self, pixel_values, labels=None):
39
+ features = self.efficientnet_base.efficientnet(pixel_values)
40
+ pooled_features = features.last_hidden_state.mean(dim=[2, 3])
41
+ age_logits = self.age_classifier(pooled_features)
42
+ gender_logits = self.gender_classifier(pooled_features)
43
+ race_logits = self.race_classifier(pooled_features)
44
+
45
+ loss = None
46
+ if labels is not None:
47
+ loss_fct = nn.CrossEntropyLoss()
48
+ age_loss = loss_fct(age_logits, labels[:, 0])
49
+ gender_loss = loss_fct(gender_logits, labels[:, 1])
50
+ race_loss = loss_fct(race_logits, labels[:, 2])
51
+ loss = (2.0 * age_loss) + gender_loss + race_loss
52
+
53
+ return {"loss": loss, "age_logits": age_logits, "gender_logits": gender_logits, "race_logits": race_logits}
54
+
55
+ class FairFaceDataset(Dataset):
56
+ def __init__(self, dataframe, processor, transforms):
57
+ self.dataframe = dataframe
58
+ self.processor = processor
59
+ self.transforms = transforms
60
+ self.normalize = Normalize(mean=processor.image_mean, std=processor.image_std)
61
+
62
+ def __len__(self):
63
+ return len(self.dataframe)
64
+
65
+ def __getitem__(self, idx):
66
+ row = self.dataframe.iloc[idx]
67
+ image_path = row['image_file_path']
68
+ image = Image.open(image_path).convert("RGB")
69
+ pixel_values = self.transforms(image)
70
+ pixel_values = self.normalize(pixel_values)
71
+
72
+ labels = torch.tensor([row['age_id'], row['gender_id'], row['race_id']], dtype=torch.long)
73
+ return {"pixel_values": pixel_values, "labels": labels}
74
+
75
+ def compute_multitask_metrics(eval_pred):
76
+ predictions, labels = eval_pred
77
+ age_logits, gender_logits, race_logits = predictions['age_logits'], predictions['gender_logits'], predictions['race_logits']
78
+ age_preds = np.argmax(age_logits, axis=1)
79
+ gender_preds = np.argmax(gender_logits, axis=1)
80
+ race_preds = np.argmax(race_logits, axis=1)
81
+ age_labels, gender_labels, race_labels = labels[:, 0], labels[:, 1], labels[:, 2]
82
+ age_acc = accuracy_score(age_labels, age_preds)
83
+ gender_acc = accuracy_score(gender_labels, gender_preds)
84
+ race_acc = accuracy_score(race_labels, race_preds)
85
+ overall_acc = (age_acc + gender_acc + race_acc) / 3.0
86
+ return {"age_accuracy": age_acc, "gender_accuracy": gender_acc, "race_accuracy": race_acc, "overall_accuracy": overall_acc}
87
+
88
+ class MultiTaskModelTrainer:
89
+ def __init__(self, config: MultiTaskModelTrainerConfig):
90
+ self.config = config
91
+ self.processor = AutoImageProcessor.from_pretrained(config.model_name)
92
+
93
+ def train(self):
94
+ device = "cuda" if torch.cuda.is_available() else "cpu"
95
+ logger.info(f"Using device: {device}")
96
+
97
+ logger.info("Loading and preparing dataset from cleaned CSV...")
98
+ df = pd.read_csv(self.config.data_path)
99
+
100
+ label_maps = {}
101
+ for task in ['age', 'gender', 'race']:
102
+ labels = sorted(df[task].unique())
103
+ label_maps[f'{task}_label2id'] = {label: i for i, label in enumerate(labels)}
104
+ df[f'{task}_id'] = df[task].map(label_maps[f'{task}_label2id'])
105
+
106
+ num_classes_age = len(label_maps['age_label2id'])
107
+ num_classes_gender = len(label_maps['gender_label2id'])
108
+ num_classes_race = len(label_maps['race_label2id'])
109
+ train_df, test_df = train_test_split(df, test_size=self.config.test_split_size, random_state=self.config.random_state, stratify=df['age'])
110
+
111
+ train_transforms = Compose([
112
+ Resize((self.config.image_size, self.config.image_size)),
113
+ RandomHorizontalFlip(),
114
+ RandomRotation(10),
115
+ ToTensor(), # Normalization is now in the Dataset
116
+ ])
117
+
118
+ val_transforms = Compose([
119
+ Resize((self.config.image_size, self.config.image_size)),
120
+ ToTensor(),
121
+ ])
122
+
123
+ train_dataset = FairFaceDataset(dataframe=train_df, processor=self.processor, transforms=train_transforms)
124
+ test_dataset = FairFaceDataset(dataframe=test_df, processor=self.processor, transforms=val_transforms)
125
+
126
+ model = MultiTaskEfficientNet(model_name=self.config.model_name, num_labels_age=num_classes_age, num_labels_gender=num_classes_gender, num_labels_race=num_classes_race).to(device)
127
+
128
+ args = TrainingArguments(
129
+ output_dir=self.config.root_dir,
130
+ logging_dir=f'{self.config.root_dir}/logs',
131
+ evaluation_strategy="epoch",
132
+ learning_rate=self.config.learning_rate,
133
+ per_device_train_batch_size=self.config.batch_size,
134
+ per_device_eval_batch_size=self.config.batch_size,
135
+ num_train_epochs=self.config.num_train_epochs,
136
+ weight_decay=self.config.weight_decay,
137
+ save_strategy='epoch',
138
+ load_best_model_at_end=True,
139
+ metric_for_best_model="eval_overall_accuracy",
140
+ dataloader_num_workers=4,
141
+ lr_scheduler_type='cosine',
142
+ report_to="none"
143
+ )
144
+
145
+ class EvalTrainer(Trainer):
146
+ def prediction_step(self, model, inputs, prediction_loss_only, ignore_keys=None):
147
+ has_labels = "labels" in inputs
148
+ inputs = self._prepare_inputs(inputs)
149
+ with torch.no_grad():
150
+ outputs = model(**inputs)
151
+ loss = outputs.get("loss")
152
+ predictions = {"age_logits": outputs["age_logits"], "gender_logits": outputs["gender_logits"], "race_logits": outputs["race_logits"]}
153
+ return (loss, predictions, inputs["labels"] if has_labels else None)
154
+
155
+ trainer = EvalTrainer(model=model, args=args, train_dataset=train_dataset, eval_dataset=test_dataset, compute_metrics=compute_multitask_metrics)
156
+
157
+ trainer.train()
158
+
159
+ logger.info(f"Saving final model and processor to {self.config.trained_model_path}")
160
+ trainer.save_model(self.config.trained_model_path)
161
+ self.processor.save_pretrained(self.config.trained_model_path)
src/cnnClassifier/config/configuration.py CHANGED
@@ -1,6 +1,10 @@
1
  from cnnClassifier.constants import *
2
  from cnnClassifier.utils.common import read_yaml, create_directories
3
- from cnnClassifier.entity.config_entity import DataIngestionConfig, DataPreparationConfig, ModelTrainerConfig
 
 
 
 
4
 
5
  class ConfigurationManager:
6
  def __init__(
@@ -15,46 +19,45 @@ class ConfigurationManager:
15
 
16
  def get_data_ingestion_config(self) -> DataIngestionConfig:
17
  config = self.config.data_ingestion
18
-
19
  create_directories([config.root_dir])
20
 
21
  data_ingestion_config = DataIngestionConfig(
22
  root_dir=config.root_dir,
23
  dataset_name=config.dataset_name,
24
- local_data_file=config.local_data_file,
25
- unzip_dir=config.unzip_dir
26
  )
27
  return data_ingestion_config
28
-
29
- def get_data_preparation_config(self) -> DataPreparationConfig:
30
  config = self.config.data_preparation
 
31
  create_directories([config.root_dir])
32
-
33
  data_preparation_config = DataPreparationConfig(
34
  root_dir=config.root_dir,
35
- data_path=config.data_path,
36
- dataset_name=config.dataset_name
37
  )
38
  return data_preparation_config
39
-
40
- def get_model_trainer_config(self) -> ModelTrainerConfig:
41
- config = self.config.model_trainer
42
- data_prep_config = self.config.data_preparation
43
  params = self.params
44
  create_directories([config.root_dir])
45
 
46
- model_trainer_config = ModelTrainerConfig(
47
- root_dir=Path(config.root_dir),
48
- data_path=Path(data_prep_config.data_path),
49
- trained_model_path=Path(config.trained_model_path),
50
- model_name=config.model_name,
51
- image_size=int(params.IMAGE_SIZE),
52
- learning_rate=float(params.LEARNING_RATE), # <<< CORRECTED
53
- batch_size=int(params.BATCH_SIZE),
54
- num_train_epochs=int(params.NUM_TRAIN_EPOCHS),
55
- weight_decay=float(params.WEIGHT_DECAY), # <<< CORRECTED
56
- warmup_steps=int(params.WARMUP_STEPS),
57
- test_split_size=float(params.TEST_SPLIT_SIZE), # <<< CORRECTED
58
- random_state=int(params.RANDOM_STATE)
59
  )
60
- return model_trainer_config
 
1
  from cnnClassifier.constants import *
2
  from cnnClassifier.utils.common import read_yaml, create_directories
3
+ from cnnClassifier.entity.config_entity import (
4
+ DataIngestionConfig,
5
+ DataPreparationConfig,
6
+ MultiTaskModelTrainerConfig # <-- Import the new one
7
+ )
8
 
9
  class ConfigurationManager:
10
  def __init__(
 
19
 
20
  def get_data_ingestion_config(self) -> DataIngestionConfig:
21
  config = self.config.data_ingestion
 
22
  create_directories([config.root_dir])
23
 
24
  data_ingestion_config = DataIngestionConfig(
25
  root_dir=config.root_dir,
26
  dataset_name=config.dataset_name,
27
+ dataset_config=config.dataset_config,
28
+ local_data_dir=config.local_data_dir
29
  )
30
  return data_ingestion_config
31
+
32
+ def get_data_preparation_config(self) -> DataPreparationConfig: # <<< NEW METHOD
33
  config = self.config.data_preparation
34
+
35
  create_directories([config.root_dir])
36
+
37
  data_preparation_config = DataPreparationConfig(
38
  root_dir=config.root_dir,
39
+ raw_data_path=config.raw_data_path,
40
+ cleaned_data_path=config.cleaned_data_path
41
  )
42
  return data_preparation_config
43
+
44
+ def get_multi_task_model_trainer_config(self) -> MultiTaskModelTrainerConfig:
45
+ config = self.config.multi_task_model_trainer
 
46
  params = self.params
47
  create_directories([config.root_dir])
48
 
49
+ multi_task_model_trainer_config = MultiTaskModelTrainerConfig(
50
+ root_dir=Path(config.root_dir),
51
+ data_path=config.data_path,
52
+ trained_model_path=Path(config.trained_model_path),
53
+ model_name=config.model_name,
54
+ image_size=int(params.IMAGE_SIZE),
55
+ learning_rate=float(params.LEARNING_RATE),
56
+ batch_size=int(params.BATCH_SIZE),
57
+ num_train_epochs=int(params.NUM_TRAIN_EPOCHS),
58
+ weight_decay=float(params.WEIGHT_DECAY),
59
+ warmup_steps=int(params.WARMUP_STEPS),
60
+ test_split_size=float(params.TEST_SPLIT_SIZE),
61
+ random_state=int(params.RANDOM_STATE)
62
  )
63
+ return multi_task_model_trainer_config
src/cnnClassifier/entity/config_entity.py CHANGED
@@ -5,19 +5,21 @@ from pathlib import Path
5
  class DataIngestionConfig:
6
  root_dir: Path
7
  dataset_name: str
8
- local_data_file: Path
9
- unzip_dir: Path
10
 
11
  @dataclass(frozen=True)
12
- class DataPreparationConfig:
13
  root_dir: Path
14
- data_path: Path
15
- dataset_name: str
 
 
16
 
17
  @dataclass(frozen=True)
18
- class ModelTrainerConfig:
19
  root_dir: Path
20
- data_path: Path
21
  trained_model_path: Path
22
  model_name: str
23
  image_size: int
 
5
  class DataIngestionConfig:
6
  root_dir: Path
7
  dataset_name: str
8
+ local_data_dir: Path
9
+ dataset_config: str
10
 
11
  @dataclass(frozen=True)
12
+ class DataPreparationConfig: # <<< NEW DATACLASS
13
  root_dir: Path
14
+ raw_data_path: Path
15
+ cleaned_data_path: Path
16
+
17
+ # ... (other configs are above)
18
 
19
  @dataclass(frozen=True)
20
+ class MultiTaskModelTrainerConfig:
21
  root_dir: Path
22
+ data_path: str
23
  trained_model_path: Path
24
  model_name: str
25
  image_size: int
src/cnnClassifier/pipeline/prediction.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import pandas as pd
3
+ import numpy as np
4
+ from PIL import Image
5
+ from transformers import AutoImageProcessor
6
+ import cv2
7
+ from mtcnn import MTCNN
8
+ from pathlib import Path
9
+ import sys
10
+ import os
11
+ from torchvision.transforms import Compose, Resize, ToTensor, Normalize
12
+ from safetensors.torch import load_file as load_safetensors
13
+ from collections import OrderedDict
14
+ from scipy.spatial import distance as dist
15
+
16
+ try:
17
+ src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
18
+ if src_path not in sys.path: sys.path.append(src_path)
19
+ from components.multi_task_model_trainer import MultiTaskEfficientNet
20
+ from utils.common import read_yaml
21
+ except ImportError as e:
22
+ print(f"Could not import custom modules: {e}.")
23
+ sys.exit(1)
24
+
25
+ class CentroidTracker:
26
+ def __init__(self, max_disappeared=20):
27
+ self.next_object_id = 0
28
+ self.objects = OrderedDict()
29
+ self.disappeared = OrderedDict()
30
+ self.max_disappeared = max_disappeared
31
+
32
+ def register(self, centroid, box):
33
+ self.objects[self.next_object_id] = {'centroid': centroid, 'box': box, 'labels': {}, 'ema_preds': {}}
34
+ self.disappeared[self.next_object_id] = 0
35
+ self.next_object_id += 1
36
+
37
+ def deregister(self, object_id):
38
+ del self.objects[object_id]
39
+ del self.disappeared[object_id]
40
+
41
+ def update(self, boxes):
42
+ if len(boxes) == 0:
43
+ for object_id in list(self.disappeared.keys()):
44
+ self.disappeared[object_id] += 1
45
+ if self.disappeared[object_id] > self.max_disappeared:
46
+ self.deregister(object_id)
47
+ return self.objects
48
+
49
+ input_centroids = np.array([(x + w // 2, y + h // 2) for (x, y, w, h) in boxes])
50
+
51
+ if len(self.objects) == 0:
52
+ for i in range(len(input_centroids)):
53
+ self.register(input_centroids[i], boxes[i])
54
+ else:
55
+ object_ids = list(self.objects.keys())
56
+ object_centroids = np.array([v['centroid'] for v in self.objects.values()])
57
+ D = dist.cdist(object_centroids, input_centroids)
58
+ rows = D.min(axis=1).argsort()
59
+ cols = D.argmin(axis=1)[rows]
60
+ used_rows, used_cols = set(), set()
61
+ for row, col in zip(rows, cols):
62
+ if row in used_rows or col in used_cols: continue
63
+ object_id = object_ids[row]
64
+ self.objects[object_id]['centroid'] = input_centroids[col]
65
+ self.objects[object_id]['box'] = boxes[col]
66
+ self.disappeared[object_id] = 0
67
+ used_rows.add(row)
68
+ used_cols.add(col)
69
+
70
+ unused_rows = set(range(D.shape[0])).difference(used_rows)
71
+ unused_cols = set(range(D.shape[1])).difference(used_cols)
72
+
73
+ if D.shape[0] >= D.shape[1]:
74
+ for row in unused_rows:
75
+ object_id = object_ids[row]
76
+ self.disappeared[object_id] += 1
77
+ if self.disappeared[object_id] > self.max_disappeared:
78
+ self.deregister(object_id)
79
+ else:
80
+ for col in unused_cols:
81
+ self.register(input_centroids[col], boxes[col])
82
+ return self.objects
83
+
84
+ class PredictionPipeline:
85
+ def __init__(self, model_path: str = "artifacts/multi_task_model_trainer/checkpoint-26873"):
86
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
87
+ self.model_path = Path(model_path)
88
+ self.base_model_name = "google/efficientnet-b2"
89
+ params = read_yaml(Path("params.yaml"))
90
+ self.processor = AutoImageProcessor.from_pretrained(self.base_model_name)
91
+ self.transforms = Compose([Resize((params.IMAGE_SIZE, params.IMAGE_SIZE)), ToTensor(), Normalize(mean=self.processor.image_mean, std=self.processor.image_std)])
92
+ self.label_maps = self._load_label_maps()
93
+ self.model = self._load_model()
94
+ self.face_detector = MTCNN()
95
+ self.tracker = CentroidTracker()
96
+ print(f"--- Pipeline Initialized on device: {self.device} ---")
97
+
98
+ def _load_label_maps(self):
99
+ maps = {'age_id2label': {'0': '0-2', '1': '3-9', '2': '10-19', '3': '20-29', '4': '30-39', '5': '40-49', '6': '50-59', '7': '60-69', '8': 'more than 70'},
100
+ 'gender_id2label': {'0': 'Male', '1': 'Female'}}
101
+ return maps
102
+
103
+ def _load_model(self):
104
+ num_age, num_gender, num_race = len(self.label_maps['age_id2label']), len(self.label_maps['gender_id2label']), 7
105
+ model = MultiTaskEfficientNet(self.base_model_name, num_age, num_gender, num_race)
106
+ weight_file = self.model_path / 'model.safetensors'
107
+ if not weight_file.exists(): weight_file = self.model_path / 'pytorch_model.bin'
108
+ state_dict = load_safetensors(weight_file, device="cpu") if weight_file.suffix == ".safetensors" else torch.load(weight_file, map_location="cpu")
109
+ model.load_state_dict(state_dict)
110
+ model.to(self.device)
111
+ model.eval()
112
+ return model
113
+
114
+ def _draw_predictions(self, image, box, labels):
115
+ x, y, w, h = [int(c) for c in box]
116
+ font, font_scale, font_thickness = cv2.FONT_HERSHEY_DUPLEX, 0.6, 1
117
+ text_color, bg_color = (255, 255, 255), (255, 75, 75)
118
+ text_lines = [f"Gender: {labels['gender']}", f"Age: {labels['age']}"]
119
+ max_width, line_height = 0, 25
120
+ for line in text_lines:
121
+ (w_text, _), _ = cv2.getTextSize(line, font, font_scale, font_thickness)
122
+ if w_text > max_width: max_width = w_text
123
+ total_height = len(text_lines) * line_height - 5
124
+ cv2.rectangle(image, (x, y), (x + w, y + h), bg_color, 2)
125
+ cv2.rectangle(image, (x-1, y - total_height), (x + max_width + 10, y), bg_color, -1)
126
+ for i, line in enumerate(text_lines):
127
+ y_text = y - total_height + (i * line_height) + 18
128
+ cv2.putText(image, line, (x + 5, y_text), font, font_scale, text_color, font_thickness, cv2.LINE_AA)
129
+
130
+ def _predict_for_box(self, frame, box):
131
+ x, y, w, h = [int(c) for c in box]
132
+ face_img = frame[max(0,y):min(frame.shape[0],y+h), max(0,x):min(frame.shape[1],x+w)]
133
+ if face_img.size == 0: return None
134
+ pixel_values = self.transforms(Image.fromarray(face_img)).unsqueeze(0).to(self.device)
135
+ with torch.no_grad(): outputs = self.model(pixel_values=pixel_values)
136
+ return outputs
137
+
138
+ def predict_image(self, image_array):
139
+ annotated_image, predictions = image_array.copy(), []
140
+ face_results = self.face_detector.detect_faces(image_array)
141
+ if not face_results: return annotated_image, predictions
142
+ for face in face_results:
143
+ if face['confidence'] < 0.9: continue
144
+ box = face['box']
145
+ outputs = self._predict_for_box(annotated_image, box)
146
+ if outputs:
147
+ age_label = self.label_maps['age_id2label'][str(outputs['age_logits'].argmax(1).item())]
148
+ gender_label = self.label_maps['gender_id2label'][str(outputs['gender_logits'].argmax(1).item())]
149
+ prediction_labels = {"age": age_label, "gender": gender_label}
150
+ predictions.append({**prediction_labels, 'box': box})
151
+ self._draw_predictions(annotated_image, box, prediction_labels)
152
+ return annotated_image, predictions
153
+
154
+ def process_video_stream(self, frame_generator):
155
+ self.tracker = CentroidTracker()
156
+ for frame in frame_generator:
157
+ face_results = self.face_detector.detect_faces(frame)
158
+ boxes = [tuple(face['box']) for face in face_results if face['confidence'] > 0.9]
159
+ tracked_objects = self.tracker.update(boxes)
160
+
161
+ for obj_id, data in tracked_objects.items():
162
+ # Predict only for new tracks or tracks that have just been re-found
163
+ if 'labels' not in data or self.tracker.disappeared[obj_id] == 0:
164
+ outputs = self._predict_for_box(frame, data['box'])
165
+ if outputs:
166
+ alpha = 0.3
167
+ current_probs = {
168
+ 'age': outputs['age_logits'].softmax(1).cpu().numpy()[0],
169
+ 'gender': outputs['gender_logits'].softmax(1).cpu().numpy()[0]
170
+ }
171
+ # Apply EMA smoothing
172
+ if not data.get('ema_preds'): data['ema_preds'] = current_probs
173
+ else:
174
+ for task in ['age', 'gender']:
175
+ data['ema_preds'][task] = alpha * current_probs[task] + (1 - alpha) * data['ema_preds'][task]
176
+
177
+ # Always update the label from the latest smoothed probabilities
178
+ if data.get('ema_preds'):
179
+ age_label = self.label_maps['age_id2label'][str(np.argmax(data['ema_preds']['age']))]
180
+ gender_label = self.label_maps['gender_id2label'][str(np.argmax(data['ema_preds']['gender']))]
181
+ data['labels'] = {"age": age_label, "gender": gender_label}
182
+
183
+ annotated_frame = frame.copy()
184
+ for obj_id, data in tracked_objects.items():
185
+ if 'labels' in data:
186
+ self._draw_predictions(annotated_frame, data['box'], data['labels'])
187
+ yield annotated_frame
188
+
189
+ def process_live_frame(self, frame):
190
+ annotated_frame, _ = self.predict_image(frame)
191
+ return annotated_frame
src/cnnClassifier/pipeline/stage_01_data_ingestion.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  from cnnClassifier.config.configuration import ConfigurationManager
2
  from cnnClassifier.components.data_ingestion import DataIngestion
3
  from cnnClassifier import logger
@@ -12,8 +14,7 @@ class DataIngestionTrainingPipeline:
12
  config = ConfigurationManager()
13
  data_ingestion_config = config.get_data_ingestion_config()
14
  data_ingestion = DataIngestion(config=data_ingestion_config)
15
- data_ingestion.download_file()
16
- data_ingestion.extract_zip_file()
17
 
18
 
19
  if __name__ == '__main__':
 
1
+ # src/cnnClassifier/pipeline/stage_01_data_ingestion.py
2
+
3
  from cnnClassifier.config.configuration import ConfigurationManager
4
  from cnnClassifier.components.data_ingestion import DataIngestion
5
  from cnnClassifier import logger
 
14
  config = ConfigurationManager()
15
  data_ingestion_config = config.get_data_ingestion_config()
16
  data_ingestion = DataIngestion(config=data_ingestion_config)
17
+ data_ingestion.download_dataset() # Call the new method
 
18
 
19
 
20
  if __name__ == '__main__':
src/cnnClassifier/pipeline/stage_02_data_preparation.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/cnnClassifier/pipeline/stage_02_data_preparation.py
2
+
3
+ from cnnClassifier.config.configuration import ConfigurationManager
4
+ from cnnClassifier.components.data_preparation import DataPreparation
5
+ from cnnClassifier import logger
6
+
7
+ STAGE_NAME = "Data Preparation stage"
8
+
9
+ class DataPreparationTrainingPipeline:
10
+ def __init__(self):
11
+ pass
12
+
13
+ def main(self):
14
+ config = ConfigurationManager()
15
+ data_preparation_config = config.get_data_preparation_config()
16
+ data_preparation = DataPreparation(config=data_preparation_config)
17
+ data_preparation.create_cleaned_dataframe()
18
+
19
+ if __name__ == '__main__':
20
+ try:
21
+ logger.info(f">>>>>> stage {STAGE_NAME} started <<<<<<")
22
+ obj = DataPreparationTrainingPipeline()
23
+ obj.main()
24
+ logger.info(f">>>>>> stage {STAGE_NAME} completed <<<<<<\n\nx==========x")
25
+ except Exception as e:
26
+ logger.exception(e)
27
+ raise e
src/cnnClassifier/pipeline/stage_03_multi_task_model_training.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/cnnClassifier/pipeline/stage_03_multi_task_model_training.py
2
+ from cnnClassifier.config.configuration import ConfigurationManager
3
+ from cnnClassifier.components.multi_task_model_trainer import MultiTaskModelTrainer
4
+ from cnnClassifier import logger
5
+
6
+ STAGE_NAME = "Multi-Task Model Training stage"
7
+
8
+ class MultiTaskModelTrainingPipeline:
9
+ def __init__(self):
10
+ pass
11
+
12
+ def main(self):
13
+ config = ConfigurationManager()
14
+ multi_task_model_trainer_config = config.get_multi_task_model_trainer_config()
15
+ trainer = MultiTaskModelTrainer(config=multi_task_model_trainer_config)
16
+ trainer.train()
17
+
18
+ if __name__ == '__main__':
19
+ try:
20
+ logger.info(f">>>>>> stage {STAGE_NAME} started <<<<<<")
21
+ obj = MultiTaskModelTrainingPipeline()
22
+ obj.main()
23
+ logger.info(f">>>>>> stage {STAGE_NAME} completed <<<<<<\n\nx==========x")
24
+ except Exception as e:
25
+ logger.exception(e)
26
+ raise e