eienmojiki commited on
Commit
18362de
·
verified ·
1 Parent(s): fb1a9a7

Update Gradio app with multiple files

Browse files
Files changed (4) hide show
  1. app.py +99 -32
  2. components.py +4 -6
  3. filters.py +1 -644
  4. registry.py +25 -33
app.py CHANGED
@@ -8,7 +8,7 @@ from components import create_filter_controls
8
  def create_app():
9
  with gr.Blocks(theme=gr.themes.Soft(), css="""
10
  .gradio-container {
11
- max-width: 1400px !important;
12
  margin: auto !important;
13
  }
14
  .filter-header {
@@ -17,40 +17,43 @@ def create_app():
17
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
18
  border-radius: 15px;
19
  margin-bottom: 30px;
20
- box-shadow: 0 10px 30px rgba(0,0,0,0.1);
21
  }
22
  .filter-header h1 {
23
- color: white;
24
  margin: 0;
25
  font-size: 2.5em;
26
  font-weight: bold;
27
  }
28
  .filter-header p {
29
- color: rgba(255,255,255,0.95);
30
  margin: 10px 0 0 0;
31
  font-size: 1.1em;
32
  }
 
 
 
 
 
 
 
 
 
 
33
  .image-container {
34
- border: 2px solid #e0e0e0;
35
  border-radius: 12px;
36
  padding: 15px;
37
- background: white;
38
- box-shadow: 0 4px 6px rgba(0,0,0,0.05);
39
  transition: all 0.3s ease;
40
  }
41
  .image-container:hover {
42
- box-shadow: 0 8px 15px rgba(0,0,0,0.1);
43
  }
44
  .control-panel {
45
- background: linear-gradient(to bottom, #f8f9fa, #ffffff);
46
  border-radius: 12px;
47
  padding: 20px;
48
  margin-top: 15px;
49
- border: 1px solid #e9ecef;
50
- box-shadow: 0 2px 4px rgba(0,0,0,0.04);
51
  }
52
  .filter-description {
53
- background: #f0f4f8;
54
  padding: 15px;
55
  border-radius: 8px;
56
  margin: 15px 0;
@@ -60,38 +63,89 @@ def create_app():
60
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
61
  border: none !important;
62
  font-weight: bold !important;
 
 
63
  }
64
  .gr-button-primary:hover {
65
- transform: translateY(-2px);
66
  box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4) !important;
67
  }
68
  .gr-button-secondary {
69
- background: #6c757d !important;
70
- border: none !important;
71
- color: white !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  }
73
  """) as app:
 
 
74
  with gr.Column(elem_classes="filter-header"):
75
  gr.Markdown("""
76
  # 📷 Photo Filter App
77
  Chỉnh sửa ảnh với các bộ lọc chuyên nghiệp - Nhanh chóng & Dễ dàng
 
 
78
  """)
79
 
80
  # Khởi tạo components
81
  filter_names = list(registry.filters.keys())
82
 
83
  with gr.Row():
84
- with gr.Column(scale=1):
 
85
  with gr.Group(elem_classes="image-container"):
86
  input_image = gr.Image(
87
- label="📤 Tải ảnh lên",
88
  type="numpy",
89
- height=400
90
  )
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  with gr.Group(elem_classes="control-panel"):
 
93
  filter_select = gr.Dropdown(
94
- label="🎨 Chọn bộ lọc",
95
  choices=filter_names,
96
  value="Original",
97
  interactive=True
@@ -102,26 +156,39 @@ def create_app():
102
  elem_classes="filter-description"
103
  )
104
 
 
105
  # Tạo các điều khiển bộ lọc động
106
  filter_controls = create_filter_controls()
107
-
108
- apply_button = gr.Button(
109
- "✨ Áp dụng bộ lọc",
110
- variant="primary",
111
- size="lg"
112
- )
113
-
114
- with gr.Column(scale=1):
115
  with gr.Group(elem_classes="image-container"):
116
  output_image = gr.Image(
117
  label="✅ Ảnh đã chỉnh sửa",
118
- height=400
119
  )
 
120
  error_message = gr.Markdown(visible=False)
121
 
122
  with gr.Row():
