by3axap commited on
Commit
aed1f04
·
verified ·
1 Parent(s): 6c0bfcf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +168 -161
app.py CHANGED
@@ -1,33 +1,106 @@
1
  import os
2
- import io
3
  import re
 
 
 
4
  import numpy as np
5
- from flask import Flask, render_template, request, send_file
6
- from PIL import Image
7
- import xml.etree.ElementTree as ET
8
 
 
9
  try:
10
  from moviepy.editor import VideoFileClip
11
- HAS_MOVIEPY = True
12
  except ImportError:
13
- HAS_MOVIEPY = False
14
 
15
  app = Flask(__name__)
16
 
17
- # Рекурсивная функция для удаления жестко заданных цветов внутри SVG-файлов
18
- def strip_fill_colors(elem):
19
- # Удаляем fill если он не "none" (none обычно используется для обводки)
20
- if 'fill' in elem.attrib and elem.attrib['fill'].lower() != 'none':
21
- del elem.attrib['fill']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- # Удаляем fill из инлайн-стилей (style="fill: #000; ...")
24
- if 'style' in elem.attrib:
25
- style_str = elem.attrib['style']
26
- style_str = re.sub(r'fill:\s*(?!none)[^;]+;?', '', style_str, flags=re.IGNORECASE)
27
- elem.attrib['style'] = style_str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- for child in elem:
30
- strip_fill_colors(child)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  @app.route('/')
33
  def index():
@@ -36,176 +109,110 @@ def index():
36
  @app.route('/download_svg', methods=['POST'])
37
  def download_svg():
38
  try:
39
- density = int(request.form.get('density', 76))
40
- gap_x = float(request.form.get('gap_x', 0.2))
 
 
 
41
  base_scale = float(request.form.get('base_scale', 1.0))
 
 
42
  invert = request.form.get('invert') == 'true'
43
- bg_color = request.form.get('bg_color', '#000000')
44
 
45
- canvas_w = float(request.form.get('canvas_w', 1400))
46
- canvas_h = float(request.form.get('canvas_h', 900))
47
 
48
- fill_colors = request.form.getlist('fill_colors[]')
49
- svg_files = request.files.getlist('shape_svg[]')
50
- raster = request.files.get('raster_image')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
- img_data = None
53
- if raster:
54
- img = Image.open(raster).convert('L').resize((density, int(canvas_h / (canvas_w / density))))
55
- img_data = np.array(img)
56
-
57
- dwg = ET.Element('svg', {
58
- 'xmlns': 'http://www.w3.org/2000/svg',
59
- 'width': str(canvas_w),
60
- 'height': str(canvas_h),
61
- 'viewBox': f'0 0 {canvas_w} {canvas_h}'
62
- })
63
 
64
- ET.SubElement(dwg, 'rect', {'width': '100%', 'height': '100%', 'fill': bg_color})
65
-
66
- parsed_patterns = []
67
- for i, f in enumerate(svg_files):
68
- try:
69
- # Читаем файл, убираем теги неймспейсов для корректной работы
70
- content = f.read().decode('utf-8')
71
- content = re.sub(r'\sxmlns="[^"]+"', '', content, count=1)
72
- svg_root = ET.fromstring(content)
73
-
74
- # ОЧИЩАЕМ ИСХОДНЫЕ ЦВЕТА ПАТТЕРНА
75
- strip_fill_colors(svg_root)
76
-
77
- parsed_patterns.append({
78
- 'elements': list(svg_root),
79
- 'color': fill_colors[i] if i < len(fill_colors) else '#ffffff'
80
- })
81
- except Exception as e:
82
- print("SVG Parse Error:", e)
83
- continue
84
 
85
- step = canvas_w / density
86
- rows = int(canvas_h / step)
87
- num_p = len(parsed_patterns)
88
-
89
- for r in range(rows):
90
- for c in range(density):
91
- x = c * step + step/2
92
- y = r * step + step/2
93
-
94
- bright = 1.0
95
- if img_data is not None:
96
- py, px = min(r, img_data.shape[0]-1), min(c, img_data.shape[1]-1)
97
- bright = img_data[py, px] / 255.0
98
-
99
- if invert: bright = 1.0 - bright
100
- if bright > 0.96: continue
101
-
102
- size = step * (1 - gap_x) * base_scale
103
-
104
- if num_p > 0:
105
- p_idx = int(np.floor(bright * num_p))
106
- p_idx = max(0, min(num_p - 1, p_idx))
107
- target = parsed_patterns[p_idx]
108
-
109
- group = ET.SubElement(dwg, 'g', {
110
- 'transform': f'translate({x - size/2}, {y - size/2}) scale({size/100})',
111
- 'fill': target['color']
112
- })
113
- for elem in target['elements']:
114
- group.append(elem)
115
- else:
116
- ET.SubElement(dwg, 'rect', {
117
- 'x': str(x - size/2), 'y': str(y - size/2),
118
- 'width': str(size), 'height': str(size), 'fill': fill_colors[0] if fill_colors else '#000'
119
- })
120
-
121
- svg_io = io.BytesIO(ET.tostring(dwg))
122
- return send_file(svg_io, mimetype='image/svg+xml', as_attachment=True, download_name='cell_clipper_export.svg')
123
-
124
  except Exception as e:
125
  return str(e), 500
126
 
127
-
128
  @app.route('/process_video', methods=['POST'])
129
  def process_video():
130
- if not HAS_MOVIEPY:
131
- return "MoviePy not installed", 400
132
-
 
133
  try:
134
- density = int(request.form.get('density', 76))
135
- gap_x = float(request.form.get('gap_x', 0.2))
 
 
136
  base_scale = float(request.form.get('base_scale', 1.0))
 
 
137
  invert = request.form.get('invert') == 'true'
138
- bg_color = request.form.get('bg_color', '#000000')
139
- fallback_fill = request.form.get('fallback_fill', '#000000')
140
-
141
- # Получаем готовые цветные PNG паттерны с фронтенда!
142
- png_files = request.files.getlist('pattern_pngs')
143
- pil_patterns = [Image.open(p).convert("RGBA") for p in png_files]
144
- num_p = len(pil_patterns)
145
 
146
  video_file = request.files.get('video_file')
147
  if not video_file:
148
- return "No video file", 400
149
-
150
- in_path = "temp_in.mp4"
151
- out_path = "temp_out.mp4"
152
- video_file.save(in_path)
153
- clip = VideoFileClip(in_path)
154
 
155
- w, h = clip.size
156
- step = w / density
157
- rows = int(h / step)
158
- cell_size = int(step * (1 - gap_x) * base_scale)
159
-
160
- # Ресайзим паттерны заранее под размер ячейки для скорости рендера
161
- resized_patterns = [p.resize((cell_size, cell_size), Image.Resampling.LANCZOS) for p in pil_patterns]
162
 
163
- def make_frame(get_frame, t):
164
  frame = get_frame(t)
165
- # Переводим кадр в ЧБ для анализа яркости
166
- gray = Image.fromarray(frame).convert("L")
167
- # Сжимаем кадр до размера сетки для ультра-быстрого анализа
168
- gray_small = np.array(gray.resize((density, rows)))
169
 
170
- # Создаем чистый кадр с цветом фона
171
- canvas_img = Image.new("RGB", (w, h), bg_color)
 
172
 
173
- from PIL import ImageDraw
174
- draw = ImageDraw.Draw(canvas_img)
175
-
176
- for r in range(rows):
177
- for c in range(density):
178
- bright = gray_small[r, c] / 255.0
179
- if invert: bright = 1.0 - bright
180
- if bright > 0.96: continue
181
-
182
- x = int(c * step + step/2 - cell_size/2)
183
- y = int(r * step + step/2 - cell_size/2)
184
-
185
- if num_p > 0:
186
- p_idx = int(np.floor(bright * num_p))
187
- p_idx = max(0, min(num_p - 1, p_idx))
188
- pat = resized_patterns[p_idx]
189
- # Накладываем цветной PNG паттерн с поддержкой прозрачности
190
- canvas_img.paste(pat, (x, y), mask=pat)
191
- else:
192
- draw.rectangle([x, y, x + cell_size, y + cell_size], fill=fallback_fill)
193
 
194
- return np.array(canvas_img)
195
-
196
- new_clip = clip.fl(make_frame)
197
- new_clip.write_videofile(out_path, codec="libx264", audio=False)
 
 
 
 
 
 
 
 
198
 
199
- clip.close()
200
- new_clip.close()
201
- os.remove(in_path)
202
 
203
- return send_file(out_path, mimetype='video/mp4', as_attachment=True, download_name='cell_clipper_render.mp4')
 
204
 
 
 
205
  except Exception as e:
206
- import traceback
207
- traceback.print_exc()
208
- return str(e), 500
209
 
210
  if __name__ == '__main__':
211
- app.run(host='0.0.0.0', port=7860, debug=True)
 
1
  import os
2
+ import math
3
  import re
4
+ import tempfile
5
+ from flask import Flask, render_template, request, send_file, Response
6
+ from PIL import Image, ImageOps, ImageDraw
7
  import numpy as np
 
 
 
8
 
9
+ # Проверка наличия MoviePy
10
  try:
11
  from moviepy.editor import VideoFileClip
