exponenta event banner

Автоматизировать маркировку истинности земли по нескольким сигналам

В этом примере показано, как автоматизировать маркировку нескольких сигналов одновременно с помощью приложения Ground Truth Labeler и AutomationAlgorithm интерфейс. Алгоритм автоматизации, используемый в этом примере, оценивает положения меток транспортных средств в кадрах облака точек на основе положений меток транспортных средств в соответствующих кадрах изображения с использованием параметров калибровки камера-лидар.

Приложение «Маркировщик истины»

Для разработки алгоритмов вождения и оценки их эффективности крайне важны достоверные данные. Однако создание богатого и разнообразного набора аннотированных данных о вождении требует значительного времени и ресурсов. Приложение Ground Truth Labeler делает этот процесс эффективным. Это приложение можно использовать в качестве полностью ручного инструмента аннотаций для разметки границ полос движения, границ транспортного средства и других объектов, представляющих интерес для системы технического зрения. Однако ручная маркировка требует значительного количества времени и ресурсов. Это приложение также предоставляет структуру для создания алгоритмов для расширения и автоматизации процесса маркировки. Можно создать и использовать алгоритмы для быстрой маркировки целых наборов данных, а затем выполнить более эффективный и короткий этап проверки вручную. Можно также отредактировать результаты этапа автоматизации, чтобы учесть сложные сценарии, которые алгоритм автоматизации мог пропустить.

В этом примере описывается создание алгоритма, который можно использовать в приложении Ground Truth Labeler для автоматического обнаружения транспортных средств на изображении и оценки их положения в соответствующем облаке точек с помощью параметров калибровки камера-лидар.

Обнаружение транспортных средств с помощью детектора транспортного средства ACF

Для обнаружения транспортных средств на изображениях в алгоритме автоматизации используется предварительно обученный детектор характеристик канала (ACF) транспортного средства, vehicleDetectorACF. Просмотрите, как работает алгоритм, загрузив образец изображения и детектор транспортного средства ACF, обнаружив транспортные средства на изображении и вставив 2-D ограничивающие рамки вокруг транспортных средств на изображении.

% Load the data from the MAT file and extract the image.
data = load(fullfile(toolboxdir('lidar'),'lidardata','lcc','bboxGT.mat'));
I = data.im;

% Load the pretrained detector for vehicles.
detector = vehicleDetectorACF('front-rear-view');

% Detect vehicles and show the bounding boxes.
[imBboxes,~] = detect(detector, I);
Iout = insertShape(I,'rectangle',imBboxes,'LineWidth',4);
figure
imshow(Iout)
title('Detected Vehicles')

Figure contains an axes. The axes with title Detected Vehicles contains an object of type image.

Если имеется информация о калибровке камеры, можно улучшить работу этого детектора путем фильтрации ложных срабатываний при обнаружении. Пример визуального восприятия с использованием монокулярной камеры описывает, как создать предварительно обученный детектор транспортного средства и настроить его для обнаружения ограничивающих коробок транспортного средства с использованием калиброванной конфигурации монокулярной камеры.

Оценка 3-D ограничивающей рамки для транспортных средств в облаке точек

Для оценки транспортных средств в кадрах облака точек из соответствующих обнаруженных транспортных средств в кадрах изображения алгоритм использует bboxCameraToLidar(Панель инструментов Lidar). Эта функция использует параметры калибровки lidar-to-camera для оценки 3-D ограничивающих боксов на основе 2-D ограничивающих боксов. Чтобы оценить ограничивающие рамки, функция принимает за входные параметры внутренней камеры. cameraIntrinsicsи жесткое преобразование камера-лидар, rigid3d.

Просмотрите, как работает алгоритм, загрузив облако точек, соответствующее изображению, оценив 3-D ограничивающие рамки транспортных средств в облаке точек и вставив ограничивающие рамки вокруг транспортных средств в облаке точек.

% Extract the point cloud.   
ptCloud = data.pc;

% Extract the intrinsic camera parameters.
intrinsics = data.cameraParams;

% Extract the camera-to-lidar rigid transformation.
tform = data.camToLidar;
               
% Estimate the bounding boxes in the point cloud.
pcBboxes = bboxCameraToLidar(imBboxes, ptCloud, intrinsics, tform);