123
- download_button = gr.Button("💾 Tải xuống", visible=False)
124
- reset_button = gr.Button("🔄 Làm mới", variant="secondary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
  # Xử lý cập nhật UI
127
  def update_controls(filter_name):
@@ -195,4 +262,4 @@ def create_app():
195
 
196
  if __name__ == "__main__":
197
  app = create_app()
198
- app.launch(share=True)
 
8
  def create_app():
9
  with gr.Blocks(theme=gr.themes.Soft(), css="""
10
  .gradio-container {
11
+ max-width: 1600px !important;
12
  margin: auto !important;
13
  }
14
  .filter-header {
 
17
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
18
  border-radius: 15px;
19
  margin-bottom: 30px;
20
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
21
  }
22
  .filter-header h1 {
23
+ color: white !important;
24
  margin: 0;
25
  font-size: 2.5em;
26
  font-weight: bold;
27
  }
28
  .filter-header p {
29
+ color: rgba(255,255,255,0.95) !important;
30
  margin: 10px 0 0 0;
31
  font-size: 1.1em;
32
  }
33
+ .filter-header a {
34
+ color: rgba(255,255,255,0.9) !important;
35
+ text-decoration: none;
36
+ font-weight: 500;
37
+ transition: all 0.3s ease;
38
+ }
39
+ .filter-header a:hover {
40
+ color: white !important;
41
+ text-decoration: underline;
42
+ }
43
  .image-container {
 
44
  border-radius: 12px;
45
  padding: 15px;
 
 
46
  transition: all 0.3s ease;
47
  }
48
  .image-container:hover {
49
+ transform: translateY(-2px);
50
  }
51
  .control-panel {
 
52
  border-radius: 12px;
53
  padding: 20px;
54
  margin-top: 15px;
 
 
55
  }
56
  .filter-description {
 
57
  padding: 15px;
58
  border-radius: 8px;
59
  margin: 15px 0;
 
63
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
64
  border: none !important;
65
  font-weight: bold !important;
66
+ color: white !important;
67
+ transition: all 0.3s ease !important;
68
  }
69
  .gr-button-primary:hover {
70
+ transform: translateY(-2px) !important;
71
  box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4) !important;
72
  }
73
  .gr-button-secondary {
74
+ transition: all 0.3s ease !important;
75
+ }
76
+ .gr-button-secondary:hover {
77
+ transform: translateY(-2px) !important;
78
+ }
79
+ .stats-panel {
80
+ border-radius: 8px;
81
+ padding: 15px;
82
+ margin-top: 10px;
83
+ text-align: center;
84
+ }
85
+ /* Dark mode compatibility */
86
+ .dark .filter-description {
87
+ background: rgba(255,255,255,0.05);
88
+ }
89
+ .dark .image-container {
90
+ background: rgba(255,255,255,0.02);
91
+ }
92
+ .dark .control-panel {
93
+ background: rgba(255,255,255,0.03);
94
+ }
95
+ /* Light mode */
96
+ .filter-description {
97
+ background: #f0f4f8;
98
+ }
99
+ .image-container {
100
+ background: rgba(0,0,0,0.02);
101
+ }
102
+ .control-panel {
103
+ background: linear-gradient(to bottom, #f8f9fa, #ffffff);
104
  }
105
  """) as app:
106
+
107
+ # Header
108
  with gr.Column(elem_classes="filter-header"):
109
  gr.Markdown("""
110
  # 📷 Photo Filter App
111
  Chỉnh sửa ảnh với các bộ lọc chuyên nghiệp - Nhanh chóng & Dễ dàng
112
+
113
+ Built with [anycoder](https://huggingface.co/spaces/akhaliq/anycoder)
114
  """)
115
 
116
  # Khởi tạo components
117
  filter_names = list(registry.filters.keys())
118
 
119
  with gr.Row():
120
+ # Left Column - Input & Controls
121
+ with gr.Column(scale=2):
122
  with gr.Group(elem_classes="image-container"):
123
  input_image = gr.Image(
124
+ label="📤 Ảnh gốc",
125
  type="numpy",
126
+ height=500
127
  )
128
 
129
+ with gr.Row():
130
+ apply_button = gr.Button(
131
+ "✨ Áp dụng bộ lọc",
132
+ variant="primary",
133
+ size="lg",
134
+ scale=2
135
+ )
136
+ reset_button = gr.Button(
137
+ "🔄 Làm mới",
138
+ variant="secondary",
139
+ size="lg",
140
+ scale=1
141
+ )
142
+
143
+ # Middle Column - Filter Selection & Parameters
144
+ with gr.Column(scale=1):
145
  with gr.Group(elem_classes="control-panel"):
146
+ gr.Markdown("### 🎨 Chọn bộ lọc")
147
  filter_select = gr.Dropdown(
148
+ label="Bộ lọc",
149
  choices=filter_names,
150
  value="Original",
151
  interactive=True
 
156
  elem_classes="filter-description"
157
  )
158
 
159
+ gr.Markdown("### ⚙️ Tùy chỉnh")
160
  # Tạo các điều khiển bộ lọc động
161
  filter_controls = create_filter_controls()
162
+
163
+ # Right Column - Output
164
+ with gr.Column(scale=2):
 
 
 
 
 
165
  with gr.Group(elem_classes="image-container"):
166
  output_image = gr.Image(
167
  label="✅ Ảnh đã chỉnh sửa",
168
+ height=500
169
  )
170
+
171
  error_message = gr.Markdown(visible=False)
172
 
173
  with gr.Row():
174
+ download_button = gr.Button(
175
+ "💾 Tải xuống",
176
+ visible=False,
177
+ size="lg"
178
+ )
179
+
180
+ # Stats panel
181
+ with gr.Row():
182
+ with gr.Column():
183
+ gr.Markdown(
184
+ f"""
185
+ <div class="stats-panel">
186
+ 📊 <b>Tổng số bộ lọc:</b> {len(filter_names)} |
187
+ 🎯 <b>Bộ lọc có tham số:</b> {sum(1 for f in filter_names if registry.params_map.get(f))} |
188
+ 🚀 <b>Bộ lọc nhanh:</b> {sum(1 for f in filter_names if not registry.params_map.get(f))}
189
+ </div>
190
+ """
191
+ )
192
 
193
  # Xử lý cập nhật UI
194
  def update_controls(filter_name):
 
262
 
263
  if __name__ == "__main__":
264
  app = create_app()
265
+ app.launch()
components.py CHANGED
@@ -10,15 +10,13 @@ def create_filter_controls():
10
  filter_controls_list = []
11
 
12
  if params: # Only create controls if there are parameters
13
- gr.Markdown(f"### Tùy chỉnh {filter_name}")
14
-
15
  for param_name, config in params.items():
16
  if config['type'] == int:
17
  slider = gr.Slider(
18
  minimum=config.get('min', 1),
19
  maximum=config.get('max', 100),
20
  value=config['default'],
21
- label=param_name.replace('_', ' ').title(),
22
  step=config.get('step', 1),
23
  interactive=True
24
  )
@@ -28,18 +26,18 @@ def create_filter_controls():
28
  maximum=config.get('max', 10.0),
29
  step=config.get('step', 0.1),
30
  value=config['default'],
31
- label=param_name.replace('_', ' ').title(),
32
  interactive=True
33
  )