12
+ MOVIEPY_AVAILABLE = True
13
  except ImportError:
14
+ MOVIEPY_AVAILABLE = False
15
 
16
  app = Flask(__name__)
17
 
18
+ # --- Вспомогательные функции ---
19
+
20
+ def prepare_svg_content(file_storage):
21
+ """Читает SVG и готовит его для вставки"""
22
+ try:
23
+ raw_data = file_storage.read().decode('utf-8')
24
+ vb_match = re.search(r'viewBox=["\']([^"\']+)["\']', raw_data, re.I)
25
+ vbox = vb_match.group(1) if vb_match else "0 0 100 100"
26
+
27
+ inner = re.sub(r'<svg[^>]*>', '', raw_data, flags=re.I)
28
+ inner = re.sub(r'</svg>', '', inner, flags=re.I)
29
+ inner = re.sub(r'<\?xml[^>]*\?>|<!DOCTYPE[^>]*>', '', inner, flags=re.I)
30
+
31
+ if 'fill=' not in inner:
32
+ inner = f'<g fill="black">{inner}</g>'
33
+ return inner, vbox
34
+ except:
35
+ return '<rect width="100" height="100" fill="black"/>', "0 0 100 100"
36
+
37
+ def calculate_grid_items(width, height, density, gap_x, gap_y, shift, base_scale, growth, growth_type, invert, pixel_data=None):
38
+ """Математическое ядро расчета сетки"""
39
+ step = width / density
40
+ rows = math.ceil(height / step)
41
 
42
+ cell_w_max = step * (1.0 - gap_x)
43
+ cell_h_max = step * (1.0 - gap_y)
44
+
45
+ items = []
46
+
47
+ for r in range(rows):
48
+ row_shift = (r * shift * step)
49
+ # Оптимизация цикла по колонкам
50
+ start_c = -int(density * 0.5)
51
+ end_c = int(density * 1.5)
52
+
53
+ for c in range(start_c, end_c):
54
+ grid_x = c * step + row_shift
55
+ grid_y = r * step
56
+
57
+ if grid_x < -step or grid_x > width:
58
+ continue
59
+
60
+ # Логика яркости
61
+ is_visible = True
62
+
63
+ if pixel_data is not None:
64
+ # pixel_data должен быть PIL Image (Grayscale)
65
+ sx = max(0, min(int(grid_x + step/2), width - 1))
66
+ sy = max(0, min(int(grid_y + step/2), height - 1))
67
+ try:
68
+ val = pixel_data.getpixel((sx, sy))
69
+ brightness = val / 255.0
70
+ limit = 0.5
71
+ is_visible = brightness > limit if invert else brightness < limit
72
+ except:
73
+ pass
74
+
75
+ if not is_visible:
76
+ continue
77
 
78
+ # Логика искажения (Growth)
79
+ wave = 0
80
+ if growth_type == 'sine':
81
+ wave = (math.sin(c * 0.3 + r * 0.3) + 1) / 2
82
+ elif growth_type == 'linear':
83
+ wave = (c % density) / density
84
+ elif growth_type == 'steps':
85
+ wave = (r % 5) / 5.0
86
+
87
+ mod_w = 1.0 - (wave * growth)
88
+ mod_w = max(0.01, min(1.0, mod_w))
89
+
90
+ content_w = cell_w_max * base_scale * mod_w
91
+ content_h = cell_h_max * base_scale
92
+
93
+ cx = grid_x + step / 2
94
+ cy = grid_y + step / 2
95
+
96
+ items.append({
97
+ 'cx': cx, 'cy': cy,
98
+ 'w': content_w, 'h': content_h
99
+ })
100
+
101
+ return items, step
102
+
103
+ # --- Роуты ---
104
 
105
  @app.route('/')
106
  def index():
 
109
  @app.route('/download_svg', methods=['POST'])
110
  def download_svg():
111
  try:
112
+ # Параметры
113
+ density = int(request.form.get('density', 40))
114
+ gap_x = float(request.form.get('gap_x', 0))
115
+ gap_y = float(request.form.get('gap_y', 0))
116
+ shift = float(request.form.get('shift_stagger', 0))
117
  base_scale = float(request.form.get('base_scale', 1.0))
118
+ growth = float(request.form.get('growth', 0))
119
+ growth_type = request.form.get('growth_type', 'sine')
120
  invert = request.form.get('invert') == 'true'
 
121
 
122
+ canvas_w, canvas_h = 1400, 900
 
123
 