% Display bounding boxes in the point cloud.
figure
ax = pcshow(ptCloud.Location);
showShape('cuboid',pcBboxes,'Parent',ax,'Opacity',0.1,'Color',[0.06 1.00 1.00],'LineWidth',0.5)
hold on
zoom(ax,1.5)
title('Estimated Bounding Box in Point Cloud')
hold off

Figure contains an axes. The axes with title Estimated Bounding Box in Point Cloud contains an object of type scatter.

Подготовка класса автоматизации мультисигнальных детекторов транспортных средств

Чтобы включить алгоритм мультисигнального детектора транспортного средства в процесс автоматизации приложения Ground Truth Labeler, создайте класс, который наследует от абстрактного базового класса, vision.labeler.AutomationAlgorithm. Этот базовый класс определяет свойства и сигнатуры для методов, используемых приложением для настройки и запуска пользовательского алгоритма. Приложение Ground Truth Labeler предоставляет удобный способ получения начального шаблона класса автоматизации. Дополнительные сведения см. в разделе Создание алгоритма автоматизации для маркировки. Класс MultiSignalVehicleDetector основан на этом шаблоне и предоставляет готовый к использованию класс автоматизации для обнаружения транспортного средства при оценке изображения и ограничивающей рамки транспортного средства в облаке точек. Комментарии класса описывают основные шаги, необходимые для реализации каждого вызова API.

Шаг 1 содержит свойства, определяющие имя и описание алгоритма и направления использования алгоритма.

    % ----------------------------------------------------------------------
    % Step 1: Define the properties required for describing the algorithm,
    % which include Name, Description, and UserDirections.
    properties(Constant)
        
        % Name Algorithm name
        %   Character vector specifying the name of the algorithm.
        Name = 'Multisignal Vehicle Detector';
        
        % Description Algorithm description
        %   Character vector specifying the short description of the algorithm.
        Description = ['Detect vehicles using ACF Vehicle Detector in ' ...
            'image and estimate them in point cloud.'];
        
        % UserDirections Algorithm usage directions
        %   Cell array of character vectors specifying directions for
        %   algorithm users to follow.
        UserDirections = {['Select one of the rectangle ROI labels to ' ...
            'label objects as Vehicle.'], ...
            ['Click Settings and on the Lidar Camera Calibration ' ...
            'Parameters tab, load the cameraIntrinsics and rigid3d ' ...
            'objects from the workspace.'], ...
            ['Specify additional parameters under Settings.'], ...
            ['Click Run to detect vehicles in each image and point cloud.'], ...
            ['Review automated labels manually. You can modify, delete ', ...
            'and add new labels.'], ...
            ['If you are not satisfied with the results, click Undo ' ...
            'Run. Click Settings to modify algorithm settings and click ', ...
            'Run again.'] ...
            ['When you are satisfied with the results, click Accept and ', ...
            'return to manual labeling.']};
    end