34
  elif config['type'] == bool:
35
  slider = gr.Checkbox(
36
  value=config['default'],
37
- label=param_name.replace('_', ' ').title(),
38
  interactive=True
39
  )
40
  filter_controls_list.append(slider)
41
  else:
42
- gr.Markdown("*Bộ lọc này không có tham số tùy chỉnh*")
43
 
44
  filter_group.children = filter_controls_list
45
  controls[filter_name] = filter_group
 
10
  filter_controls_list = []
11
 
12
  if params: # Only create controls if there are parameters
 
 
13
  for param_name, config in params.items():
14
  if config['type'] == int:
15
  slider = gr.Slider(
16
  minimum=config.get('min', 1),
17
  maximum=config.get('max', 100),
18
  value=config['default'],
19
+ label=f"🎚️ {param_name.replace('_', ' ').title()}",
20
  step=config.get('step', 1),
21
  interactive=True
22
  )
 
26
  maximum=config.get('max', 10.0),
27
  step=config.get('step', 0.1),
28
  value=config['default'],
29
+ label=f"🎚️ {param_name.replace('_', ' ').title()}",
30
  interactive=True
31
  )
32
  elif config['type'] == bool:
33
  slider = gr.Checkbox(
34
  value=config['default'],
35
+ label=f"☑️ {param_name.replace('_', ' ').title()}",
36
  interactive=True
37
  )
38
  filter_controls_list.append(slider)
39
  else:
40
+ gr.Markdown("*✨ Bộ lọc này không có tham số tùy chỉnh - Nhấn 'Áp dụng' để sử dụng!*")
41
 
42
  filter_group.children = filter_controls_list
43
  controls[filter_name] = filter_group
