Files
AIM-PIbd-31-Yaruskin-S-A/lab_9/laba9.ipynb
2025-05-24 00:35:06 +04:00

5.1 MiB
Raw Blame History

Лаб 9. Обработка изображений
В данной работе будем использовать библиотек mahotas.
Я буду использовать библиотку OpenCV, потому что она написана на C++, а это круто

Загружаем изображения

In [4]:
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt

# Функция для загрузки изображений из папки на OpenCV
def load_images(folder, target_size=(512, 512), allowed_extensions=("jpg", "jpeg", "png", "bmp", "gif")):
    images = []
    filenames = []

    if not os.path.exists(folder):
        raise FileNotFoundError(f"Папка {folder} не найдена.")
    
    print(f"Проверка файлов в папке {folder}...")

    for filename in sorted(os.listdir(folder)):
        print(f"Проверяется файл: {filename}")
        
        if filename.lower().endswith(allowed_extensions):
            image_path = os.path.join(folder, filename)
            print(f"Загружаем изображение: {image_path}")
            
            try:
                image = cv2.imread(image_path)
                if image is None:
                    print(f"Ошибка загрузки: {filename}")
                    continue
                
                image_resized = cv2.resize(image, target_size)

                images.append(image_resized)
                
                filenames.append(filename)
            except Exception as e:
                print(f"Ошибка при загрузке и обработке изображения {filename}: {e}")

    return images, filenames
                               

# Сохранение файла
# mh.imsave()

# Указываем путь к папке с изображениями
folder_path = "../static/images"
images, filenames = load_images(folder_path)

# Проверяем, были ли загружены изображения
if not images:
    print("Нет изображений для отображения.")
else:
    # Ограничиваем количество изображений для отображения
    count_images_to_display = min(20, len(images))

    # Количество столбцов для отображения
    cols = 5
    rows = count_images_to_display // cols + (count_images_to_display % cols > 0)

# Настройка фигуры для отображения
    plt.figure(figsize=(15, 5 * rows))
    for i in range(count_images_to_display):
        plt.subplot(rows, cols, i + 1)
        # Отображаем изображение и Конвертируем BGR -> RGB
        plt.imshow(cv2.cvtColor(images[i], cv2.COLOR_BGR2RGB))
        plt.title(f"{filenames[i]}")    # Подпись с именем файла
        plt.axis('off') # Отключаем оси
    
    # Отображаем изображения
    plt.tight_layout() 
    plt.show()