Шаг 2 содержит пользовательские свойства основного алгоритма.

    % ---------------------------------------------------------------------
    % Step 2: Define properties to be used to manage algorithm execution.
    properties
        
        % SelectedLabelName Selected label name
        %   Name of the selected label. Vehicles detected by the algorithm will
        %   be assigned this variable name.
        SelectedLabelName
        
        % Detector Detector
        %   Pretrained vehicle detector, an object of class
        %   acfObjectDetector.
        Detector
        
        % VehicleModelName Vehicle detector model name
        %   Name of pretrained vehicle detector model.
        VehicleModelName = 'full-view';
        
        % OverlapThreshold Overlap threshold
        %   Threshold value used to eliminate overlapping bounding boxes
        %   around the reference bounding box, between 0 and 1. The
        %   bounding box overlap ratio denominator, 'RatioType', is set to
        %   'Min'.
        OverlapThreshold = 0.45;
        
        % ScoreThreshold Classification score threshold
        %   Threshold value used to reject detections with low detection
        %   scores.
        ScoreThreshold = 20;
        
        % ConfigureDetector Detection configuration flag
        %   Boolean value that determines whether the detector is 
        %   configured using monoCamera sensor.
        ConfigureDetector = false;
        
        % SensorObj monoCamera sensor
        %   Monocular camera sensor object, monoCamera, used to configure
        %   the detector. A configured detector runs faster and can 
        %   potentially result in better detections.
        SensorObj = [];
        
        % SensorStr monoCamera sensor variable name
        %   Character vector specifying the monoCamera object variable name 
        %   used to configure the detector.
        SensorStr = '';
        
        % VehicleWidth Vehicle width
        %   Vehicle width used to configure the detector, specified as
        %   [minWidth, maxWidth], which describes the approximate width of the
        %   object in world units.
        VehicleWidth = [1.5 2.5];
        
        % VehicleLength Vehicle length
        %   Vehicle length used to configure the detector, specified as
        %   [minLength, maxLength] vector, which describes the approximate
        %   length of the object in world units.
        VehicleLength = [];  
        
        % IntrinsicsObj Camera intrinsics
        %   cameraIntrinsics object, which represents a projective
        %   transformation from camera to image coordinates.
        IntrinsicsObj = [];
        
        % IntrinsicsStr cameraIntrinsics variable name
        %   cameraIntrinsics object variable name.
        IntrinsicsStr = '';
        
        % ExtrinsicsObj Camera-to-lidar rigid transformation
        %   rigid3d object representing the 3-D rigid geometric transformation 
        %   from the camera to the lidar.
        ExtrinsicsObj = [];
        
        % ExtrinsicsStr rigid3d variable name
        %   Camera-to-lidar rigid3d object variable name.
        ExtrinsicsStr = '';
        
        % ClusterThreshold Clustering threshold for two adjacent points
        %   Threshold specifying the maximum distance between two adjacent points
        %   for those points to belong to the same cluster.
        ClusterThreshold = 1;
        
    end

На шаге 3 рассматриваются определения функций.

Первая функция, supportsMultisignalAutomation, проверяет, поддерживает ли алгоритм несколько сигналов. Для мультисигнального детектора транспортного средства загружаются сигналы как изображения, так и облака точек, поэтому success имеет значение true.

        function success = supportsMultisignalAutomation(~)
            % Supports MultiSignal.
            success = true;
        end

Следующая функция, checkSignalType, проверяет, поддерживаются ли для автоматизации только сигналы соответствующего типа. Многосигнальный детектор транспортного средства должен поддерживать сигналы типа Image и PointCloudТаким образом, эта версия функции проверяет оба типа сигналов.

        
        function isValid = checkSignalType(signalType)
            % Only video/image sequence and point cloud signal data 
            % is valid. 
            isValid = any(signalType == vision.labeler.loading.SignalType.Image) && ...
               any(signalType == vision.labeler.loading.SignalType.PointCloud);  
        end

Следующая функция, checkLabelDefinitionпроверяет, включены ли для автоматизации только метки соответствующего типа. Для обнаружения транспортного средства в сигналах изображения и облаков точек необходимо проверить, что только метки типа Rectangle/Cuboid включены, поэтому эта версия функции проверяет Type этикеток.

        function isValid = checkLabelDefinition(~, labelDef)            
            % Only Rectangular/Cuboid ROI Label definitions are valid for the
            % Vehicle Detector.
            isValid = (labelDef.Type == labelType.Cuboid || labelDef.Type == labelType.Rectangle);
        end

Следующая функция, checkSetupпроверяет, что для автоматизации выбрано только одно определение метки ROI.

        function isReady = checkSetup(algObj, ~)
            % Is there one selected ROI Label definition to automate?
            isReady = ~isempty(algObj.SelectedLabelDefinitions);
        end

Далее, settingsDialog функция получает и изменяет свойства, определенные на этапе 2. Этот вызов API позволяет создать диалоговое окно, которое открывается при нажатии пользователем кнопки «Параметры» на вкладке «Автоматизация». Чтобы создать это диалоговое окно, используйте dialog для создания модального окна, чтобы попросить пользователя указать cameraIntrinsics объект и rigid3d объект. multiSignalVehicleDetectorSettings содержит код для настроек, а также добавляет шаги проверки ввода.

        function settingsDialog(algObj)
            % Invoke dialog box to input camera intrinsics and
            % camera-to-lidar rigid transformation and options for choosing
            % a pretrained model, overlap threshold, detection score
            % threshold, and clustering threshold. Optionally, input a
            % calibrated monoCamera sensor to configure the detector.
            multiSignalVehicleDetectorSettings(algObj);
        end

