Skip to content

Commit 6cbf7d5

Browse files
committed
Added the ability to visualize both the elongation and widening of particles
1 parent d5a9da3 commit 6cbf7d5

File tree

4 files changed

+112
-52
lines changed

4 files changed

+112
-52
lines changed

particleanalyzer/core/StatisticsBuilder.py

Lines changed: 90 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ def build_distribution_fig(self, image):
6868
if self.df.empty:
6969
return None
7070
row_heights = [0.14, 0.14, 0.14, 0.14, 0.14, 0.30]
71-
# Создаем 3 строки и 2 колонки (последний график будет один в третьей строке)
7271
fig = make_subplots(
7372
rows=6, cols=2, row_heights=row_heights,
7473
subplot_titles=(
@@ -126,13 +125,36 @@ def build_distribution_fig(self, image):
126125

127126
for row, col, col_name, label, color in params:
128127
self._add_distribution(fig, row, col, self.df[col_name], label, color)
128+
fig.update_traces(
129+
showlegend=False,
130+
)
129131

130132
if 'centroid_x' in self.df.columns and 'centroid_y' in self.df.columns:
131133
self._add_vector_field(fig, self.df, row=6, col=1, image=image)
134+
135+
# Включаем легенду только для векторного поля
136+
fig.update_traces(
137+
showlegend=True,
138+
selector=dict(row=6, col=1)
139+
)
140+
# Настраиваем позицию легенды только для этого subplot
141+
# Настраиваем позицию легенды только для этого subplot
142+
fig.update_layout(
143+
legend=dict(
144+
itemsizing='constant',
145+
itemclick='toggle',
146+
itemdoubleclick=False,
147+
orientation="h", # Горизонтальная ориентация
148+
yanchor="top", # Якорь по верхнему краю
149+
y=-0.025, # Позиция ниже графика (отрицательное значение)
150+
xanchor="center", # Центрирование по горизонтали
151+
x=0.475, # Центр по оси X
152+
font=dict(size=16)
153+
)
154+
)
132155

133156
fig.update_layout(
134157
height=1600,
135-
showlegend=False,
136158
plot_bgcolor="white",
137159
margin=dict(l=50, r=50, b=50, t=50),
138160
modebar={
@@ -190,8 +212,6 @@ def _add_distribution(self, fig, row, col, data, title, color):
190212
showgrid=False
191213
)
192214

193-
194-
195215
def _add_vector_field(self, fig, df, row, col, image):
196216
image = np.flipud(image)
197217
image_pil = Image.fromarray(image.astype(np.uint8))
@@ -213,52 +233,84 @@ def _add_vector_field(self, fig, df, row, col, image):
213233
row=row,
214234
col=col
215235
)
216-
217-
if not {'centroid_x', 'centroid_y'}.issubset(df.columns):
236+
237+
required_columns = {'centroid_x', 'centroid_y'}
238+
if not required_columns.issubset(df.columns):
218239
return
219240

220-
angle_column = self._get_translation("θₘₐₓ [°]")
221-
if angle_column not in df.columns:
241+
angle_max_col = self._get_translation("θₘₐₓ [°]")
242+
angle_min_col = self._get_translation("θₘᵢₙ [°]")
243+
244+
if not all(col in df.columns for col in [angle_max_col, angle_min_col]):
222245
return
223246

224247
if self.scale_selector == self._get_translation('Instrument scale in µm'):
225-
diameter_col = self._get_translation("Dₘₐₓ [мкм]")
248+
diameter_max_col = self._get_translation("Dₘₐₓ [мкм]")
249+
diameter_min_col = self._get_translation("Dₘᵢₙ [мкм]")
226250
else:
227-
diameter_col = self._get_translation("Dₘₐₓ [пикс]")
251+
diameter_max_col = self._get_translation("Dₘₐₓ [пикс]")
252+
diameter_min_col = self._get_translation("Dₘᵢₙ [пикс]")
228253

229-
if diameter_col not in df.columns:
254+
if not all(col in df.columns for col in [diameter_max_col, diameter_min_col]):
230255
return
231256

232257
x = df['centroid_x'].values
233258
y = df['centroid_y'].values
234-
theta_deg = df[angle_column].values
235-
theta_rad = np.deg2rad(theta_deg)
236-
diameters = df[diameter_col].values
237-
238-
min_length = 20 # минимальная длина стрелки
239-
max_length = 60 # максимальная длина стрелки
240-
if len(diameters) > 1:
241-
normalized_lengths = min_length + (max_length - min_length) * (diameters - min(diameters)) / (max(diameters) - min(diameters))
259+
260+
theta_max_deg = df[angle_max_col].values
261+
theta_max_rad = np.deg2rad(theta_max_deg)
262+
diameters_max = df[diameter_max_col].values
263+
264+
min_length = 20
265+
max_length = 60
266+
if len(diameters_max) > 1:
267+
lengths_max = min_length + (max_length - min_length) * (diameters_max - min(diameters_max)) / (max(diameters_max) - min(diameters_max))
242268
else:
243-
normalized_lengths = [min_length]
244-
245-
u = np.cos(theta_rad) * normalized_lengths
246-
v = np.sin(theta_rad) * normalized_lengths
247-
248-
quiver_fig = ff.create_quiver(
249-
x, y, u, v,
250-
scale=1,
251-
arrow_scale=0.3,
252-
line=dict(
253-
width=2,
254-
color='yellow',
255-
shape='spline'
256-
)
257-
)
258-
259-
for trace in quiver_fig.data:
269+
lengths_max = [min_length]
270+
271+
u_max = np.cos(theta_max_rad) * lengths_max
272+
v_max = np.sin(theta_max_rad) * lengths_max
273+
274+
theta_min_deg = df[angle_min_col].values
275+
theta_min_rad = np.deg2rad(theta_min_deg)
276+
diameters_min = df[diameter_min_col].values
277+
278+
if len(diameters_min) > 1:
279+
lengths_min = min_length + (max_length - min_length) * (diameters_min - min(diameters_min)) / (max(diameters_min) - min(diameters_min))
280+
else:
281+
lengths_min = [min_length]
282+
283+
u_min = np.cos(theta_min_rad) * lengths_min
284+
v_min = np.sin(theta_min_rad) * lengths_min
285+
286+
quiver_max = ff.create_quiver(
287+
x, y, u_max, v_max,
288+
scale=1,
289+
arrow_scale=0.3,
290+
line=dict(
291+
width=2,
292+
color='yellow',
293+
shape='spline'
294+
),
295+
name=self._get_translation('Удлинение')
296+
)
297+
298+
quiver_min = ff.create_quiver(
299+
x, y, u_min, v_min,
300+
scale=1,
301+
arrow_scale=0.25,
302+
line=dict(
303+
width=1.5,
304+
color='cyan',
305+
shape='spline'
306+
),
307+
name=self._get_translation('Утолщение'),
308+
visible='legendonly'
309+
)
310+
311+
for trace in quiver_max.data + quiver_min.data:
260312
fig.add_trace(trace, row=row, col=col)
261-
313+
262314
fig.update_xaxes(
263315
title_text=self._get_translation("X"),
264316
row=row,
@@ -267,7 +319,7 @@ def _add_vector_field(self, fig, df, row, col, image):
267319
constrain='domain',
268320
showgrid=False
269321
)
270-
322+
271323
fig.update_yaxes(
272324
title_text=self._get_translation("Y"),
273325
row=row,

particleanalyzer/core/languages.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@
111111
'Максимальный угол [°]': 'Maximum angle [°]',
112112
'Минимальный угол [°]': 'Minimum angle [°]',
113113
'Векторное поле ориентации': 'Orientation Vector Field',
114+
'Удлинение': 'Elongation',
115+
'Утолщение': 'Thickening',
114116
},
115117
'ru': {
116118
'Подготовка...': 'Подготовка...',
@@ -222,6 +224,8 @@
222224
'Максимальный угол [°]': 'Максимальный угол [°]',
223225
'Минимальный угол [°]': 'Минимальный угол [°]',
224226
'Векторное поле ориентации': 'Векторное поле ориентации',
227+
'Удлинение': 'Удлинение',
228+
'Утолщение': 'Утолщение',
225229
},
226230
'zh-cn': {
227231
'Подготовка...': '初始化中...',
@@ -333,6 +337,8 @@
333337
'Максимальный угол [°]': '最大角度[°]',
334338
'Минимальный угол [°]': '最小角度[°]',
335339
'Векторное поле ориентации': '取向矢量场',
340+
'Удлинение': '伸长',
341+
'Утолщение': '加厚',
336342
},
337343
'zh-tw': {
338344
'Подготовка...': '初始化中...',
@@ -444,6 +450,8 @@
444450
'Максимальный угол [°]': '最大角度[°]',
445451
'Минимальный угол [°]': '最小角度[°]',
446452
'Векторное поле ориентации': '取向向量場',
453+
'Удлинение': '伸長',
454+
'Утолщение': '加厚',
447455
}
448456
}
449457

particleanalyzer/core/ui.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def create_interface():
3232

3333
with demo:
3434
with gr.Column(elem_id="app-container"):
35-
gr.Markdown("# 🔎 ParticleAnalyzer v0.1.21")
35+
gr.Markdown("# 🔎 ParticleAnalyzer v0.1.22")
3636
gr.Markdown(i18n("При помощи данного инструмента можно анализировать размерные характеристики частиц на изображениях SEM.<br>В случае проблем с сегментацией изображения или возникновения ошибок, пожалуйста, направляйте материалы на электронную почту: [email protected]"))
3737
mode_state = gr.State(value=i18n("Тёмный режим"))
3838
with gr.Tabs():
@@ -145,7 +145,6 @@ def create_interface():
145145
step=0.01,
146146
label=i18n("Точность обнаружения (порог уверенности)")
147147
)
148-
with gr.Row():
149148
# Слайдер для iou
150149
confidence_iou = gr.Slider(
151150
minimum=0.0,
@@ -190,17 +189,18 @@ def create_interface():
190189
# Выпадающий список для округления
191190
round_value = gr.Dropdown([0, 1, 2, 3, 4, 5, 6], value=2, label=i18n("Округлять результаты до"))
192191
with gr.Row():
193-
# Слайдер для number_of_bins
194-
number_of_bins = gr.Slider(
195-
minimum=0.0,
196-
maximum=100,
197-
value=20,
198-
step=1,
199-
label=i18n("Количество интервалов на гистограмме")
200-
)
201-
with gr.Row():
202-
# Слайдер для number_of_bins
203-
show_Feret_diametr = gr.Checkbox(label=i18n("Включить"), info=i18n("Включить отображение диаметров Ферета?"))
192+
with gr.Column(scale=1):
193+
# Слайдер для number_of_bins
194+
number_of_bins = gr.Slider(
195+
minimum=0.0,
196+
maximum=100,
197+
value=20,
198+
step=1,
199+
label=i18n("Количество интервалов на гистограмме")
200+
)
201+
with gr.Column(scale=1):
202+
# Слайдер для number_of_bins
203+
show_Feret_diametr = gr.Checkbox(label=i18n("Включить"), info=i18n("Включить отображение диаметров Ферета?"))
204204

205205
# Основная функция обработки изображений при нажатии кнопки
206206
process_button.click(

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def read_file(filename):
88

99
setup(
1010
name="ParticleAnalyzer",
11-
version="0.1.21",
11+
version="0.1.22",
1212
packages=find_packages(exclude=['tests*']),
1313
package_data={
1414
'particleanalyzer': [

0 commit comments

Comments
 (0)