Проверка файлов в папке ../static/images...
Проверяется файл: 0001.jpg
Загружаем изображение: ../static/images\0001.jpg
Проверяется файл: 0002.jpg
Загружаем изображение: ../static/images\0002.jpg
Проверяется файл: 0003.jpg
Загружаем изображение: ../static/images\0003.jpg
Проверяется файл: 0004.jpg
Загружаем изображение: ../static/images\0004.jpg
Проверяется файл: 0005.jpg
Загружаем изображение: ../static/images\0005.jpg
Проверяется файл: 0006.jpg
Загружаем изображение: ../static/images\0006.jpg
Проверяется файл: 0007.jpg
Загружаем изображение: ../static/images\0007.jpg
Проверяется файл: 0008.jpg
Загружаем изображение: ../static/images\0008.jpg
Проверяется файл: 0009.jpg
Загружаем изображение: ../static/images\0009.jpg
Проверяется файл: 0010.jpg
Загружаем изображение: ../static/images\0010.jpg
Проверяется файл: 0011.jpg
Загружаем изображение: ../static/images\0011.jpg
Проверяется файл: 0012.jpg
Загружаем изображение: ../static/images\0012.jpg
Проверяется файл: 0013.jpg
Загружаем изображение: ../static/images\0013.jpg
Проверяется файл: 0014.jpg
Загружаем изображение: ../static/images\0014.jpg
Проверяется файл: 0015.jpg
Загружаем изображение: ../static/images\0015.jpg
Проверяется файл: 0016.jpg
Загружаем изображение: ../static/images\0016.jpg
Проверяется файл: 0017.jpg
Загружаем изображение: ../static/images\0017.jpg
Проверяется файл: 0018.jpg
Загружаем изображение: ../static/images\0018.jpg
Проверяется файл: 0019.jpg
Загружаем изображение: ../static/images\0019.jpg
Проверяется файл: 0020.jpg
Загружаем изображение: ../static/images\0020.jpg
Проверяется файл: 0021.jpg
Загружаем изображение: ../static/images\0021.jpg
Проверяется файл: 0022.jpg
Загружаем изображение: ../static/images\0022.jpg
Проверяется файл: 0023.jpg
Загружаем изображение: ../static/images\0023.jpg
Проверяется файл: 0024.jpg
Загружаем изображение: ../static/images\0024.jpg
Проверяется файл: 0025.jpg
Загружаем изображение: ../static/images\0025.jpg
Проверяется файл: 0026.jpg
Загружаем изображение: ../static/images\0026.jpg
Проверяется файл: 0027.jpg
Загружаем изображение: ../static/images\0027.jpg
Проверяется файл: 0028.jpg
Загружаем изображение: ../static/images\0028.jpg
Проверяется файл: 0029.jpg
Загружаем изображение: ../static/images\0029.jpg
Проверяется файл: 0030.jpg
Загружаем изображение: ../static/images\0030.jpg
Проверяется файл: 0031.jpg
Загружаем изображение: ../static/images\0031.jpg
Проверяется файл: 0032.jpg
Загружаем изображение: ../static/images\0032.jpg
Проверяется файл: 0033.jpg
Загружаем изображение: ../static/images\0033.jpg
Проверяется файл: 0034.jpg
Загружаем изображение: ../static/images\0034.jpg
Проверяется файл: 0035.jpg
Загружаем изображение: ../static/images\0035.jpg
Проверяется файл: 0036.jpg
Загружаем изображение: ../static/images\0036.jpg
Проверяется файл: 0037.jpg
Загружаем изображение: ../static/images\0037.jpg
Проверяется файл: 0038.jpg
Загружаем изображение: ../static/images\0038.jpg
Проверяется файл: 0039.jpg
Загружаем изображение: ../static/images\0039.jpg
Проверяется файл: 0040.jpg
Загружаем изображение: ../static/images\0040.jpg
Проверяется файл: 0041.jpg
Загружаем изображение: ../static/images\0041.jpg
Проверяется файл: 0042.jpg
Загружаем изображение: ../static/images\0042.jpg
Проверяется файл: 0043.jpg
Загружаем изображение: ../static/images\0043.jpg
Проверяется файл: 0044.jpg
Загружаем изображение: ../static/images\0044.jpg
Проверяется файл: 0045.jpg
Загружаем изображение: ../static/images\0045.jpg
Проверяется файл: 0046.jpg
Загружаем изображение: ../static/images\0046.jpg
Проверяется файл: 0047.jpg
Загружаем изображение: ../static/images\0047.jpg
Проверяется файл: 0048.jpg
Загружаем изображение: ../static/images\0048.jpg
Проверяется файл: 0049.jpg
Загружаем изображение: ../static/images\0049.jpg
Проверяется файл: 0050.jpg
Загружаем изображение: ../static/images\0050.jpg
Проверяется файл: 0051.jpg
Загружаем изображение: ../static/images\0051.jpg
Проверяется файл: 0052.jpg
Загружаем изображение: ../static/images\0052.jpg
Проверяется файл: 0053.jpg
Загружаем изображение: ../static/images\0053.jpg
Проверяется файл: 0054.jpg
Загружаем изображение: ../static/images\0054.jpg
Проверяется файл: 0055.jpg
Загружаем изображение: ../static/images\0055.jpg
Проверяется файл: 0056.jpg
Загружаем изображение: ../static/images\0056.jpg
Проверяется файл: test
Проверяется файл: train
No description has been provided for this image

Сделаем предобработку

In [6]:
for i, image in enumerate(images):
    # Получаем размеры изображения
    im, w, _ = image.shape
    size = image.size

# Получение RGB
print("\n\n Получение RGB матрицы")
plt.imshow(image)
plt.show()

r, g, b = image.transpose((2, 0, 1))
display(r, g, b)

 Получение RGB матрицы
No description has been provided for this image
array([[128, 128, 128, ..., 151, 151, 151],
       [128, 128, 128, ..., 151, 151, 151],
       [128, 128, 128, ..., 151, 151, 151],
       ...,
       [111, 111, 112, ..., 138, 138, 138],
       [111, 111, 112, ..., 138, 138, 138],
       [111, 111, 112, ..., 138, 138, 138]], dtype=uint8)
array([[113, 113, 114, ..., 138, 138, 138],
       [113, 113, 113, ..., 138, 138, 138],
       [112, 112, 113, ..., 138, 138, 138],
       ...,
       [101, 101, 102, ..., 126, 126, 126],
       [101, 101, 102, ..., 126, 126, 126],
       [101, 101, 102, ..., 126, 126, 126]], dtype=uint8)