Шаг 4 определяет функции выполнения. initialize заполняет начальное состояние алгоритма на основе существующих меток в приложении. MultiSignalVehicleDetector класс, initialize функция была настроена для хранения имени выбранного определения этикетки и загрузки предварительно обученного детектора транспортного средства ACF и сохранения его в Detector собственность.

       function initialize(algObj, ~)
            
            % Store the name of the selected label definition. Use this
            % name to label the detected vehicles.
            algObj.SelectedLabelName = algObj.SelectedLabelDefinitions.Name;
            
            % Initialize the vehicle detector with a pretrained model.
            algObj.Detector = vehicleDetectorACF(algObj.VehicleModelName);
        end

Далее, run функция определяет основной алгоритм обнаружения транспортного средства данного класса автоматизации. run функция вызывается для каждого кадра последовательности изображения и облака точек и ожидает, что класс автоматизации вернет набор меток. run функция в MultiSignalVehicleDetector содержит вышеописанную логику для обнаружения 2-D ограничивающих коробок транспортного средства в кадрах изображения и оценки 3-D ограничивающих коробок транспортного средства в кадрах облака точек.

       function autoLabels = run(algObj, I)
            % autoLabels a cell array of length the same as the number of 
            %  signals.
            autoLabels = cell(size(I,1),1);
            
            % Get the index of Image and PointCloud frames.
            if isa(I{1,1},"pointCloud")
                pcIdx = 1;
                imIdx = 2;
            else
                imIdx = 1;
                pcIdx = 2;
            end
            
            % Detect bounding boxes on image frame.
            selectedBboxes = detectVehicle(algObj, I{imIdx,1});
            
            % Estimate bounding boxes on point cloud frame.
            if ~isempty(selectedBboxes)
                
                % Store labels from the image. 
                imageLabels = struct('Type', labelType.Rectangle, ...
                'Name', algObj.SelectedLabelDefinitions.Name, ...
                'Position', selectedBboxes);
                autoLabels{imIdx, 1} = imageLabels;
                
                % Remove the ground plane for the point cloud.
                groundPtsIndex = segmentGroundFromLidarData(I{pcIdx,1}, ...
                    "ElevationAngleDelta", 15, "InitialElevationAngle", 10);

                nonGroundPts = select(I{pcIdx,1}, ~groundPtsIndex);
                
                % Predict 3-D bounding boxes.
                pcBboxes = bboxCameraToLidar(selectedBboxes, nonGroundPts, algObj.IntrinsicsObj, ...
                    algObj.ExtrinsicsObj, "ClusterThreshold", algObj.ClusterThreshold);
                
                % Store labels from the point cloud.
                if(~isempty(pcBboxes))
                    pcLabels = struct('Type', labelType.Cuboid,...
                    'Name', algObj.SelectedLabelDefinitions.Name,...
                    'Position', pcBboxes);
                    autoLabels{pcIdx, 1} = pcLabels;
                else
                    autoLabels{pcIdx, 1} = {};
                end
            else
                autoLabels{imIdx, 1} = {};
                autoLabels{pcIdx, 1} = {};
            end                           
        end

Наконец, terminate функция обрабатывает любую очистку или разрыв, необходимые после выполнения автоматизации. Этот алгоритм не требует никакой очистки, поэтому функция пуста.

       function terminate(~)
       end

Использование класса автоматизации мультисигнальных детекторов транспортных средств в приложении

Свойства и методы, описанные в предыдущем разделе, реализованы в MultiSignalVehicleDetector файл класса алгоритма автоматизации. Чтобы использовать этот класс в приложении:

Создание структуры папок +vision/+labeler требуется в текущей папке и скопируйте в нее класс автоматизации.

    mkdir('+vision/+labeler');
    copyfile(fullfile(matlabroot,'examples','driving','main','MultiSignalVehicleDetector.m'), ...
        '+vision/+labeler');

Загрузите последовательность облаков точек (PCD) и последовательность изображений. В целях иллюстрации этот пример использует данные о лидаре WPI, собранные по шоссе от Изгнания датчик лидара OS1 и данные изображения WPI из фронтальной камеры, установленной на транспортном средстве эго. Выполните следующий блок кода для загрузки и сохранения данных лидара и изображения во временной папке. В зависимости от подключения к Интернету процесс загрузки может занять некоторое время. Код приостанавливает выполнение MATLAB ® до завершения процесса загрузки. Можно также загрузить набор данных на локальный диск с помощью веб-браузера и извлечь файл.