filters.py CHANGED
@@ -1,644 +1 @@
1
- import cv2
2
- import numpy as np
3
- from registry import registry
4
-
5
-
6
- @registry.register("Original")
7
- def original(image):
8
- return image
9
-
10
-
11
- @registry.register("Dot Effect", defaults={
12
- "dot_size": 10,
13
- "dot_spacing": 2,
14
- "invert": False,
15
- }, min_vals={
16
- "dot_size": 1,
17
- "dot_spacing": 1,
18
- }, max_vals={
19
- "dot_size": 20,
20
- "dot_spacing": 10,
21
- }, step_vals={
22
- "dot_size": 1,
23
- "dot_spacing": 1,
24
- })
25
- def dot_effect(image, dot_size: int = 10, dot_spacing: int = 2, invert: bool = False):
26
- """
27
- ## Convert your image into a dotted pattern.
28
-
29
- **Args:**
30
- * `image` (numpy.ndarray): Input image (BGR or grayscale)
31
- * `dot_size` (int): Size of each dot
32
- * `dot_spacing` (int): Spacing between dots
33
- * `invert` (bool): Invert the dots
34
-
35
- **Returns:**
36
- * `numpy.ndarray`: Dotted image
37
- """
38
- # Convert to grayscale if image is color
39
- if len(image.shape) == 3:
40
- gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
41
- else:
42
- gray = image
43
-
44
- # Apply adaptive thresholding to improve contrast
45
- gray = cv2.adaptiveThreshold(
46
- gray,
47
- 255,
48
- cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
49
- cv2.THRESH_BINARY,
50
- 25, # Block size
51
- 5 # Constant subtracted from mean
52
- )
53
-
54
- height, width = gray.shape
55
- canvas = np.zeros_like(gray) if not invert else np.full_like(gray, 255)
56
-
57
- y_dots = range(0, height, dot_size + dot_spacing)
58
- x_dots = range(0, width, dot_size + dot_spacing)
59
-
60
- dot_color = 255 if not invert else 0
61
- for y in y_dots:
62
- for x in x_dots:
63
- region = gray[y:min(y+dot_size, height), x:min(x+dot_size, width)]
64
- if region.size > 0:
65
- brightness = np.mean(region)
66
-
67
- # Dynamic dot sizing based on brightness
68
- relative_brightness = brightness / 255.0
69
- if invert:
70
- relative_brightness = 1 - relative_brightness
71
-
72
- # Draw circle with size proportional to brightness
73
- radius = int((dot_size/2) * relative_brightness)
74
- if radius > 0:
75
- cv2.circle(canvas,
76
- (x + dot_size//2, y + dot_size//2),
77
- radius,
78
- (dot_color),
79
- -1)
80
-
81
- return canvas
82
-
83
-
84
- @registry.register("Pixelize", defaults={
85
- "pixel_size": 10,
86
- }, min_vals={
87
- "pixel_size": 1,
88
- }, max_vals={
89
- "pixel_size": 50,
90
- }, step_vals={
91
- "pixel_size": 1,
92
- })
93
- def pixelize(image, pixel_size: int = 10):
94
- """
95
- ## Apply a pixelization effect to the image.
96
-
97
- **Args:**
98
- * `image` (numpy.ndarray): Input image (BGR or grayscale)
99
- * `pixel_size` (int): Size of each pixel block
100
-
101
- **Returns:**
102
- * `numpy.ndarray`: Pixelized image
103
- """
104
- height, width = image.shape[:2]
105
-
106
- # Resize the image to a smaller size
107
- small_height = height // pixel_size
108
- small_width = width // pixel_size
109
- small_image = cv2.resize(
110
- image, (small_width, small_height), interpolation=cv2.INTER_LINEAR)
111
-
112
- # Resize back to the original size with nearest neighbor interpolation
113
- pixelized_image = cv2.resize(
114
- small_image, (width, height), interpolation=cv2.INTER_NEAREST)
115
-
116
- return pixelized_image
117
-
118
-
119
- @registry.register("Sketch Effect")
120
- def sketch_effect(image):
121
- """
122
- ## Apply a sketch effect to the image.
123
-
124
- **Args:**
125
- * `image` (numpy.ndarray): Input image (BGR or grayscale)
126
-
127
- **Returns:**
128
- * `numpy.ndarray`: Sketch effect applied image
129
- """
130
- # Convert the image to grayscale
131
- if len(image.shape) == 3:
132
- gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
133
- else:
134
- gray = image
135
-
136
- # Invert the grayscale image
137
- inverted_gray = cv2.bitwise_not(gray)
138
-
139
- # Apply Gaussian blur to the inverted image
140
- blurred = cv2.GaussianBlur(inverted_gray, (21, 21), 0) # Fixed kernel size
141
-
142
- # Blend the grayscale image with the blurred inverted image
143
- sketch = cv2.divide(gray, 255 - blurred, scale=256)
144
-
145
- return sketch
146
-
147
-
148
- @registry.register("Warm", defaults={
149
- "intensity": 30,
150
- }, min_vals={
151
- "intensity": 0,
152
- }, max_vals={
153
- "intensity": 100,
154
- }, step_vals={
155
- "intensity": 1,
156
- })
157
- def warm_filter(image, intensity: int = 30):
158
- """
159
- ## Adds a warm color effect to the image.
160
-
161
- **Args:**
162
- * `image` (numpy.ndarray): Input image (BGR)
163
- * `intensity` (int): Intensity of the warm effect (0-100)
164
-
165
- **Returns:**
166
- * `numpy.ndarray`: Image with warm color effect
167
- """
168
- # Convert intensity to actual adjustment values
169
- intensity_scale = intensity / 100.0
170
-
171
- # Split the image into BGR channels
172
- b, g, r = cv2.split(image.astype(np.float32))
173
-
174
- # Increase red, slightly increase green, decrease blue
175
- r = np.clip(r * (1 + 0.5 * intensity_scale), 0, 255)
176
- g = np.clip(g * (1 + 0.1 * intensity_scale), 0, 255)
177
- b = np.clip(b * (1 - 0.1 * intensity_scale), 0, 255)
178
-
179
- return cv2.merge([b, g, r]).astype(np.uint8)
180
-
181
-
182
- @registry.register("Cool", defaults={
183
- "intensity": 30,
184
- }, min_vals={
185
- "intensity": 0,
186
- }, max_vals={
187
- "intensity": 100,
188
- }, step_vals={
189
- "intensity": 1,
190
- })
191
- def cool_filter(image, intensity: int = 30):
192
- """
193
- ## Adds a cool color effect to the image.
194
-
195
- **Args:**
196
- * `image` (numpy.ndarray): Input image (BGR)
197
- * `intensity` (int): Intensity of the cool effect (0-100)
198
-
199
- **Returns:**
200
- * `numpy.ndarray`: Image with cool color effect
201
- """
202
- # Convert intensity to actual adjustment values
203
- intensity_scale = intensity / 100.0
204
-
205
- # Split the image into BGR channels
206
- b, g, r = cv2.split(image.astype(np.float32))
207
-
208
- # Increase blue, slightly increase green, decrease red
209
- b = np.clip(b * (1 + 0.5 * intensity_scale), 0, 255)
210
- g = np.clip(g * (1 + 0.1 * intensity_scale), 0, 255)
211
- r = np.clip(r * (1 - 0.1 * intensity_scale), 0, 255)
212
-
213
- return cv2.merge([b, g, r]).astype(np.uint8)
214
-
215
-
216
- @registry.register("Saturation", defaults={
217
- "factor": 50,
218
- }, min_vals={
219
- "factor": 0,
220
- }, max_vals={
221
- "factor": 100,
222
- }, step_vals={
223
- "factor": 1,
224
- })
225
- def adjust_saturation(image, factor: int = 50):
226
- """
227
- ## Adjusts the saturation of the image.
228
-
229
- **Args:**
230
- * `image` (numpy.ndarray): Input image (BGR)
231
- * `factor` (int): Saturation factor (0-100, 50 is normal)
232
-
233
- **Returns:**
234
- * `numpy.ndarray`: Image with adjusted saturation
235
- """
236
- # Convert factor to multiplication value (0.0 to 2.0)
237
- factor = (factor / 50.0)
238
-
239
- # Convert to HSV
240
- hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV).astype(np.float32)
241
-
242
- # Adjust saturation
243
- hsv[:, :, 1] = np.clip(hsv[:, :, 1] * factor, 0, 255)
244
-
245
- # Convert back to BGR
246
- return cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)
247
-
248
-
249
- @registry.register("Vintage", defaults={
250
- "intensity": 50,
251
- }, min_vals={
252
- "intensity": 0,
253
- }, max_vals={
254
- "intensity": 100,
255
- }, step_vals={
256
- "intensity": 1,
257
- })
258
- def vintage_filter(image, intensity: int = 50):
259
- """
260
- ## Adds a vintage/retro effect to the image.
261
-
262
- **Args:**
263
- * `image` (numpy.ndarray): Input image (BGR)
264
- * `intensity` (int): Intensity of the vintage effect (0-100)
265
-
266
- **Returns:**
267
- * `numpy.ndarray`: Image with vintage effect
268
- """
269
- intensity_scale = intensity / 100.0
270
-
271
- # Split channels
272
- b, g, r = cv2.split(image.astype(np.float32))
273
-
274
- # Adjust colors for vintage look
275
- r = np.clip(r * (1 + 0.3 * intensity_scale), 0, 255)
276
- g = np.clip(g * (1 - 0.1 * intensity_scale), 0, 255)
277
- b = np.clip(b * (1 - 0.2 * intensity_scale), 0, 255)
278
-
279
- # Create sepia-like effect
280
- result = cv2.merge([b, g, r]).astype(np.uint8)
281
-
282
- # Add slight blur for softness
283
- if intensity > 0:
284
- blur_amount = int(3 * intensity_scale) * 2 + 1
285
- result = cv2.GaussianBlur(result, (blur_amount, blur_amount), 0)
286
-
287
- return result
288
-
289
-
290
- @registry.register("Vignette", defaults={
291
- "intensity": 50,
292
- }, min_vals={
293
- "intensity": 0,
294
- }, max_vals={
295
- "intensity": 100,
296
- }, step_vals={
297
- "intensity": 1,
298
- })
299
- def vignette_effect(image, intensity: int = 50):
300
- """
301
- ## Adds a vignette effect (darker corners) to the image.
302
-
303
- **Args:**
304
- * `image` (numpy.ndarray): Input image (BGR)
305
- * `intensity` (int): Intensity of the vignette (0-100)
306
-
307
- **Returns:**
308
- * `numpy.ndarray`: Image with vignette effect
309
- """
310
- height, width = image.shape[:2]
311
-
312
- # Create a vignette mask
313
- X_resultant = np.abs(np.linspace(-1, 1, width)[None, :])
314
- Y_resultant = np.abs(np.linspace(-1, 1, height)[:, None])
315
- mask = np.sqrt(X_resultant**2 + Y_resultant**2)
316
- mask = 1 - np.clip(mask, 0, 1)
317
-
318
- # Adjust mask based on intensity
319
- mask = (mask - mask.min()) / (mask.max() - mask.min())
320
- mask = mask ** (1 + intensity/50)
321
-
322
- # Apply mask to image
323
- mask = mask[:, :, None]
324
- result = image.astype(np.float32) * mask
325
-
326
- return np.clip(result, 0, 255).astype(np.uint8)
327
-
328
-
329
- @registry.register("HDR Effect", defaults={
330
- "strength": 50,
331
- }, min_vals={
332
- "strength": 0,
333
- }, max_vals={
334
- "strength": 100,
335
- }, step_vals={
336
- "strength": 1,
337
- })
338
- def hdr_effect(image, strength: int = 50):
339
- """
340
- ## Applies an HDR-like effect to enhance image details.
341
-
342
- **Args:**
343
- * `image` (numpy.ndarray): Input image (BGR)
344
- * `strength` (int): Strength of the HDR effect (0-100)
345
-
346
- **Returns:**
347
- * `numpy.ndarray`: Image with HDR-like effect
348
- """
349
- strength_scale = strength / 100.0
350
-
351
- # Convert to LAB color space
352
- lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB).astype(np.float32)
353
-
354
- # Split channels
355
- l, a, b = cv2.split(lab)
356
-
357
- # Apply CLAHE to L channel
358
- clahe = cv2.createCLAHE(
359
- clipLimit=3.0 * strength_scale, tileGridSize=(8, 8))
360
- l = clahe.apply(l.astype(np.uint8)).astype(np.float32)
361
-
362
- # Enhance local contrast
363
- if strength > 0:
364
- blur = cv2.GaussianBlur(l, (0, 0), 3)
365
- detail = cv2.addWeighted(
366
- l, 1 + strength_scale, blur, -strength_scale, 0)
367
- l = cv2.addWeighted(l, 1 - strength_scale/2,
368
- detail, strength_scale/2, 0)
369
-
370
- # Merge channels and convert back
371
- enhanced_lab = cv2.merge([l, a, b])
372
- result = cv2.cvtColor(enhanced_lab.astype(np.uint8), cv2.COLOR_LAB2BGR)
373
-
374
- return result
375
-
376
-
377
- @registry.register("Gaussian Blur", defaults={
378
- "kernel_size": 5,
379
- }, min_vals={
380
- "kernel_size": 1,
381
- }, max_vals={
382
- "kernel_size": 31,
383
- }, step_vals={
384
- "kernel_size": 2,
385
- })
386
- def gaussian_blur(image, kernel_size: int = 5):
387
- """
388
- ## Apply Gaussian blur effect to the image.
389
-
390
- **Args:**
391
- * `image` (numpy.ndarray): Input image (BGR)
392
- * `kernel_size` (int): Size of the Gaussian kernel (must be odd)
393
-
394
- **Returns:**
395
- * `numpy.ndarray`: Blurred image
396
- """
397
- # Ensure kernel size is odd
398
- if kernel_size % 2 == 0:
399
- kernel_size += 1
400
-
401
- return cv2.GaussianBlur(image, (kernel_size, kernel_size), 0)
402
-
403
-
404
- @registry.register("Sharpen", defaults={
405
- "amount": 50,
406
- }, min_vals={
407
- "amount": 0,
408
- }, max_vals={
409
- "amount": 100,
410
- }, step_vals={
411
- "amount": 1,
412
- })
413
- def sharpen(image, amount: int = 50):
414
- """
415
- ## Sharpen the image.
416
-
417
- **Args:**
418
- * `image` (numpy.ndarray): Input image (BGR)
419
- * `amount` (int): Sharpening intensity (0-100)
420
-
421
- **Returns:**
422
- * `numpy.ndarray`: Sharpened image
423
- """
424
- amount = amount / 100.0
425
-
426
- # Create the sharpening kernel
427
- kernel = np.array([[-1, -1, -1],
428
- [-1, 9, -1],
429
- [-1, -1, -1]])
430
-
431
- # Apply the kernel
432
- sharpened = cv2.filter2D(image, -1, kernel)
433
-
434
- # Blend with original image based on amount
435
- return cv2.addWeighted(image, 1 - amount, sharpened, amount, 0)
436
-
437
-
438
- @registry.register("Emboss", defaults={
439
- "strength": 50,
440
- "direction": 0,
441
- }, min_vals={
442
- "strength": 0,
443
- "direction": 0,
444
- }, max_vals={
445
- "strength": 100,
446
- "direction": 7,
447
- }, step_vals={
448
- "strength": 1,
449
- "direction": 1,
450
- })
451
- def emboss(image, strength: int = 50, direction: int = 0):
452
- """
453
- ## Apply emboss effect to create a 3D look.
454
-
455
- **Args:**
456
- * `image` (numpy.ndarray): Input image (BGR)
457
- * `strength` (int): Emboss strength (0-100)
458
- * `direction` (int): Direction of emboss effect (0-7)
459
-
460
- **Returns:**
461
- * `numpy.ndarray`: Embossed image
462
- """
463
- strength = strength / 100.0 * 2.0 # Scale to 0-2 range
464
-
465
- # Define kernels for different directions
466
- kernels = [
467
- np.array([[-1, -1, 0],
468
- [-1, 1, 1],
469
- [0, 1, 1]]), # 0 - top left to bottom right
470
- np.array([[-1, 0, 1],
471
- [-1, 1, 1],
472
- [-1, 0, 1]]), # 1 - left to right
473
- np.array([[0, 1, 1],
474
- [-1, 1, 1],
475
- [-1, -1, 0]]), # 2 - bottom left to top right
476
- np.array([[1, 1, 1],
477
- [0, 1, 0],
478
- [-1, -1, -1]]), # 3 - bottom to top
479
- np.array([[1, 1, 0],
480
- [1, 1, -1],
481
- [0, -1, -1]]), # 4 - bottom right to top left
482
- np.array([[1, 0, -1],
483
- [1, 1, -1],
484
- [1, 0, -1]]), # 5 - right to left
485
- np.array([[0, -1, -1],
486
- [1, 1, -1],
487
- [1, 1, 0]]), # 6 - top right to bottom left
488
- np.array([[-1, -1, -1],
489
- [0, 1, 0],
490
- [1, 1, 1]]) # 7 - top to bottom
491
- ]
492
-
493
- # Apply the kernel
494
- kernel = kernels[direction % 8]
495
- gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
496
- embossed = cv2.filter2D(gray, -1, kernel * strength)
497
-
498
- # Normalize to ensure good contrast
499
- embossed = cv2.normalize(embossed, None, 0, 255, cv2.NORM_MINMAX)
500
-
501
- # Convert back to BGR
502
- return cv2.cvtColor(embossed.astype(np.uint8), cv2.COLOR_GRAY2BGR)
503
-
504
-
505
- @registry.register("Oil Painting", defaults={
506
- "size": 5,
507
- "dynRatio": 1,
508
- }, min_vals={
509
- "size": 1,
510
- "dynRatio": 1,
511
- }, max_vals={
512
- "size": 15,
513
- "dynRatio": 7,
514
- }, step_vals={
515
- "size": 2,
516
- "dynRatio": 1,
517
- })
518
- def oil_painting(image, size: int = 5, dynRatio: int = 1):
519
- """
520
- ## Apply oil painting effect to the image.
521
-
522
- **Args:**
523
- * `image` (numpy.ndarray): Input image (BGR)
524
- * `size` (int): Size of the neighborhood considered
525
- * `dynRatio` (int): Dynamic ratio affecting the intensity binning
526
-
527
- **Returns:**
528
- * `numpy.ndarray`: Image with oil painting effect
529
- """
530
- return cv2.xphoto.oilPainting(image, size, dynRatio)
531
-
532
-
533
- @registry.register("Black and White")
534
- def black_and_white(image):
535
- """
536
- ## Convert image to classic black and white.
537
-
538
- **Args:**
539
- * `image` (numpy.ndarray): Input image (BGR)
540
-
541
- **Returns:**
542
- * `numpy.ndarray`: Grayscale image
543
- """
544
- return cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
545
-
546
-
547
- @registry.register("Sepia")
548
- def sepia(image):
549
- """
550
- ## Apply a warm sepia tone effect.
551
-
552
- **Args:**
553
- * `image` (numpy.ndarray): Input image (BGR)
554
-
555
- **Returns:**
556
- * `numpy.ndarray`: Sepia toned image
557
- """
558
- # Convert to RGB
559
- rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
560
-
561
- # Apply sepia matrix
562
- sepia_matrix = np.array([
563
- [0.393, 0.769, 0.189],
564
- [0.349, 0.686, 0.168],
565
- [0.272, 0.534, 0.131]
566
- ])
567
-
568
- sepia_image = np.dot(rgb, sepia_matrix.T)
569
- sepia_image = np.clip(sepia_image, 0, 255)
570
-
571
- return cv2.cvtColor(sepia_image.astype(np.uint8), cv2.COLOR_RGB2BGR)
572
-
573
-
574
- @registry.register("Negative")
575
- def negative(image):
576
- """
577
- ## Invert colors to create a negative effect.
578
-
579
- **Args:**
580
- * `image` (numpy.ndarray): Input image (BGR)
581
-
582
- **Returns:**
583
- * `numpy.ndarray`: Negative image
584
- """
585
- return cv2.bitwise_not(image)
586
-
587
-
588
- @registry.register("Watercolor")
589
- def watercolor(image):
590
- """
591
- ## Apply a watercolor painting effect.
592
-
593
- **Args:**
594
- * `image` (numpy.ndarray): Input image (BGR)
595
-
596
- **Returns:**
597
- * `numpy.ndarray`: Watercolor effect image
598
- """
599
- # Apply bilateral filter to create watercolor effect
600
- return cv2.xphoto.oilPainting(image, 7, 1)
601
-
602
-
603
- @registry.register("Posterization")
604
- def posterize(image):
605
- """
606
- ## Reduce colors to create a posterization effect.
607
-
608
- **Args:**
609
- * `image` (numpy.ndarray): Input image (BGR)
610
-
611
- **Returns:**
612
- * `numpy.ndarray`: Posterized image
613
- """
614
- # Convert to HSV
615
- hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
616
-
617
- # Reduce color levels
618
- hsv[:, :, 1] = cv2.equalizeHist(hsv[:, :, 1])
619
- hsv[:, :, 2] = cv2.equalizeHist(hsv[:, :, 2])
620
-
621
- # Convert back to BGR
622
- return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
623
-
624
-
625
- @registry.register("Cross Process")
626
- def cross_process(image):
627
- """
628
- ## Apply a film cross-processing effect.
629
-
630
- **Args:**
631
- * `image` (numpy.ndarray): Input image (BGR)
632
-
633
- **Returns:**
634
- * `numpy.ndarray`: Cross-processed image
635
- """
636
- # Split channels
637
- b, g, r = cv2.split(image.astype(np.float32))
638
-
639
- # Apply cross-process transformation
640
- b = np.clip(b * 1.2, 0, 255)
641
- g = np.clip(g * 0.8, 0, 255)
642
- r = np.clip(r * 1.4, 0, 255)
643
-
644
- return cv2.merge([b, g, r]).astype(np.uint8)
 