array([[ 94,  94,  95, ..., 122, 122, 122],
       [ 94,  94,  94, ..., 123, 122, 122],
       [ 93,  93,  94, ..., 122, 122, 122],
       ...,
       [101, 101, 102, ..., 126, 126, 126],
       [101, 101, 102, ..., 126, 126, 126],
       [101, 101, 102, ..., 126, 126, 126]], dtype=uint8)

Применим методы предобработки изображений
Будет несколько этап предобработки изображений, таких как изменение размеров, цвета, оттенков и увеличение контраста.

In [ ]:
import mahotas as mh

# Функция предобработки изображений на библиотеке mahotas
def preprocess_images_on_mh(images):
    processed_images = []
    for image in images:
        # Изменение размеров
        image_resized = mh.resize.resize_rgb_to(image, (100, 100))

        # Увеличение контраста с помощью гистограммы. Преобразование в оттенки серого
        image_gray = mh.colors.rgb2gray(image_resized)

        # Растяжение контраста (замена mh.equalize)
        image_eq = mh.stretch(image_gray)

        processed_images.append(image_eq)

    return np.array(processed_images)


# Функция отображения оригинала и обработанного изображения
def display_image(original, processed, index):
    plt.figure(figsize=(10, 5))

    # Оригинальное изображение
    plt.subplot(1, 2, 1)
    plt.imshow(original[index])
    plt.title(f'Оригинал {index+1}')
    plt.axis('off')

    # Обработанное изображение
    plt.subplot(1, 2, 2)
    plt.imshow(processed[index], cmap='gray')
    plt.title(f'Обработанное {index+1}')
    plt.axis('off')

    plt.show()

print("На Mahotas")

# Применяем предобработку 
processed_images_on_mahotas = preprocess_images_on_mh(images)
display_image(images, processed_images_on_mahotas, i)


# Функция предобработки изображений на библиотеке CV2
def preprocess_images(images):
    processed_images = []
    for image in images:
        # Изменение размера
        image_resized = cv2.resize(image, (256, 256))

        # Преобразование в оттенки серого
        image_gray = cv2.cvtColor(image_resized, cv2.COLOR_BGR2GRAY)

        # Увеличим контраст с помощью выравнивания гистограммы
        image_eq = cv2.equalizeHist(image_gray)

        processed_images.append(image_eq)
    
    return np.array(processed_images)

print("На OpenCV")

procesed_images = preprocess_images(images)
display_image(images, procesed_images, i)
На Mahotas
No description has been provided for this image
На OpenCV
No description has been provided for this image

Работа с гаммой изображения и встроенные методы преобразования

In [ ]:
new_greenless_image = mh.as_rgb(r, g * 0, b)
plt.imshow(new_greenless_image)
plt.show()


# Удаляем зелёный канал
greenless_image = image.copy()
greenless_image[:, :, 1] = 0  # Обнуляем G-канал

plt.imshow(greenless_image)
plt.title("Без зелёного канала")
plt.axis("off")
plt.show()
No description has been provided for this image
No description has been provided for this image
In [9]:
grey_image_mh = mh.colors.rgb2gray(image)

sepia_image = mh.colors.rgb2gray(image)

fig, axes = plt.subplots(1, 2, figsize=(12, 8))
axes[0].imshow(grey_image_mh, cmap="gray")
axes[0].set_title("Greyscale")
axes[1].imshow(sepia_image)
axes[1].set_title("Sepia")
plt.show()


# Матрица для эффекта сепии
sepia_filter = np.array([[0.393, 0.769, 0.189],
                         [0.349, 0.686, 0.168],
                         [0.272, 0.534, 0.131]])

# Применение фильтра
sepia_image = cv2.transform(image.astype(np.float32) / 255, sepia_filter)
sepia_image = np.clip(sepia_image * 255, 0, 255).astype(np.uint8)  # Ограничиваем диапазон

fig, axes = plt.subplots(1, 2, figsize=(12, 8))
axes[0].imshow(image, cmap="gray")
axes[0].set_title("Greyscale")
axes[0].axis("off")
axes[1].imshow(sepia_image)
axes[1].set_title("Sepia")
axes[1].axis("off")
plt.show()
No description has been provided for this image
No description has been provided for this image

Фильтрация:

