andevs commited on
Commit
c10db71
·
verified ·
1 Parent(s): 6257565

Create cosmetics_matching.py

Browse files
Files changed (1) hide show
  1. cosmetics_matching.py +274 -0
cosmetics_matching.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Depends
2
+ import numpy as np
3
+ import cv2
4
+ from sklearn.cluster import KMeans
5
+ import colorsys
6
+
7
+ class SkinToneAnalyzer:
8
+ def __init__(self):
9
+ self.skin_tone_ranges = {
10
+ 'fair': (0, 30),
11
+ 'light': (30, 45),
12
+ 'medium': (45, 60),
13
+ 'tan': (60, 75),
14
+ 'deep': (75, 90),
15
+ 'dark': (90, 100)
16
+ }
17
+
18
+ self.undertone_rules = {
19
+ 'warm': {'g/r_ratio': (0.4, 0.6), 'b/g_ratio': (0.5, 0.7)},
20
+ 'cool': {'g/r_ratio': (0.6, 0.8), 'b/g_ratio': (0.7, 0.9)},
21
+ 'neutral': {'g/r_ratio': (0.5, 0.7), 'b/g_ratio': (0.6, 0.8)},
22
+ 'olive': {'g/r_ratio': (0.5, 0.7), 'b/g_ratio': (0.4, 0.6)}
23
+ }
24
+
25
+ def analyze_skin_tone(self, image_bytes: bytes) -> dict:
26
+ """
27
+ Analyze skin tone from image
28
+ """
29
+ # Convert to OpenCV format
30
+ nparr = np.frombuffer(image_bytes, np.uint8)
31
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
32
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
33
+
34
+ # Detect skin regions using face detection
35
+ face_cascade = cv2.CascadeClassifier(
36
+ cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
37
+ )
38
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
39
+ faces = face_cascade.detectMultiScale(gray, 1.1, 4)
40
+
41
+ if len(faces) == 0:
42
+ return {"error": "No face detected"}
43
+
44
+ # Extract skin samples from cheeks
45
+ skin_samples = []
46
+ for (x, y, w, h) in faces:
47
+ # Left cheek area
48
+ left_cheek = img_rgb[y + h//3:y + h//2, x + w//8:x + w//4]
49
+ # Right cheek area
50
+ right_cheek = img_rgb[y + h//3:y + h//2, x + 3*w//4:x + 7*w//8]
51
+
52
+ skin_samples.extend(left_cheek.reshape(-1, 3))
53
+ skin_samples.extend(right_cheek.reshape(-1, 3))
54
+
55
+ # Cluster to find dominant skin tone
56
+ kmeans = KMeans(n_clusters=3, random_state=42)
57
+ kmeans.fit(skin_samples)
58
+
59
+ # Get the most representative cluster
60
+ dominant_color = kmeans.cluster_centers_[np.argmax(np.bincount(kmeans.labels_))]
61
+ r, g, b = dominant_color
62
+
63
+ # Calculate melanin level
64
+ melanin_level = self._calculate_melanin(r, g, b)
65
+
66
+ # Determine primary tone
67
+ primary_tone = self._get_primary_tone(melanin_level)
68
+
69
+ # Determine undertone
70
+ undertone = self._get_undertone(r, g, b)
71
+
72
+ # Calculate RGB values
73
+ rgb_values = {
74
+ 'r': float(r),
75
+ 'g': float(g),
76
+ 'b': float(b),
77
+ 'hex': f"{int(r):02x}{int(g):02x}{int(b):02x}"
78
+ }
79
+
80
+ return {
81
+ 'primary_tone': primary_tone,
82
+ 'undertone': undertone,
83
+ 'surface_tone': self._get_surface_tone(r, g, b),
84
+ 'melanin_level': float(melanin_level),
85
+ 'rgb_values': rgb_values,
86
+ 'foundation_match': self._get_foundation_match(primary_tone, undertone),
87
+ 'analyzed_at': datetime.utcnow().isoformat()
88
+ }
89
+
90
+ def _calculate_melanin(self, r, g, b):
91
+ """
92
+ Calculate melanin level using ITA (Individual Typology Angle)
93
+ """
94
+ # Convert to LAB color space
95
+ rgb_normalized = np.array([[[r/255, g/255, b/255]]], dtype=np.float32)
96
+ lab = cv2.cvtColor(rgb_normalized, cv2.COLOR_RGB2LAB)
97
+ l, a, _ = lab[0][0]
98
+
99
+ # ITA calculation
100
+ ita = np.arctan((l - 50) / a) * 180 / np.pi
101
+
102
+ # Convert ITA to melanin level (0-100)
103
+ melanin = 100 - ((ita + 90) / 180 * 100)
104
+ return max(0, min(100, melanin))
105
+
106
+ def _get_primary_tone(self, melanin):
107
+ """
108
+ Determine primary skin tone based on melanin level
109
+ """
110
+ for tone, (low, high) in self.skin_tone_ranges.items():
111
+ if low <= melanin <= high:
112
+ return tone
113
+ return 'medium' # default
114
+
115
+ def _get_undertone(self, r, g, b):
116
+ """
117
+ Determine undertone using color ratios
118
+ """
119
+ g_r_ratio = g / r if r > 0 else 0
120
+ b_g_ratio = b / g if g > 0 else 0
121
+
122
+ for undertone, rules in self.undertone_rules.items():
123
+ g_ratio_min, g_ratio_max = rules['g/r_ratio']
124
+ b_ratio_min, b_ratio_max = rules['b/g_ratio']
125
+
126
+ if (g_ratio_min <= g_r_ratio <= g_ratio_max and
127
+ b_ratio_min <= b_g_ratio <= b_ratio_max):
128
+ return undertone
129
+
130
+ return 'neutral' # default
131
+
132
+ def _get_surface_tone(self, r, g, b):
133
+ """
134
+ Get surface complexion (may differ from undertone)
135
+ """
136
+ # Convert to HSV for surface analysis
137
+ h, s, v = colorsys.rgb_to_hsv(r/255, g/255, b/255)
138
+
139
+ if v > 0.8:
140
+ return 'bright'
141
+ elif v < 0.3:
142
+ return 'deep'
143
+ else:
144
+ return 'natural'
145
+
146
+ def _get_foundation_match(self, primary_tone, undertone):
147
+ """
148
+ Suggest foundation match based on skin analysis
149
+ """
150
+ return f"{primary_tone.capitalize()} with {undertone} undertone"
151
+
152
+ class CosmeticMatcher:
153
+ def __init__(self):
154
+ self.product_database = load_cosmetics_database()
155
+
156
+ def find_matches(self, skin_tone: dict, conditions: list) -> list:
157
+ """
158
+ Find cosmetic matches based on skin analysis
159
+ """
160
+ matches = []
161
+
162
+ for product in self.product_database:
163
+ score = self._calculate_match_score(product, skin_tone, conditions)
164
+
165
+ if score > 70:
166
+ matches.append({
167
+ 'product': product,
168
+ 'score': score,
169
+ 'reason': self._get_match_reason(product, score),
170
+ 'shades': self._find_best_shades(product, skin_tone)
171
+ })
172
+
173
+ # Sort by score
174
+ matches.sort(key=lambda x: x['score'], reverse=True)
175
+ return matches[:20] # Top 20 matches
176
+
177
+ def _calculate_match_score(self, product, skin_tone, conditions):
178
+ """
179
+ Calculate how well a product matches the user
180
+ """
181
+ score = 50 # Base score
182
+
183
+ # Undertone compatibility
184
+ if product.get('undertone') == skin_tone['undertone']:
185
+ score += 20
186
+ elif product.get('undertone') == 'neutral':
187
+ score += 10
188
+ else:
189
+ score -= 10
190
+
191
+ # Skin tone range compatibility
192
+ if skin_tone['primary_tone'] in product.get('skin_tone_ranges', []):
193
+ score += 15
194
+ elif 'all' in product.get('skin_tone_ranges', []):
195
+ score += 10
196
+
197
+ # Skin condition safety
198
+ for condition in conditions:
199
+ if self._is_safe_for_condition(product, condition):
200
+ score += 5
201
+ else:
202
+ score -= 20 # Big penalty for unsafe products
203
+
204
+ return max(0, min(100, score))
205
+
206
+ def _find_best_shades(self, product, skin_tone):
207
+ """
208
+ Find the best shade matches within a product line
209
+ """
210
+ best_shades = []
211
+
212
+ for shade in product.get('shades', []):
213
+ shade_score = self._calculate_shade_match(shade, skin_tone)
214
+
215
+ if shade_score > 80:
216
+ best_shades.append({
217
+ 'shade': shade,
218
+ 'score': shade_score,
219
+ 'reason': self._get_shade_reason(shade, skin_tone)
220
+ })
221
+
222
+ # Sort by score and return top 3
223
+ best_shades.sort(key=lambda x: x['score'], reverse=True)
224
+ return best_shades[:3]
225
+
226
+ def _calculate_shade_match(self, shade, skin_tone):
227
+ """
228
+ Calculate how well a specific shade matches
229
+ """
230
+ score = 70 # Base score
231
+
232
+ # Compare undertone
233
+ if shade.get('undertone') == skin_tone['undertone']:
234
+ score += 15
235
+ elif self._are_complementary(shade.get('undertone'), skin_tone['undertone']):
236
+ score += 10 # Complementary colors make features pop
237
+
238
+ # Depth match
239
+ melanin = skin_tone['melanin_level'] / 100
240
+ depth_diff = abs(shade.get('depth', 0.5) - melanin)
241
+ score -= depth_diff * 30
242
+
243
+ return max(0, min(100, score))
244
+
245
+ def _are_complementary(self, shade_undertone, skin_undertone):
246
+ """
247
+ Check if undertones are complementary (make each other pop)
248
+ """
249
+ complementary_pairs = {
250
+ 'warm': 'cool',
251
+ 'cool': 'warm',
252
+ 'neutral': 'all'
253
+ }
254
+ return complementary_pairs.get(shade_undertone) == skin_undertone
255
+
256
+ def _is_safe_for_condition(self, product, condition):
257
+ """
258
+ Check if product is safe for specific skin conditions
259
+ """
260
+ unsafe_ingredients = {
261
+ 'oily_skin': ['mineral oil', 'coconut oil'],
262
+ 'dry_skin': ['alcohol', 'fragrance'],
263
+ 'sensitive_skin': ['fragrance', 'essential oils', 'alcohol'],
264
+ 'acne': ['isopropyl myristate', 'lanolin']
265
+ }
266
+
267
+ condition_id = condition.get('condition')
268
+ if condition_id in unsafe_ingredients:
269
+ product_ingredients = product.get('ingredients', [])
270
+ for ingredient in unsafe_ingredients[condition_id]:
271
+ if ingredient in str(product_ingredients).lower():
272
+ return False
273
+
274
+ return True