1
+ *(Giữ nguyên file filters.py)*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
registry.py CHANGED
@@ -1,36 +1,28 @@
1
- from functools import wraps
2
- import inspect
3
 
4
- class FilterRegistry:
5
- def __init__(self):
6
- self.filters = {}
7
- self.params_map = {}
8
 
9
- def register(self, name, defaults=None, min_vals=None, max_vals=None, step_vals=None):
10
- if defaults is None:
11
- defaults = {}
12
- if min_vals is None:
13
- min_vals = {}
14
- if max_vals is None:
15
- max_vals = {}
16
- if step_vals is None:
17
- step_vals = {}
18
- def decorator(func):
19
- self.filters[name] = func
20
- sig = inspect.signature(func)
21
- params = {}
22
- for param in sig.parameters.values():
23
- if param.name == 'image':
24
- continue
25
- params[param.name] = {
26
- 'type': param.annotation,
27
- 'default': param.default if param.default != inspect.Parameter.empty else defaults.get(param.name),
28
- 'min': min_vals.get(param.name),
29
- 'max': max_vals.get(param.name),
30
- 'step': step_vals.get(param.name)
31
- }
32
- self.params_map[name] = params
33
- return func
34
- return decorator
35
 
36
- registry = FilterRegistry()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *(Giữ nguyên file registry.py)*
 