Удалим шумы
Изменим резкость
Определим границы с помощью Canny
Воспользуемя гистерезис, для окончательного определения границы с использованием двух пороговых значений: нижнего и верхнего

In [10]:
def apply_filters(image):
    # Удалим шумы в картинке
    image_blur = cv2.GaussianBlur(image, (5, 5), 0)

    # Повысим резкость изображения
    kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]])
    image_sharp = cv2.filter2D(image_blur, -1, kernel)

    # Определим границы
    image_edges = cv2.Canny(image_sharp, 200, 200)

    return image_edges

def preprocess_images(images):
    process_images = []
    for image in images:
        # Изменение размера
        image_resized = cv2.resize(image, (256, 256))

        # Преобразование в оттенки серого
        image_gray = cv2.cvtColor(image_resized, cv2.COLOR_BGR2GRAY)

        # Удаляем зелёный канал
        greenless_image = image_gray.copy()
        greenless_image[:, 1] = 0  # Обнуляем G-канал

        # Увеличим контраст с помощью гистограммы
        image_eq = cv2.equalizeHist(greenless_image)
        process_images.append(image_eq)
    return np.array(process_images)


# Выполнение предобработки изображений
processed_images = preprocess_images(images)

# Применяем фильтры к каждому изображению
filtered_images = np.array([apply_filters(image) for image in processed_images])

# Функция для отображения одного изображения
def display_single_image(original, processed, index):
    plt.figure(figsize=(10, 5))
    
    # Отображение оригинального изображения
    plt.subplot(1, 2, 1)
    plt.imshow(cv2.cvtColor(original[index], cv2.COLOR_BGR2RGB))
    plt.title('Оригинальное изображение')
    plt.axis('off')

    # Отображение обработанного изображения
    plt.subplot(1, 2, 2)
    plt.imshow(processed[index], cmap='gray')
    plt.title('Обработанное изображение')
    plt.axis('off')

    plt.show()

index = 1
display_single_image(images, filtered_images, index)
No description has been provided for this image

Извлекем признаки из изображений

Сначало цветовые признаки, потом текстурные признаки

In [ ]:
import os
import cv2
import numpy as np
import mahotas
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.metrics import classification_report, confusion_matrix

def load_images_from_folder(main_folder):
    images = []
    filenames = []

    # Проверяем, существует ли основная папка
    if not os.path.exists(main_folder):
        raise FileNotFoundError(f"Папка {main_folder} не найдена")

    # Рекурсивно проходим по всем подпапкам
    for root, _, files in os.walk(main_folder):
        class_name = os.path.basename(root)

        for filename in files:
            file_path = os.path.join(root, filename)

            # Проверяем расширение файла
            if filename.lower().endswith(('.png', '.jpg', '.jpeg')):
                image = cv2.imread(file_path)
                if image is not None:
                    image = cv2.resize(image, (512, 512))
                    images.append(image)
                    filenames.append(class_name)

    return images, filenames

def extract_color_features(image):
    color_features = []

    if len(image.shape) == 2:
        # Чёрно-белое изображение
        color_features.extend([
            np.mean(image), np.std(image),
            *cv2.calcHist([image], [0], None, [256], [0, 256]).flatten()
        ])
    else:
        # Цветное изображение
        for channel in range(image.shape[2]):
            channel_image = image[:, :, channel]
            color_features.extend([
                np.mean(channel_image), np.std(channel_image),
                *cv2.calcHist([channel_image], [0], None, [256], [0, 256]).flatten()
            ])

    return np.array(color_features)

def extract_texture_features(image):
    gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    haralick_features = mahotas.features.haralick(gray_image).mean(axis=0)
    return haralick_features

def extract_features(images):
    features = []
    for image in images:
        color_features = extract_color_features(image)
        texture_features = extract_texture_features(image)
        combined_features = np.hstack([color_features, texture_features])
        features.append(combined_features)
    return np.array(features)

data_folder = "../static/images"

images, filenames = load_images_from_folder(data_folder)

if len(images) == 0:
    raise ValueError("Не загружено ни одного изображения. Проверьте папку images и наличие картинок")

print(f"Загружено {len(images)} изображений из {len(np.unique(filenames))} классов")

# Извлечение признаков
features_array = extract_features(images)
filenames_array = np.array(filenames)

if len(np.unique(filenames_array)) < 2:
    raise ValueError("Для классификации нужно как минимум 2 класса")

# Разделение данных
X_train, X_test, y_train, y_test = train_test_split(features_array, filenames_array, test_size=0.2, stratify=filenames_array, random_state=42)

