HuguesdeF commited on
Commit
676a148
1 Parent(s): 76e12d1

added files

Browse files
Corriger.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import streamlit_authenticator as stauth
3
+ from code.functions import pipeline_svg
4
+ from PIL import Image
5
+ import cv2
6
+ import numpy as np
7
+ from io import BytesIO
8
+ import copy
9
+ import yaml
10
+ from yaml.loader import SafeLoader
11
+
12
+ logo = Image.open("seguinmoreau.png")
13
+ st.set_page_config(
14
+ page_title="Moulinette Logos",
15
+ page_icon=logo,
16
+ layout="wide",
17
+ initial_sidebar_state="expanded"
18
+ )
19
+
20
+ # Authentication
21
+
22
+ with open('users.yaml') as file:
23
+ config = yaml.load(file, Loader=SafeLoader)
24
+
25
+ authenticator = stauth.Authenticate(
26
+ config['credentials'],
27
+ config['cookie']['name'],
28
+ config['cookie']['key'],
29
+ config['cookie']['expiry_days'],
30
+ config['preauthorized']
31
+ )
32
+
33
+ name, authentication_status, username = authenticator.login('Login', 'main')
34
+
35
+ if not authentication_status:
36
+ st.error("Nom d'utilisateur ou mot de passe incorrect")
37
+ elif authentication_status is None:
38
+ st.warning("Rentrer nom d'utilisateur et mot de passe")
39
+ elif authentication_status:
40
+ authenticator.logout('Logout', 'main')
41
+ # ------------------------------
42
+
43
+ inch_value = 2.54
44
+
45
+ logo = Image.open('seguinmoreau.png')
46
+ st.image(logo, width=200)
47
+ st.markdown(
48
+ """
49
+ # Boîte à Outils de correction de logos :wrench:
50
+
51
+ Bienvenue dans la boîte à outils de correction de logos de Seguin Moreau.
52
+
53
+ ### :hammer: Les outils
54
+ Dans cette boîte à outils, vous trouverez:
55
+ * Un outil de Correction automatique de logo (enlever les petits défauts, lissage, vectorisation, grossissement des traits trop fins).
56
+
57
+ ### :bulb: Mode d'emploi
58
+ * Cliquer sur 'Browse files'
59
+ * Sélectionner un logo
60
+ * La correction est automatique. Si la correction ne vous convient pas, il est possible de régler les paramètres en cliquant sur 'Paramétrage' à droite de l'image.
61
+ * Les deux paramètres permettent de corriger les défauts liés à la présence de gris sur le logo ou la 'pixélisation' du logo trop importante.
62
+
63
+ """
64
+ )
65
+
66
+ uploaded_files = st.file_uploader("Choisir un logo", accept_multiple_files=True)
67
+
68
+ image_width = 500
69
+ size_value = st.slider("Largeur de trait minimum", min_value=1, max_value=21, value=7, step=2)
70
+
71
+ size_value = (size_value - 1) // 2
72
+
73
+ # kernel_type_str = st.selectbox("Kernel type", ["Ellipse", "Rectangle", "Cross"])
74
+ kernel_type_str = "Ellipse"
75
+ dict_kernel_type = {"Ellipse": cv2.MORPH_ELLIPSE, "Rectangle": cv2.MORPH_RECT, "Cross": cv2.MORPH_CROSS}
76
+ kernel_type = dict_kernel_type[kernel_type_str]
77
+
78
+ for uploaded_file in uploaded_files:
79
+ col1, col2, col3 = st.columns([1, 1, 1])
80
+ col3.markdown("---")
81
+
82
+ image = Image.open(uploaded_file).convert('L')
83
+ image_input = np.array(image)
84
+ image = copy.deepcopy(image_input)
85
+ col1.image(image_input / 255.0, caption="Image d'entrée", use_column_width='auto')
86
+
87
+ with col3:
88
+ with st.expander(":gear: Paramétrage"):
89
+ st.write("Si l'image contient du gris, faire varier le seuil ci-dessous:")
90
+ threshold = st.slider("Seuil pour convertir l'image en noir&blanc.", min_value=0, max_value=255,
91
+ value=0,
92
+ step=1, key=f"{uploaded_file}_slider_threshold")
93
+ st.write("Si l'image est pixelisée, ou contient trop de détails, "
94
+ "augmenter la valeur ci-dessous:")
95
+ blur_value = st.slider("Seuil pour lisser l'image", min_value=1, max_value=11, value=1, step=2,
96
+ key=f"{uploaded_file}_slider_gaussian_sigma")
97
+ st.write("Si l'image contient des traits très fin (de l'odre du pixel),"
98
+ " augmenter le seuil ci-dessous, de 1 par 1:")
99
+ dilate_lines_value = st.slider("Dilatation de l'image d'origine: (en pixels)", min_value=0, max_value=5,
100
+ value=0, step=1, key=f"{uploaded_file}_slider_dilation_image")
101
+
102
+ st.write("Taille d'exportation d'image:")
103
+
104
+ dpi_value = st.number_input("Valeur dpi:", key=f"{uploaded_file}_number_dpi_value", value=200)
105
+ side_width_value = st.number_input("Taille max de côté cible (cm):",
106
+ key=f"{uploaded_file}_number_target_value", value=20)
107
+ new_largest_side_value = int(side_width_value / inch_value * dpi_value)
108
+
109
+ h, w, *_ = image.shape
110
+
111
+ # Resize image
112
+ ratio = w / h
113
+ if ratio > 1:
114
+ width = new_largest_side_value
115
+ height = int(new_largest_side_value / ratio)
116
+ else:
117
+ height = new_largest_side_value
118
+ width = int(ratio * new_largest_side_value)
119
+
120
+ target_width_value = st.number_input("Largeur cible (cm):", key=f"{uploaded_file}_number_width_value",
121
+ value=0)
122
+ target_height_value = st.number_input("Hauteur cible (cm):", key=f"{uploaded_file}_number_height_value",
123
+ value=0)
124
+
125
+ if target_width_value > 0 and target_height_value == 0:
126
+ width = int(target_width_value / inch_value * dpi_value)
127
+ height = int(width / ratio)
128
+ elif target_height_value > 0 and target_width_value == 0:
129
+ height = int(target_height_value / inch_value * dpi_value)
130
+ width = int(height * ratio)
131
+ elif target_height_value > 0 and target_width_value > 0:
132
+ st.warning("Vous ne pouvez pas modifier la largeur et la hauteur simultanément.")
133
+
134
+ if threshold > 0:
135
+ image = (image > threshold) * 255
136
+ image = image.astype('uint8')
137
+
138
+ if blur_value > 0:
139
+ image = cv2.GaussianBlur(image, (blur_value, blur_value), blur_value - 1)
140
+
141
+ # Process image cv32f ==> cv32f
142
+ img_final = pipeline_svg(image, size_value=size_value, level=1, threshold=threshold, kernel_type=kernel_type,
143
+ dilate_lines_value=dilate_lines_value)
144
+
145
+ col2.image(img_final, caption="Image corrigée", use_column_width='auto')
146
+
147
+ # Check for grayscale
148
+ tolerance = 10
149
+ ratio_of_gray_pixels = int(np.sum((tolerance < image) * (image < 255 - tolerance)) / np.size(image) * 100)
150
+ if ratio_of_gray_pixels > 1:
151
+ col3.warning(f":warning: Le nombre de pixels gris est élevé: {ratio_of_gray_pixels} % > 1%")
152
+
153
+ # Check reconstruction fidelity
154
+ distance = np.mean((np.array(image) - img_final) ** 2)
155
+ if distance > 10:
156
+ col3.warning(
157
+ f":warning: Le logo est peut-être trop dégradé (MSE={distance:.2f} > 10).\nVérifier visuellement.")
158
+
159
+ dim = (width, height)
160
+ # resize image
161
+ resized_img_final = cv2.resize(img_final, dim, interpolation=cv2.INTER_AREA)
162
+ resized_image_input = cv2.resize(image_input, dim, interpolation=cv2.INTER_AREA)
163
+
164
+ buf = BytesIO()
165
+ # img_stacked = np.hstack((resized_image_input, resized_img_final))
166
+ img_final = Image.fromarray(resized_image_input).convert("L")
167
+ img_final.save(buf, format="PNG")
168
+ byte_im = buf.getvalue()
169
+
170
+ btn = col3.download_button(
171
+ label=":inbox_tray: Télécharger l'image",
172
+ data=byte_im,
173
+ file_name=f"corrected_{uploaded_file.name}",
174
+ mime="image/png"
175
+ )
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ EXPOSE 7860
4
+
5
+ RUN apt-get update && apt-get install -y \
6
+ build-essential \
7
+ software-properties-common \
8
+ git \
9
+ libcairo2 \
10
+ libcairo2-dev \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ WORKDIR /moulinette
14
+ COPY . .
15
+ RUN echo $(ls -1 .. )
16
+
17
+ RUN pip3 install -r requirements.txt
18
+
19
+ ENTRYPOINT ["streamlit", "run", "Corriger.py", "--server.port=7860"]
README.md CHANGED
@@ -1,11 +1,11 @@
1
  ---
