Skip to content

Commit 4b7d2ed

Browse files
committed
Globally redesigned user interface, abandonment of gr.ImageEditor. Improved logic for working with gr.Examples. Added preliminary compression of large images.
1 parent 2c29734 commit 4b7d2ed

File tree

10 files changed

+377
-281
lines changed

10 files changed

+377
-281
lines changed
187 KB
Binary file not shown.

particleanalyzer/core/ImagePreprocessor.py

Lines changed: 8 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import pandas as pd
33
import cv2
44
import numpy as np
5-
import math
65
from tqdm import tqdm
76
import gradio as gr
87
from datetime import datetime
@@ -32,6 +31,7 @@ def _get_translation(self, text):
3231
def preprocess_image(
3332
self,
3433
image: np.ndarray,
34+
scale: float,
3535
scale_selector: dict,
3636
solution: str,
3737
request: gr.Request,
@@ -45,16 +45,13 @@ def preprocess_image(
4545
pbar.set_description(self._get_translation("Загрузка изображения..."))
4646
pr(0.25, desc=self._get_translation("Загрузка изображения..."))
4747
try:
48-
# Обработка шкалы прибора
49-
scale = None
50-
if scale_selector["scale"]:
51-
scale = self._determine_pixel_scale(image)
52-
if scale is None:
53-
return None, None, None, None
54-
image = image["background"]
55-
image = self._convert_image_channels(image)
56-
else:
57-
image = np.array(image)
48+
if scale_selector["scale"] and scale is None:
49+
gr.Info(
50+
self._get_translation(
51+
"Обозначьте на изображении масштабную шкалу при помощи двух точек."
52+
)
53+
)
54+
return None, None, None, None, None
5855

5956
# Сохранение метаданных
6057
self._save_image_metadata(image, request)
@@ -72,14 +69,6 @@ def preprocess_image(
7269
print(f"Ошибка при обработке изображения: {e}")
7370
return None, None, None, None, None
7471

75-
def _convert_image_channels(self, image: np.ndarray) -> np.ndarray:
76-
"""Конвертирует изображение в RGB при необходимости."""
77-
if image.shape[-1] == 4: # RGBA → RGB
78-
return cv2.cvtColor(image, cv2.COLOR_RGBA2RGB)
79-
elif image.shape[-1] == 1: # Grayscale → RGB
80-
return cv2.cvtColor(image, cv2.COLOR_GRAY2RGB)
81-
return image
82-
8372
@staticmethod
8473
def resize_image(
8574
image: np.ndarray,
@@ -147,81 +136,3 @@ def _save_image_metadata(self, image: np.ndarray, request: gr.Request) -> None:
147136
except Exception:
148137
pass
149138
# print(f"Ошибка при сохранении метаданных: {e}")
150-
151-
def get_error_response(self) -> tuple:
152-
"""Возвращает кортеж с None для обработки ошибок."""
153-
return (None,) * 13
154-
155-
def _determine_pixel_scale(self, data):
156-
"""Определяет размер пикселя в реальных единицах (μm/px)"""
157-
layers = data.get("layers", [])
158-
if layers:
159-
layer = layers[0]
160-
if layer.shape[-1] == 4:
161-
layer = cv2.cvtColor(layer, cv2.COLOR_RGBA2RGB)
162-
163-
# Создаем маски для разных цветов (зеленый, красный, синий, желтый)
164-
lower_green = np.array([0, 128, 0])
165-
upper_green = np.array([100, 255, 100])
166-
167-
lower_red = np.array([0, 0, 128])
168-
upper_red = np.array([100, 100, 255])
169-
170-
lower_blue = np.array([0, 0, 128])
171-
upper_blue = np.array([100, 100, 255])
172-
173-
lower_yellow = np.array([128, 128, 0])
174-
upper_yellow = np.array([255, 255, 100])
175-
176-
# Создаем маску для всех цветов
177-
mask_green = cv2.inRange(layer, lower_green, upper_green)
178-
mask_red = cv2.inRange(layer, lower_red, upper_red)
179-
mask_blue = cv2.inRange(layer, lower_blue, upper_blue)
180-
mask_yellow = cv2.inRange(layer, lower_yellow, upper_yellow)
181-
182-
# Объединяем все маски
183-
mask = cv2.bitwise_or(
184-
mask_green,
185-
cv2.bitwise_or(mask_red, cv2.bitwise_or(mask_blue, mask_yellow)),
186-
)
187-
188-
# Находим контуры на маске
189-
contours, _ = cv2.findContours(
190-
mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
191-
)
192-
193-
if len(contours) == 2:
194-
# Рассчитываем центроиды для первых двух контуров
195-
moments1 = cv2.moments(contours[0])
196-
moments2 = cv2.moments(contours[1])
197-
198-
if moments1["m00"] != 0 and moments2["m00"] != 0:
199-
cx1 = int(moments1["m10"] / moments1["m00"])
200-
cy1 = int(moments1["m01"] / moments1["m00"])
201-
cx2 = int(moments2["m10"] / moments2["m00"])
202-
cy2 = int(moments2["m01"] / moments2["m00"])
203-
204-
# Рассчитываем расстояние между центрами масс двух контуров
205-
distance = math.sqrt((cx2 - cx1) ** 2 + (cy2 - cy1) ** 2)
206-
else:
207-
gr.Info(self._get_translation("Ошибка в вычислении центроида."))
208-
elif len(contours) > 2:
209-
distance = False
210-
gr.Info(self._get_translation("Найдено больше двух точек."))
211-
else:
212-
distance = False
213-
gr.Info(
214-
self._get_translation(
215-
"Обозначьте на изображении масштабную шкалу при помощи двух точек."
216-
)
217-
)
218-
# Возвращаем расстояние
219-
return distance
220-
else:
221-
# Если слоев нет, возвращаем False
222-
gr.Info(
223-
self._get_translation(
224-
"Обозначьте на изображении масштабную шкалу при помощи двух точек."
225-
)
226-
)
227-
return False

particleanalyzer/core/ParticleAnalyzer.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@
2727
from particleanalyzer.core.StatisticsBuilder import StatisticsBuilder
2828
from particleanalyzer.core.languages import translations
2929
from particleanalyzer.core.language_context import LanguageContext
30+
from particleanalyzer.core.PointManager import PointManager
3031

3132
lang = "en"
3233

34+
point_manager = PointManager()
35+
3336

3437
class ParticleAnalyzer:
3538
SCALE_OPTIONS = {
@@ -133,12 +136,14 @@ def _create_error_return(self) -> Tuple:
133136
None,
134137
None,
135138
gr.update(visible=False),
139+
gr.update(visible=False),
136140
)
137141

138142
def analyze_image(
139143
self,
140-
image: np.ndarray,
141144
image2: Optional[np.ndarray],
145+
scale: float,
146+
points_scale: tuple,
142147
scale_input: float,
143148
confidence_threshold: float,
144149
scale_selector: str,
@@ -178,24 +183,16 @@ def analyze_image(
178183
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}{postfix}]",
179184
)
180185
pr(0, desc=self._get_translation("Подготовка..."))
181-
selected_image = image if scale_selector["scale"] else image2
182-
if scale_selector["scale"]:
183-
if (
184-
image.get("background") is None
185-
and image.get("composite") is None
186-
and not image.get("layers")
187-
):
188-
gr.Warning(
189-
self._get_translation("Ошибка: изображение отсутствует...")
190-
)
191-
return self._create_error_return()
192-
elif image2 is None:
186+
selected_image = image2
187+
188+
if selected_image is None:
193189
gr.Warning(self._get_translation("Ошибка: изображение отсутствует..."))
194190
return self._create_error_return()
195191

196192
image, orig_image, gray_image, scale, scale_factor_glob = (
197193
self.preprocessor.preprocess_image(
198194
image=selected_image,
195+
scale=scale,
199196
scale_selector=scale_selector,
200197
solution=solution,
201198
request=request,
@@ -244,6 +241,11 @@ def analyze_image(
244241
return self._create_error_return()
245242

246243
output_image = cv2.cvtColor(output_image, cv2.COLOR_BGR2RGB)
244+
if scale_selector["scale"]:
245+
output_image = point_manager.draw_scale_on_image(
246+
output_image, scale_factor_glob, scale, *points_scale
247+
)
248+
247249
df = pd.DataFrame(particle_data)
248250
points_df = pd.DataFrame(df["points"])
249251
df = df.drop(columns=["points"])
@@ -322,6 +324,7 @@ def analyze_image(
322324
value=(limits["I_min"], limits["I_max"]),
323325
),
324326
gr.update(visible=True),
327+
gr.update(visible=True),
325328
)
326329
except Exception as e:
327330
self._handle_error(e)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import numpy as np
2+
import gradio as gr
3+
from PIL import Image, ImageDraw, ImageFont
4+
from particleanalyzer.core.languages import translations
5+
from particleanalyzer.core.language_context import LanguageContext
6+
7+
8+
class PointManager:
9+
def __init__(self, lang="ru"):
10+
self.points = []
11+
self.lang = lang
12+
13+
def _get_translation(self, text):
14+
return translations.get(self.lang, {}).get(text, text)
15+
16+
def add_point(self, point):
17+
if len(self.points) >= 2:
18+
self.points = [point]
19+
else:
20+
self.points.append(point)
21+
return self.points
22+
23+
def clear_points(self):
24+
self.points = []
25+
return self.points
26+
27+
def handle_select(self, evt: gr.SelectData):
28+
self.lang = LanguageContext.get_language()
29+
self.add_point(evt.index)
30+
if len(self.points) == 2:
31+
point1, point2 = self.points[0], self.points[1]
32+
distance = np.sqrt(
33+
(point2[0] - point1[0]) ** 2 + (point2[1] - point1[1]) ** 2
34+
)
35+
return (
36+
f"{self._get_translation('Расстояние равно')}: {distance:.0f} {self._get_translation('пикселей')}",
37+
distance,
38+
(point1, point2),
39+
)
40+
else:
41+
return (
42+
f"{self._get_translation('Выбрано точек')}: {len(self.points)}/2",
43+
None,
44+
None,
45+
)
46+
47+
def draw_scale_on_image(self, image, scale_factor, distance, point1, point2):
48+
pil_image = Image.fromarray(image)
49+
draw = ImageDraw.Draw(pil_image)
50+
51+
scaled_line_width = max(1, int(3))
52+
scaled_font_size = max(10, int(20))
53+
scaled_circle_radius = max(2, int(5))
54+
scaled_padding = max(2, int(5))
55+
56+
LINE_COLOR = (255, 0, 0)
57+
TEXT_COLOR = (255, 255, 255)
58+
BG_COLOR = (0, 0, 0)
59+
60+
try:
61+
font = ImageFont.truetype("arial.ttf", scaled_font_size)
62+
except IOError:
63+
font = ImageFont.load_default()
64+
65+
scaled_point1 = (int(point1[0] / scale_factor), int(point1[1] / scale_factor))
66+
scaled_point2 = (int(point2[0] / scale_factor), int(point2[1] / scale_factor))
67+
68+
draw.line(
69+
[scaled_point1, scaled_point2], fill=LINE_COLOR, width=scaled_line_width
70+
)
71+
72+
def draw_circle(center, radius):
73+
x, y = center
74+
draw.ellipse(
75+
[x - radius, y - radius, x + radius, y + radius], fill=LINE_COLOR
76+
)
77+
78+
draw_circle(scaled_point1, scaled_circle_radius)
79+
draw_circle(scaled_point2, scaled_circle_radius)
80+
81+
mid_x = (scaled_point1[0] + scaled_point2[0]) // 2
82+
mid_y = (scaled_point1[1] + scaled_point2[1]) // 2
83+
84+
text = f"{distance:.0f} px"
85+
bbox = draw.textbbox((0, 0), text, font=font)
86+
text_width, text_height = bbox[2] - bbox[0], bbox[3] - bbox[1]
87+
88+
bg_x1 = mid_x - text_width // 2 - scaled_padding
89+
bg_y1 = mid_y - text_height // 2 - scaled_padding
90+
bg_x2 = mid_x + text_width // 2 + scaled_padding
91+
bg_y2 = mid_y + text_height // 2 + scaled_padding
92+
93+
draw.rectangle([bg_x1, bg_y1, bg_x2, bg_y2], fill=BG_COLOR)
94+
draw.text(
95+
(bg_x1 + scaled_padding, bg_y1 + scaled_padding),
96+
text,
97+
fill=TEXT_COLOR,
98+
font=font,
99+
)
100+
101+
return np.array(pil_image)

particleanalyzer/core/languages.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"Достигнут предел количества обнаружений. Увеличьте максимальное количество обнаружений в настройках.": "Достигнут предел количества обнаружений. Увеличьте максимальное количество обнаружений в настройках.",
2727
"Обработка частиц...": "Обработка частиц...",
2828
"мкм": "мкм",
29+
"нм": "нм",
2930
"пикс": "пикс",
3031
"ед.": "ед.",
3132
"Диаметр": "Диаметр",
@@ -140,6 +141,11 @@
140141
"Удалить частицы": "Удалить частицы",
141142
"Сбросить таблицу": "Сбросить таблицу",
142143
"Загрузить изображение СЭМ": "Загрузить изображение СЭМ",
144+
"Статус калибровки масштаба": "Статус калибровки масштаба",
145+
"Выберите две крайние точки на шкале": "Выберите две крайние точки на шкале",
146+
"Расстояние равно": "Расстояние равно",
147+
"пикселей": "пикселей",
148+
"Выбрано точек": "Выбрано точек",
143149
},
144150
"en": {
145151
"Подготовка...": "Initializing...",
@@ -155,6 +161,7 @@
155161
"Обработка частиц...": "Processing particles...",
156162
"ед.": "units",
157163
"мкм": "µm",
164+
"нм": "nm",
158165
"пикс": "px",
159166
"Диаметр": "Diameter",
160167
"Средний диаметр": "Average diameter",
@@ -268,6 +275,11 @@
268275
"Удалить частицы": "Remove particles",
269276
"Сбросить таблицу": "Reset table",
270277
"Загрузить изображение СЭМ": "Upload SEM image",
278+
"Статус калибровки масштаба": "Scale calibration status",
279+
"Выберите две крайние точки на шкале": "Select the two extreme points of the scale.",
280+
"Расстояние равно": "Distance is",
281+
"пикселей": "pixels",
282+
"Выбрано точек": "Points selected",
271283
},
272284
"zh-cn": {
273285
"Подготовка...": "初始化中...",
@@ -282,6 +294,7 @@
282294
"Достигнут предел количества обнаружений. Увеличьте максимальное количество обнаружений в настройках.": "达到检测数量上限。请在设置中增加最大检测数量",
283295
"Обработка частиц...": "正在处理粒子...",
284296
"мкм": "微米",
297+
"нм": "纳米",
285298
"пикс": "像素",
286299
"ед.": "单位",
287300
"Диаметр": "直径",
@@ -396,6 +409,11 @@
396409
"Удалить частицы": "去除颗粒",
397410
"Сбросить таблицу": "重置表",
398411
"Загрузить изображение СЭМ": "上传扫描电镜图像",
412+
"Статус калибровки масштаба": "秤校准状态",
413+
"Выберите две крайние точки на шкале": "选择尺度的两个极值点。",
414+
"Расстояние равно": "距离",
415+
"пикселей": "像素",
416+
"Выбрано точек": "已选点",
399417
},
400418
"zh-tw": {
401419
"Подготовка...": "初始化中...",
@@ -410,6 +428,7 @@
410428
"Достигнут предел количества обнаружений. Увеличьте максимальное количество обнаружений в настройках.": "達到檢測數量上限。請在設定中增加最大檢測數量",
411429
"Обработка частиц...": "正在處理粒子...",
412430
"мкм": "微米",
431+
"нм": "奈米",
413432
"пикс": "像素",
414433
"ед.": "單位",
415434
"Диаметр": "直徑",
@@ -531,6 +550,11 @@
531550
"Удалить частицы": "去除顆粒",
532551
"Сбросить таблицу": "重設表",
533552
"Загрузить изображение СЭМ": "上傳掃描電子顯微鏡影像",
553+
"Статус калибровки масштаба": "秤校準狀態",
554+
"Выберите две крайние точки на шкале": "選擇尺度的兩個極值點。",
555+
"Расстояние равно": "距離",
556+
"пикселей": "像素",
557+
"Выбрано точек": "已選點",
534558
},
535559
}
536560

0 commit comments

Comments
 (0)