Загрузите последовательность изображений во временное место.

    imageURL = 'https://www.mathworks.com/supportfiles/lidar/data/WPI_ImageData.tar.gz';
    imageDataFolder = fullfile(tempdir, 'WPI_ImageData',filesep);
    imageDataTarFile = imageDataFolder + "WPI_ImageData.tar.gz";

    if ~exist(imageDataFolder,'dir')
        mkdir(imageDataFolder)
    end

    if ~exist(imageDataTarFile, 'file')
        disp('Downloading WPI Image driving data (225 MB)...');
        websave(imageDataTarFile, imageURL);
        untar(imageDataTarFile, imageDataFolder);
    end
    
    % Check if image tar.gz file is downloaded, but not uncompressed.
    if ~exist(fullfile(imageDataFolder,'imageData'),'dir')
        untar(imageDataTarFile, imageDataFolder)
    end

В целях иллюстрации в этом примере используется только подмножество последовательности изображений WPI из кадров 920-940. Чтобы загрузить подмножество изображений в приложение, скопируйте изображения в папку.

    % Create new folder and copy the images.
    imDataFolder = imageDataFolder + "imageDataSequence";
    if ~exist(imDataFolder,'dir')
        mkdir(imDataFolder);
    end

    for i = 920 : 940
        filename = strcat(num2str(i,'%06.0f'),'.jpg');
        source = fullfile(imageDataFolder,'imageData',filename);
        destination = fullfile(imageDataFolder,'imageDataSequence',filename);
        copyfile(source,destination)
    end

Загрузите последовательность облаков точек во временное место.

    lidarURL = 'https://www.mathworks.com/supportfiles/lidar/data/WPI_LidarData.tar.gz';
    lidarDataFolder = fullfile(tempdir,'WPI_LidarData',filesep);        
    lidarDataTarFile = lidarDataFolder + "WPI_LidarData.tar.gz";

    if ~exist(lidarDataFolder)
        mkdir(lidarDataFolder)
    end

    if ~exist(lidarDataTarFile, 'file')       
        disp('Downloading WPI Lidar driving data (760 MB)...');
        websave(lidarDataTarFile,lidarURL);
        untar(lidarDataTarFile,lidarDataFolder);
    end
    
    % Check if lidar tar.gz file is downloaded, but not uncompressed.
    if ~exist(fullfile(lidarDataFolder,'WPI_LidarData.mat'),'file')
        untar(lidarDataTarFile,lidarDataFolder);
    end

Приложение Ground Truth Labeler поддерживает загрузку последовательностей облаков точек, состоящих из файлов PCD или PLY. Сохраните загруженные данные облака точек в PCD-файлы. Для иллюстрации в этом примере сохраняется только подмножество данных облака точек WPI из кадров 920-940.

    % Load downloaded lidar data into the workspace.
    load(fullfile(lidarDataFolder,'WPI_LidarData.mat'),'lidarData');
    lidarData = reshape(lidarData,size(lidarData,2),1);
    
    % Create new folder and write lidar data to PCD files.
    pcdDataFolder = lidarDataFolder + "lidarDataSequence";
    if ~exist(pcdDataFolder, 'dir')
        mkdir(fullfile(lidarDataFolder,'lidarDataSequence'));
    end

    disp('Saving WPI Lidar driving data to PCD files ...');
    for i = 920:940
        filename = strcat(fullfile(lidarDataFolder,'lidarDataSequence',filesep), ...
            num2str(i,'%06.0f'),'.pcd');
        pcwrite(lidarData{i},filename);
    end

Предполагается, что калибровочная информация будет представлена в виде внутренних и внешних (жесткое преобразование) параметров, упомянутых в Lidar и Camera Calibration (Lidar Toolbox). Загрузочные характеристики камеры, которые хранятся в cameraIntrinsics объект и жесткое преобразование камера-лидар, которое хранится в rigid3d объект в рабочую область. Данные WPI в этом примере калиброваны, и внутренние и внешние параметры (преобразование камеры в лидар) сохраняются в файле MAT.

    data = load(fullfile(toolboxdir('lidar'),'lidardata','lcc','bboxGT.mat'));
    cameraParams = data.cameraParams;
    camToLidar = data.camToLidar;