# Обучение модели
clf = SVC(kernel='linear')
clf.fit(X_train, y_train)

# Оценка модели
y_pred = clf.predict(X_test)

print("\nConfusion Matrix:")
print(confusion_matrix(y_test, y_pred))
print("\nОтчёт о классификации:")
print(classification_report(y_test, y_pred, zero_division=1))
Загружено 140 изображений из 3 классов

Confusion Matrix:
[[4 2 5]
 [5 0 0]
 [7 1 4]]

Отчёт о классификации:
              precision    recall  f1-score   support

      images       0.25      0.36      0.30        11
        test       0.00      0.00      0.00         5
       train       0.44      0.33      0.38        12

    accuracy                           0.29        28
   macro avg       0.23      0.23      0.23        28
weighted avg       0.29      0.29      0.28        28

Теперь обучим модель

In [13]:
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.metrics import classification_report, confusion_matrix

folder = "../static/images"
images, labels = load_images_from_folder(folder)
features_array = extract_features(images)

# Преобразуем в массив Numpy
filenames_array = np.array(filenames)

if len(np.unique(filenames_array)) < 2:
    raise ValueError("Для классификации нужно как минимум 2 класса")

# Разделю выборки на тренировочную и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(features_array, filenames_array, test_size = 0.2, stratify=filenames_array, random_state=42)

# Стандартизируем данные
scaler = StandardScaler()
x_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

# Обучим классификатор SVM
model = SVC(kernel='linear', C=1)
model.fit(X_train, y_train)

# Предсказание на тестовых данных
y_pred = model.predict(X_test)

# Оценки и метрики качества
print(confusion_matrix(y_test, y_pred))
print(classification_report(y_test, y_pred))
[[11  0  0]
 [ 5  0  0]
 [12  0  0]]
              precision    recall  f1-score   support

      images       0.39      1.00      0.56        11
        test       0.00      0.00      0.00         5
       train       0.00      0.00      0.00        12

    accuracy                           0.39        28
   macro avg       0.13      0.33      0.19        28
weighted avg       0.15      0.39      0.22        28

d:\PythonPr\AIM-PIbd-31-Yaruskin-S-A\.venv\Lib\site-packages\sklearn\metrics\_classification.py:1531: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.
  _warn_prf(average, modifier, f"{metric.capitalize()} is", len(result))
d:\PythonPr\AIM-PIbd-31-Yaruskin-S-A\.venv\Lib\site-packages\sklearn\metrics\_classification.py:1531: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.
  _warn_prf(average, modifier, f"{metric.capitalize()} is", len(result))
d:\PythonPr\AIM-PIbd-31-Yaruskin-S-A\.venv\Lib\site-packages\sklearn\metrics\_classification.py:1531: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.
  _warn_prf(average, modifier, f"{metric.capitalize()} is", len(result))

Модель не научилась нормально предсказывать. Наверное из-за маленького датасета

Попробуем решить проблему Аугментации данных для 10 случайных изображений

In [14]:
import numpy as np
import cv2
import mahotas
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, confusion_matrix