124
+ # SVG Symbol
125
+ svg_file = request.files.get('shape_svg')
126
+ if svg_file and svg_file.filename != '':
127
+ symbol_content, v_box = prepare_svg_content(svg_file)
128
+ else:
129
+ symbol_content, v_box = ('<rect width="100" height="100" fill="black"/>', "0 0 100 100")
130
+
131
+ vb_parts = [float(x) for x in v_box.split()]
132
+ vb_w, vb_h = vb_parts[2], vb_parts[3]
133
+
134
+ # Raster Image для маски
135
+ raster_file = request.files.get('raster_image')
136
+ pixel_img = None
137
+ if raster_file and raster_file.filename != '':
138
+ img = Image.open(raster_file)
139
+ img = ImageOps.fit(img, (canvas_w, canvas_h), method=Image.Resampling.LANCZOS)
140
+ img = ImageOps.grayscale(img)
141
+ pixel_img = img
142
+
143
+ items, _ = calculate_grid_items(canvas_w, canvas_h, density, gap_x, gap_y, shift, base_scale, growth, growth_type, invert, pixel_img)
144
 
145
+ body = []
146
+ for item in items:
147
+ sx = item['w'] / vb_w
148
+ sy = item['h'] / vb_h
149
+ transform = f'translate({item["cx"]:.2f} {item["cy"]:.2f}) scale({sx:.4f} {sy:.4f}) translate({-vb_w/2:.2f} {-vb_h/2:.2f})'
150
+ body.append(f'<g transform="{transform}">{symbol_content}</g>')
151
+
152
+ svg_str = f'<svg xmlns="http://www.w3.org/2000/svg" width="{canvas_w}" height="{canvas_h}" viewBox="0 0 {canvas_w} {canvas_h}" style="background:white;">{"".join(body)}</svg>'
 
 
 
153
 
154
+ return Response(svg_str, mimetype="image/svg+xml", headers={"Content-disposition": "attachment; filename=pattern.svg"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  except Exception as e:
157
  return str(e), 500
158
 
 
159
  @app.route('/process_video', methods=['POST'])
160
  def process_video():
161
+ if not MOVIEPY_AVAILABLE:
162
+ return "MoviePy not installed on server", 500
163
+
164
+ temp_dir = tempfile.mkdtemp()
165
  try:
166
+ density = int(request.form.get('density', 40))
167
+ gap_x = float(request.form.get('gap_x', 0))
168
+ gap_y = float(request.form.get('gap_y', 0))
169
+ shift = float(request.form.get('shift_stagger', 0))
170
  base_scale = float(request.form.get('base_scale', 1.0))
171
+ growth = float(request.form.get('growth', 0))
172
+ growth_type = request.form.get('growth_type', 'sine')
173
  invert = request.form.get('invert') == 'true'
 
 
 
 
 
 
 
174
 
175
  video_file = request.files.get('video_file')
176
  if not video_file:
177
+ return "No video uploaded", 400
 
 
 
 
 
178
 
179
+ input_path = os.path.join(temp_dir, 'input.mp4')
180
+ video_file.save(input_path)
 
 
 
 
 
181
 
182
+ def process_frame(get_frame, t):
183
  frame = get_frame(t)
184
+ img_pil = Image.fromarray(frame).convert("L")
185
+ w, h = img_pil.size
 
 
186
 
187
+ # Рендеринг кадра
188
+ canvas = Image.new("RGB", (w, h), "white")
189
+ draw = ImageDraw.Draw(canvas)
190
 
191
+ # Используем ту же логику расчета
192
+ items, _ = calculate_grid_items(w, h, density, gap_x, gap_y, shift, base_scale, growth, growth_type, invert, img_pil)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
+ for item in items:
195
+ # Рисуем прямоугольники (быстро для видео)
196
+ # Если нужно SVG в видео, это сильно замедлит процесс
197
+ x0 = item['cx'] - item['w']/2
198
+ y0 = item['cy'] - item['h']/2
199
+ draw.rectangle([x0, y0, x0 + item['w'], y0 + item['h']], fill="black")
200
+
201
+ return np.array(canvas)
202
+
203
+ clip = VideoFileClip(input_path)
204
+ # Ограничение FPS для ускорения, если видео большое
205
+ # clip = clip.set_fps(24)
206
 
207
+ new_clip = clip.fl(process_frame)
 
 
208
 
209
+ output_path = os.path.join(temp_dir, 'output.mp4')
210
+ new_clip.write_videofile(output_path, codec='libx264', audio_codec='aac', fps=clip.fps)
211
 
212
+ return send_file(output_path, as_attachment=True, download_name="processed_video.mp4")
213
+
214
  except Exception as e:
215
+ return f"Error processing video: {str(e)}", 500
 
 
216
 
217
  if __name__ == '__main__':
218
+ app.run(host='0.0.0.0', port=7860)