Откройте приложение Ground Truth Labeler.

    imageDir = fullfile(tempdir, 'WPI_ImageData', 'imageDataSequence');
    pointCloudDir = fullfile(tempdir, 'WPI_LidarData', 'lidarDataSequence');

    groundTruthLabeler

На панели инструментов приложения выберите Импорт, а затем Добавить сигналы. В окне Add/Remove Signal загрузите последовательность изображений.

  1. Задать тип источника как Image Sequence.

  2. Найдите папку последовательности изображений, которая находится в расположении, указанном imageDir переменная.

  3. Используйте метки времени по умолчанию и щелкните Добавить источник. Папка последовательности изображений, imageDataSequence, добавляется в таблицу источников сигнала.

На панели инструментов приложения выберите Импорт, а затем Добавить сигналы. В окне «Добавление/удаление сигнала» загрузите последовательность облаков точек.

  1. Задать тип источника как Point Cloud Sequence.

  2. Найдите папку последовательности облаков точек, которая находится в расположении, указанном pointCloudDir переменная.

  3. Используйте метки времени по умолчанию и щелкните Добавить источник. Папка последовательности облаков точек, lidarDataSequence, добавляется в таблицу источников сигнала.

Нажмите кнопку ОК, чтобы импортировать сигналы в приложение. Чтобы просмотреть сигналы бок о бок, на вкладке Метка (Label) щелкните Показать сетку (Display Grid) и отобразите сигналы в сетке 1 на 2.

На вкладке Метки окупаемости инвестиций в левой панели щелкните Метка и определите метку окупаемости инвестиций с именем Vehicle и тип Rectangle/Cuboid, как показано здесь. При необходимости выберите цвет и нажмите кнопку ОК.

Выберите оба сигнала для автоматизации. На вкладке Метка (Label) выберите Алгоритм (Algorithm), а затем Выбрать сигналы (Select Signals) и выберите оба сигнала. Нажмите кнопку ОК.

В разделе Выбрать алгоритм (Select Algorithm) выберите Обновить (Refresh). Затем выберите Алгоритм (Algorithm), а затем Мультисигнальный детектор транспортного средства (Multisignal Vehicle Detector). Если этот параметр не отображается, убедитесь, что текущая рабочая папка содержит папку с именем +vision/+labeler, с именем файла MultiSignalVehicleDetector.m в этом.

Щелкните Автоматизировать (Automate). Приложение открывает сеанс автоматизации для выбранных сигналов и отображает направления использования алгоритма.

Загрузите внутренние параметры камеры в сеанс автоматизации.

  1. На вкладке Автоматизировать (Automate) щелкните Настройки (Settings).

  2. На вкладке «Параметры калибровки Lidar-to-Camera» выберите «Импорт характеристик камеры из рабочего пространства».

  3. Импорт параметров внутренней камеры, cameraParams, из рабочей области MATLAB. Нажмите кнопку ОК.

Загрузите преобразование камера-лидар в сеанс автоматизации.

  1. На вкладке Параметры калибровки Lidar-to-Camera щелкните Импорт преобразования camera-to-lidar из рабочего пространства.

  2. Импорт преобразования, camToLidar, из рабочей области MATLAB. Нажмите кнопку ОК.

При необходимости измените дополнительные настройки детектора транспортного средства и нажмите кнопку OK. Затем на вкладке Automate нажмите кнопку Run. Созданный алгоритм выполняется на каждом кадре последовательности и обнаруживает транспортные средства с помощью Vehicle тип метки. После того как приложение завершит запуск автоматизации, используйте ползунок или клавиши со стрелками для прокрутки последовательности, чтобы найти кадры, где алгоритм автоматизации помечен неправильно. Вручную сдвиньте результаты, настроив обнаруженные ограничивающие рамки или добавив новые ограничивающие рамки.

После того как обнаруженные ограничивающие рамки транспортного средства для всей последовательности будут удовлетворены, щелкните Принять (Accept). После этого можно будет вручную скорректировать метки или экспортировать помеченную истинность грунта в рабочее пространство MATLAB.

Концепции, описанные в этом примере, можно использовать для создания собственных алгоритмов автоматизации мультисигналов и расширения функциональных возможностей приложения.

См. также

Приложения

Функции

Объекты

Классы

Связанные темы