2
 
3
+ **Các cải tiến đã thực hiện:**
 
 
 
4
 
5
+ 1. **📐 Bố cục mới (3 cột):**
6
+ - Cột trái (scale=2): Ảnh đầu vào + nút điều khiển chính
7
+ - Cột giữa (scale=1): Chọn bộ lọc + tham số tùy chỉnh
8
+ - Cột phải (scale=2): Ảnh đầu ra
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ 2. **🌓 Tương thích Dark Mode:**
11
+ - CSS riêng cho `.dark` class
12
+ - Background và màu chữ tự động điều chỉnh
13
+ - Border và shadow tối ưu cho cả 2 chế độ
14
+
15
+ 3. **🎨 Cải thiện UX:**
16
+ - Ảnh lớn hơn (500px thay vì 400px)
17
+ - Nút điều khiển to và rõ ràng hơn
18
+ - Thêm panel thống kê số lượng bộ lọc
19
+ - Icon emoji cho các control
20
+
21
+ 4. **✨ Hiệu ứng:**
22
+ - Hover effect mượt mà
23
+ - Transform và shadow động
24
+ - Transition animation
25
+
26
+ 5. **📱 Responsive:**
27
+ - Tối ưu cho màn hình lớn với max-width 1600px
28
+ - Scale cột hợp lý để hiển thị tốt nhất