def load_and_prepare_data():
    # Создаём тестовые данные (4 изображения 256x256)
    processed_images = np.random.randint(0, 256, (4, 128, 128), dtype=np.uint8)
    # 12 меток для 4 изображений
    labels = np.array([0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3])
    
    # Преобразуем в 1 метку на изображение (берем первую для каждого)
    unique_labels = []
    unique_images = []
    for i in range(0, len(labels), 3):
        unique_images.append(processed_images[i//3])
        unique_labels.append(labels[i])
    
    return np.array(unique_images), np.array(unique_labels)

def augment_data(images, labels, augment_count=40):
    images = np.array(images)
    labels = np.array(labels)
    
    if len(images) != len(labels):
        raise ValueError(f"Несоответствие размеров: {len(images)} изображений и {len(labels)} меток")
    
    # Создаём объект ImageDataGenerator для аугментации
    datagen = ImageDataGenerator(
        rotation_range=10,
        width_shift_range=0.1,
        height_shift_range=0.1,
        shear_range=0.1,
        zoom_range=0.1,
        horizontal_flip=True,
        fill_mode='nearest'
    )

    augmented_images = []
    augmented_labels = []

    for image, label in zip(images, labels):
        image = np.expand_dims(image, axis=-1) if len(image.shape) == 2 else image
        image = np.expand_dims(image, axis=0)  # Добавляем batch-измерение
        
        # Генерация 10 аугментированных вариантов
        for _ in range(augment_count):
            augmented_image = next(datagen.flow(image, batch_size=1))[0].astype(np.uint8)
            augmented_images.append(augmented_image.squeeze())
            augmented_labels.append(label)

    return np.array(augmented_images), np.array(augmented_labels)

def extract_features(images):
    # Извлечение признаков с проверками
    features = []
    for image in images:
        # Цветовые признаки

        channels = [image] if len (image.shape) == 2 else [image[:, :, i] for i in range(image.shape[2])]
        
        color_features = []
        for channel in range(image.shape[2]):
            channel_image = image[:, :, channel]
            color_features.extend([np.mean(channel_image), np.std(channel_image),
                *cv2.calcHist([channel_image], [0], None, [256], [0, 256]).flatten()
            ])
        
        # Текстура
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if image.shape[2] == 3 else image[:, :, 0]
        texture_features = mahotas.features.haralick(gray).mean(axis=0)
        
        features.append(np.hstack([color_features, texture_features]))
    
    return np.array(features)


def extract_texture_features(image):
    # Если изображение уже чёрно-белое (одноканальное), преобразовывать не нужно
    if len(image.shape) == 2 or image.shape[2] == 1:
        gray_image = image if len(image.shape) == 2 else image[:, :, 0]
    else:
        gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    return mahotas.features.haralick(gray_image).mean(axis=0)


def extract_features(images):
    # Извлекает признаки из всех изображений.
    return np.array([np.hstack([extract_color_features(image), extract_texture_features(image)]) for image in images])


# Основной поток выполнения
try:
    # 1. Загрузка данных
    processed_images, labels = load_and_prepare_data()
    print(f"Загружено {len(processed_images)} изображений и {len(labels)} меток")
    
    # 2. Аугментация
    augmented_images, augmented_labels = augment_data(processed_images, labels)
    print(f"Создано {len(augmented_images)} аугментированных изображений")
    
    # 3. Преобразование в RGB (если нужно)
    if augmented_images.shape[-1] == 1:
        augmented_images = np.repeat(augmented_images, 3, axis=-1)
    
    # 4. Извлечение признаков
    features = extract_features(augmented_images)
    print(f"Извлечено {len(features)} наборов признаков")
    
    # 5. Разделение данных
    X_train, X_test, y_train, y_test = train_test_split(
        features, augmented_labels, test_size=0.2, random_state=42
    )
    print(f"Обучающая выборка: {len(X_train)}, тестовая: {len(X_test)}")
    
    # 6. Масштабирование
    scaler = StandardScaler()
    X_train = scaler.fit_transform(X_train)
    X_test = scaler.transform(X_test)
    
    # 7. Обучение модели (пример)
    from sklearn.ensemble import RandomForestClassifier
    model = RandomForestClassifier()
    model.fit(X_train, y_train)
    
    # 8. Оценка
    y_pred = model.predict(X_test)
    print(classification_report(y_test, y_pred))
    print(confusion_matrix(y_test, y_pred))

except Exception as e:
    print(f"Ошибка: {str(e)}")
Загружено 4 изображений и 4 меток
Создано 160 аугментированных изображений
Извлечено 160 наборов признаков
Обучающая выборка: 128, тестовая: 32
              precision    recall  f1-score   support

           0       0.86      0.60      0.71        10
           1       0.86      1.00      0.92         6
           2       0.82      1.00      0.90         9
           3       0.43      0.43      0.43         7

    accuracy                           0.75        32
   macro avg       0.74      0.76      0.74        32
weighted avg       0.75      0.75      0.74        32

[[6 0 0 4]
 [0 6 0 0]
 [0 0 9 0]
 [1 1 2 3]]

Другое дело. Существенный прирост по сравнению с сырым обучением. Ранее модель тупо предсказывала один класс (accuracy ≈ 0.39). После аугментации accuracy поднялась до 0.75, а большинство F1-метрик вышли на уровень 0.70.9. Классы 1 и 2 решаются почти идеально: recall = 1.0, F1 > 0.9, класс 0 тоже неплох — F1 = 0.71, но половину «0» модель путает с «3» (4 из 10), Класс 3 остаётся слабым: precision/recall по 0.43, F1 = 0.43. Он часто смешивается с другими. Вероятно, для класса 3 аугментации либо слишком мало, либо они слишком «одинаковые», и модель не охватывает вариативность этого класса. Нужно добавить больше данных, тогда станет лучше