2
- title: Moulinette Streamlit
3
- emoji: 🌖
4
  colorFrom: gray
5
  colorTo: blue
6
  sdk: streamlit
7
  sdk_version: 1.19.0
8
- app_file: app.py
9
  pinned: false
10
  ---
11
 
 
1
  ---
2
+ title: Moulinette
3
+ emoji: ⚙️
4
  colorFrom: gray
5
  colorTo: blue
6
  sdk: streamlit
7
  sdk_version: 1.19.0
8
+ app_file: Corriger.py
9
  pinned: false
10
  ---
11
 
code/__init__.py ADDED
File without changes
code/__pycache__/__init__.cpython-39.pyc ADDED
Binary file (135 Bytes). View file
 
code/__pycache__/functions.cpython-39.pyc ADDED
Binary file (3.92 kB). View file
 
code/functions.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ import os
5
+ import cairosvg
6
+ from potrace import POTRACE_CORNER, Path, Bitmap
7
+ import io
8
+ from PIL import Image, ImageStat
9
+
10
+ import streamlit
11
+ from PIL import Image
12
+
13
+ @streamlit.cache_data
14
+ def pipeline_svg(image_input, size_value, level=3, streamlit=False, threshold=0, kernel_type=cv2.MORPH_ELLIPSE, dilate_lines_value=0):
15
+ """
16
+ uint8 ==> uint8
17
+
18
+ Args:
19
+ streamlit:
20
+ size_value:
21
+ image_input:
22
+
23
+ Returns:
24
+
25
+ """
26
+
27
+ # Process image
28
+ image_processed = process_svg(image_input, size_value=size_value, streamlit=streamlit, kernel_type=kernel_type, dilate_lines_value=dilate_lines_value)
29
+
30
+ return image_processed
31
+
32
+ def process_svg(img, size_value=12, level=1, streamlit=False, kernel_type=cv2.MORPH_ELLIPSE, dilate_lines_value=0):
33
+
34
+ image_path = "input_image.png"
35
+ img = img.astype('uint8')
36
+
37
+ # Lines very small
38
+ if dilate_lines_value > 0:
39
+ size = dilate_lines_value + 1 # No sens to dilate by one pixel (doesn't do anything).
40
+ kernel = get_kernel_ellipse(size, kernel_type=kernel_type)
41
+ img = cv2.erode(img, kernel, iterations=1)
42
+
43
+ #cv2.imwrite(image_path, img)
44
+ img_array = convert_to_svg_and_back(img)
45
+
46
+ img_array = binarise(img_array)
47
+ img_bin = (255 - img_array)
48
+ img_bin = img_bin.astype('uint8')
49
+ image_already_added = np.zeros_like(img_bin)
50
+
51
+ target_min_size = max(1, size_value)
52
+
53
+ image_final = img_bin
54
+ for i in range(target_min_size+1):
55
+ size = 2 * i + 1
56
+ kernel = get_kernel_ellipse(size, kernel_type=kernel_type)
57
+
58
+ erosion = cv2.erode((img_bin - image_already_added), kernel, iterations=1)
59
+ dilation = cv2.dilate(erosion, kernel, iterations=1)
60
+
61
+ image_petits_objets = (img_bin - dilation)
62
+ image_petits_objets = remove_solo_pixels(image_petits_objets, kernel_size=3)
63
+
64
+ size = 2 * (target_min_size - i) + 1
65
+ kernel = get_kernel_ellipse(size, kernel_type=kernel_type)
66
+ dilate_image_petits_objets = cv2.dilate(image_petits_objets, kernel, iterations=1)
67
+
68
+ image_already_added = (image_already_added + image_petits_objets)
69
+
70
+ if i > level:
71
+ image_final = (image_final + dilate_image_petits_objets)
72
+
73
+ #cv2.imwrite("image_finale.png", (255 - image_final))
74
+ image = convert_to_svg_and_back((255 - image_final))
75
+ return image
76
+ def get_kernel_ellipse(size, kernel_type=cv2.MORPH_ELLIPSE):
77
+ list_coords = [size, size]
78
+ return cv2.getStructuringElement(kernel_type, (list_coords[0], list_coords[1]),
79
+ (int((list_coords[0] - 1) / 2), int((list_coords[1] - 1) / 2)))
80
+
81
+
82
+ def binarise(img):
83
+ img = img > 200
84
+ img = img * 255
85
+ img = img.astype('uint8')
86
+ return img
87
+
88
+
89
+ def imshow(title, image, vmin=0, vmax=1):
90
+ plt.figure()
91
+ plt.title(title)
92
+ plt.imshow(image * 255, vmin=vmin * 255, vmax=vmax * 255, cmap='gray')
93
+
94
+
95
+ def remove_solo_pixels(image, kernel_size=3):
96
+ kernel = get_kernel_ellipse(kernel_size)
97
+
98
+ erosion = cv2.erode(image, kernel, iterations=1)
99
+ dilation = cv2.dilate(erosion, kernel, iterations=1)
100
+
101
+ dilation = dilation.astype('uint8')
102
+ return dilation
103
+
104
+ def convert_to_svg_and_back(image_array) -> np.array:
105
+ image_pil = Image.fromarray(image_array)
106
+
107
+ bm = Bitmap(image_pil, blacklevel=0.5)
108
+
109
+ plist = bm.trace(
110
+ turdsize=2,
111
+ turnpolicy=4,
112
+ alphamax=1,
113
+ opticurve= False,
114
+ opttolerance=0.2)
115
+
116
+ image = backend_svg_no_file(image_pil, plist)
117
+
118
+ return np.array(image)
119
+
120
+ def backend_svg_no_file(image, path: Path):
121
+ output = f'<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{image.width}" height="{image.height}" viewBox="0 0 {image.width} {image.height}">'
122
+
123
+ parts = []
124
+ for curve in path:
125
+ fs = curve.start_point
126
+ parts.append("M%f,%f" % (fs.x, fs.y))
127
+ for segment in curve.segments:
128
+ if segment.is_corner:
129
+ a = segment.c
130
+ parts.append("L%f,%f" % (a.x, a.y))
131
+ b = segment.end_point
132
+ parts.append("L%f,%f" % (b.x, b.y))
133
+ else:
134
+ a = segment.c1
135
+ b = segment.c2
136
+ c = segment.end_point
137
+ parts.append("C%f,%f %f,%f %f,%f" % (a.x, a.y, b.x, b.y, c.x, c.y))
138
+ parts.append("z")
139
+ output += f'<path stroke="none" fill="#000000" fill-rule="evenodd" d="{"".join(parts)}"/>'
140
+
141
+ output += "</svg>"
142
+ # From svg to png (bytes)
143
+ image_data = cairosvg.surface.PNGSurface.convert(output)
144
+ image = Image.open(io.BytesIO(image_data)).split()[-1]
145
+ return image
packages.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ libcairo2
2
+ libcairo2-dev
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ matplotlib==3.4.1
2
+ numpy==1.20.2
3
+ opencv_python_headless==4.5.5.64
4
+ Pillow==9.4.0
5
+ scipy==1.6.2
6
+ streamlit==1.20.0
7
+ potracer==0.0.4
8
+ cairosvg==2.7.0
9
+ streamlit-authenticator==0.2.1
seguinmoreau.png ADDED
users.yaml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ credentials:
2
+ usernames:
3
+ hdeferrieres:
4
5
+ name: Hugues de Ferrieres
6
+ password: $2b$12$mm/Y4S.x.50vtHVDTqEgSO51ne4JpQEVM20Ao8uJXFffje/4xgkr. # To be replaced with hashed password
7
+ pdeferieres:
8
9
+ name: Paul de Ferrieres
10
+ password: $2b$12$mm/Y4S.x.50vtHVDTqEgSO51ne4JpQEVM20Ao8uJXFffje/4xgkr. # To be replaced with hashed password
11
+ seguinmoreau:
12
13
+ name: Seguin Moreau
14
+ password: $2b$12$mm/Y4S.x.50vtHVDTqEgSO51ne4JpQEVM20Ao8uJXFffje/4xgkr.
15
+ cookie:
16
+ expiry_days: 30
17
+ key: random_signature_key # Must be string
18
+ name: random_cookie_name
19
+ preauthorized:
20
+ emails:
21