diff --git a/web_ui/src/core/configurable-parameters/dtos/configuration.interface.ts b/web_ui/src/core/configurable-parameters/dtos/configuration.interface.ts index 2d059c0beb..761d6cced7 100644 --- a/web_ui/src/core/configurable-parameters/dtos/configuration.interface.ts +++ b/web_ui/src/core/configurable-parameters/dtos/configuration.interface.ts @@ -10,7 +10,7 @@ interface ParameterBaseDTO { interface NumberParameterDTO extends ParameterBaseDTO { type: 'int' | 'float'; value: number; - min_value: number; + min_value: number | null; max_value: number | null; default_value: number; } diff --git a/web_ui/src/core/configurable-parameters/services/configuration.interface.ts b/web_ui/src/core/configurable-parameters/services/configuration.interface.ts index ca99f3ca03..7100155209 100644 --- a/web_ui/src/core/configurable-parameters/services/configuration.interface.ts +++ b/web_ui/src/core/configurable-parameters/services/configuration.interface.ts @@ -10,7 +10,7 @@ interface ParameterBase { export interface NumberParameter extends ParameterBase { type: 'int' | 'float'; value: number; - minValue: number; + minValue: number | null; maxValue: number | null; defaultValue: number; } diff --git a/web_ui/src/pages/project-details/components/project-models/train-model-dialog/advanced-settings/data-management/filters/filters-options.component.tsx b/web_ui/src/pages/project-details/components/project-models/train-model-dialog/advanced-settings/data-management/filters/filters-options.component.tsx index a0bdec4f2b..16b952f96f 100644 --- a/web_ui/src/pages/project-details/components/project-models/train-model-dialog/advanced-settings/data-management/filters/filters-options.component.tsx +++ b/web_ui/src/pages/project-details/components/project-models/train-model-dialog/advanced-settings/data-management/filters/filters-options.component.tsx @@ -57,7 +57,7 @@ const FilterOption: FC = ({ option, onOptionChange }) => { { @@ -32,7 +32,7 @@ const getStep = ({ return step; } - if (maxValue === null) { + if (maxValue === null || minValue === null) { return type === 'int' ? DEFAULT_INT_STEP : DEFAULT_FLOAT_STEP; } @@ -63,13 +63,14 @@ export const NumberParameterField: FC = ({ setParameterValue(value); }, [value]); - if (maxValue === null) { + if (maxValue === null || minValue === null) { return ( diff --git a/web_ui/src/pages/project-details/components/project-models/train-model-dialog/advanced-settings/ui/range-parameter-field.component.tsx b/web_ui/src/pages/project-details/components/project-models/train-model-dialog/advanced-settings/ui/range-parameter-field.component.tsx index 51345f4260..a71577542c 100644 --- a/web_ui/src/pages/project-details/components/project-models/train-model-dialog/advanced-settings/ui/range-parameter-field.component.tsx +++ b/web_ui/src/pages/project-details/components/project-models/train-model-dialog/advanced-settings/ui/range-parameter-field.component.tsx @@ -54,14 +54,14 @@ export const RangeParameterField: FC = ({ const handleRangeChange = (inputValue: RangeValue): void => { const { start, end } = inputValue; // Prevent start and end from being equal - if (end - start > fieldStep) { + if (end - start >= fieldStep) { setParameterValue({ start, end }); } }; const handleNumberChange = (start: number, end: number): void => { // Prevent start and end from being equal - if (end - start > fieldStep) { + if (end - start >= fieldStep) { setParameterValue({ start, end }); onChange([start, end]); } @@ -90,7 +90,7 @@ export const RangeParameterField: FC = ({ step={fieldStep} flex={1} isDisabled={isDisabled} - aria-label={`Change ${name} range value`} + aria-label={`Change ${name} value`} UNSAFE_className={isDisabled ? '' : classes.rangeSlider} /> { @@ -18,17 +18,15 @@ describe('RangeParameterField', () => { const renderApp = ({ value = defaultValue, isDisabled = false }: { value?: number[]; isDisabled?: boolean }) => { return render( - - - + ); }; @@ -37,7 +35,7 @@ describe('RangeParameterField', () => { expect(screen.getByLabelText(`Change ${name} start range value`)).toBeInTheDocument(); expect(screen.getByLabelText(`Change ${name} end range value`)).toBeInTheDocument(); - expect(screen.getByLabelText(`Change ${name} range value`)).toBeInTheDocument(); + expect(screen.getByLabelText(`Change ${name} value`)).toBeInTheDocument(); }); it('calls onChange when start value changes', async () => { @@ -85,7 +83,7 @@ describe('RangeParameterField', () => { expect(screen.getByLabelText(`Change ${name} start range value`)).toBeDisabled(); expect(screen.getByLabelText(`Change ${name} end range value`)).toBeDisabled(); - const slider = screen.getByLabelText(`Change ${name} range value`); + const slider = screen.getByLabelText(`Change ${name} value`); expect(slider).toHaveClass('A-RCEa_is-disabled'); }); diff --git a/web_ui/src/shared/components/header/active-learning-configuration/training-settings/required-annotations.component.tsx b/web_ui/src/shared/components/header/active-learning-configuration/training-settings/required-annotations.component.tsx index a9776b9547..5ab4df58eb 100644 --- a/web_ui/src/shared/components/header/active-learning-configuration/training-settings/required-annotations.component.tsx +++ b/web_ui/src/shared/components/header/active-learning-configuration/training-settings/required-annotations.component.tsx @@ -33,7 +33,7 @@ export const RequiredAnnotations: FC = ({ 0.0. For example, (0.8, 1.2) will randomly scale the image between 80% and 120% of its original size.', + value: [0.5, 1.5], + default_value: [0.5, 1.5], + }, + { + key: 'max_shear_degree', + name: 'Maximum shear degree', + type: 'float', + description: + 'Maximum absolute shear angle in degrees to apply during affine transformation. A random shear in the range [-max_shear_degree, max_shear_degree] will be applied.', + value: 2.0, + default_value: 2.0, + min_value: null, max_value: null, - min_value: 0, + }, + ], + random_horizontal_flip: [ + { + key: 'enable', + name: 'Enable random horizontal flip', + type: 'bool', + description: + 'Whether to apply random flip images horizontally along the vertical axis (swap left and right)', + value: true, + default_value: true, + }, + { + key: 'max_translate_ratio', name: 'Horizontal translation', type: 'float', - value: 0, + description: + 'Maximum translation as a fraction of image width or height. A random translation in the range [-max_translate_ratio, max_translate_ratio] will be applied along both axes. For example, 0.1 allows up to ±10% translation.', + value: 0.1, + default_value: 0.1, + min_value: 0.0, + max_value: 1.0, + }, + { + key: 'scaling_ratio_range', + name: 'Scaling ratio range', + type: 'array', + description: + 'Range (min, max) of scaling factors to apply during affine transformation. Both values should be > 0.0. For example, (0.8, 1.2) will randomly scale the image between 80% and 120% of its original size.', + value: [0.5, 1.5], + default_value: [0.5, 1.5], }, { - default_value: 0, - description: 'Maximum vertical translation as a fraction of image height', - key: 'translate_y', + key: 'max_shear_degree', + name: 'Maximum shear degree', + type: 'float', + description: + 'Maximum absolute shear angle in degrees to apply during affine transformation. A random shear in the range [-max_shear_degree, max_shear_degree] will be applied.', + value: 2.0, + default_value: 2.0, + min_value: null, max_value: null, + }, + ], + random_vertical_flip: [ + { + key: 'enable', + name: 'Enable random vertical flip', + type: 'bool', + description: + 'Whether to apply random flip images vertically along the horizontal axis (swap top and bottom)', + value: false, + default_value: false, + }, + { + key: 'probability', + name: 'Probability', + type: 'float', + description: + 'Probability of applying vertical flip. A value of 0.5 means each image has a 50% chance to be flipped vertically.', + value: 0.5, + default_value: 0.5, + min_value: 0.0, + max_value: 1.0, + }, + ], + color_jitter: [ + { + key: 'enable', + name: 'Enable color jitter', + type: 'bool', + description: 'Whether to apply random color jitter to the image', + value: false, + default_value: false, + }, + { + key: 'brightness', + name: 'Brightness range', + type: 'array', + description: + 'Range (min, max) of brightness adjustment factors. A random factor from this range will be multiplied with the image brightness. For example, (0.8, 1.2) means brightness can be reduced by 20% or increased by 20%.', + value: [0.875, 1.125], + default_value: [0.875, 1.125], + }, + { + key: 'contrast', + name: 'Contrast range', + type: 'array', + description: + 'Range (min, max) of contrast adjustment factors. A random factor from this range will be multiplied with the image contrast. For example, (0.5, 1.5) means contrast can be halved or increased by up to 50%.', + value: [0.5, 1.5], + default_value: [0.5, 1.5], + }, + { + key: 'saturation', + name: 'Saturation range', + type: 'array', + description: + 'Range (min, max) of saturation adjustment factors. A random factor from this range will be multiplied with the image saturation. For example, (0.5, 1.5) means saturation can be halved or increased by up to 50%.', + value: [0.5, 1.5], + default_value: [0.5, 1.5], + }, + { + key: 'hue', + name: 'Hue range', + type: 'array', + description: + 'Range (min, max) of hue adjustment values. A random value from this range will be added to the image hue. For example, (-0.05, 0.05) means hue can be shifted by up to ±0.05.', + value: [-0.05, 0.05], + default_value: [-0.05, 0.05], + }, + { + key: 'probability', + name: 'Probability', + type: 'float', + description: + 'Probability of applying color jitter. A value of 0.5 means each image has a 50% chance to be color jittered.', + value: 0.5, + default_value: 0.5, + min_value: 0.0, + max_value: 1.0, + }, + ], + gaussian_blur: [ + { + key: 'enable', + name: 'Enable Gaussian blur', + type: 'bool', + description: 'Whether to apply Gaussian blur to the image', + value: false, + default_value: false, + }, + { + key: 'kernel_size', + name: 'Kernel size', + type: 'int', + description: + 'Size of the Gaussian kernel. Larger kernel sizes result in stronger blurring. Must be a positive odd integer.', + value: 5, + default_value: 5, min_value: 0, - name: 'Vertical translation', + max_value: null, + }, + { + key: 'sigma', + name: 'Sigma range', + type: 'array', + description: + 'Range (min, max) of sigma values for Gaussian blur. Sigma controls the amount of blurring. A random value from this range will be used for each image.', + value: [0.1, 2.0], + default_value: [0.1, 2.0], + }, + { + key: 'probability', + name: 'Probability', type: 'float', - value: 0, + description: + 'Probability of applying Gaussian blur. A value of 0.5 means each image has a 50% chance to be blurred.', + value: 0.5, + default_value: 0.5, + min_value: 0.0, + max_value: 1.0, }, + ], + gaussian_noise: [ { - default_value: 1, - description: 'Scaling factor for the image during affine transformation', - key: 'scale', + key: 'enable', + name: 'Enable Gaussian noise', + type: 'bool', + description: 'Whether to apply Gaussian noise to the image', + value: false, + default_value: false, + }, + { + key: 'mean', + name: 'Mean', + type: 'float', + description: + 'Mean of the Gaussian noise to be added to the image. Typically set to 0.0 for zero-mean noise.', + value: 0.0, + default_value: 0.0, + min_value: null, max_value: null, - min_value: 1, - name: 'Scale factor', + }, + { + key: 'sigma', + name: 'Standard deviation', type: 'float', - value: 1, + description: + 'Standard deviation of the Gaussian noise. Controls the intensity of the noise. Higher values result in noisier images.', + value: 0.1, + default_value: 0.1, + min_value: 0.0, + max_value: null, + }, + { + key: 'probability', + name: 'Probability', + type: 'float', + description: + 'Probability of applying Gaussian noise. A value of 0.5 means each image has a 50% chance to have noise added.', + value: 0.5, + default_value: 0.5, + min_value: 0.0, + max_value: 1.0, }, ], tiling: [ { - default_value: false, - description: 'Whether to apply tiling to the image', key: 'enable', name: 'Enable tiling', type: 'bool', + description: 'Whether to apply tiling to the image', value: true, + default_value: false, }, { - default_value: false, - description: 'Whether to use adaptive tiling based on image content', key: 'adaptive_tiling', name: 'Adaptive tiling', type: 'bool', + description: 'Whether to use adaptive tiling based on image content', value: false, + default_value: true, }, { - default_value: 128, - description: 'Size of each tile in pixels', key: 'tile_size', - max_value: null, - min_value: 0, name: 'Tile size', type: 'int', - value: 256, + description: + 'Size of each tile in pixels. Decreasing the tile size typically results in higher accuracy, but it is also more computationally expensive due to the higher number of tiles. In any case, the tile must be large enough to capture the entire object and its surrounding context, so choose a value larger than the size of most annotations.', + value: 400, + default_value: 400, + min_value: 0, + max_value: null, }, { - default_value: 64, - description: 'Overlap between adjacent tiles in pixels', key: 'tile_overlap', - max_value: null, - min_value: 0, name: 'Tile overlap', - type: 'int', - value: 64, + type: 'float', + description: 'Overlap between adjacent tiles as a fraction of tile size', + value: 0.2, + default_value: 0.2, + min_value: 0.0, + max_value: 1.0, }, ], }, @@ -3105,26 +3313,90 @@ export const expectedTrainingConfiguration: TrainingConfigurationUpdatePayloadDT value: 0.6, }, ], - random_affine: [ + color_jitter: [ { key: 'enable', value: true, }, { - key: 'degrees', - value: 15, + key: 'brightness', + value: [0.876, 1.091], }, { - key: 'translate_x', - value: 0, + key: 'contrast', + value: [0.5, 1.5], }, { - key: 'translate_y', + key: 'saturation', + value: [0.5, 1.5], + }, + { + key: 'hue', + value: [-0.05, 0.05], + }, + { + key: 'probability', + value: 0.5, + }, + ], + gaussian_blur: [ + { + key: 'enable', + value: false, + }, + { + key: 'kernel_size', + value: 5, + }, + { + key: 'sigma', + value: [0.1, 2], + }, + { + key: 'probability', + value: 0.5, + }, + ], + gaussian_noise: [ + { + key: 'enable', + value: false, + }, + { + key: 'mean', value: 0, }, { - key: 'scale', - value: 1, + key: 'sigma', + value: 0.1, + }, + { + key: 'probability', + value: 0.5, + }, + ], + iou_random_crop: [ + { + key: 'enable', + value: true, + }, + ], + random_affine: [ + { + key: 'enable', + value: true, + }, + { + key: 'degrees', + value: 15, + }, + { + key: 'scaling_ratio_range', + value: [0.5, 1.5], + }, + { + key: 'max_shear_degree', + value: 2, }, ], tiling: [ @@ -3142,9 +3414,19 @@ export const expectedTrainingConfiguration: TrainingConfigurationUpdatePayloadDT }, { key: 'tile_overlap', - value: 64, + value: 0.2, }, ], + random_horizontal_flip: [ + { key: 'enable', value: true }, + { key: 'max_translate_ratio', value: 0.1 }, + { key: 'scaling_ratio_range', value: [0.5, 1.5] }, + { key: 'max_shear_degree', value: 2 }, + ], + random_vertical_flip: [ + { key: 'enable', value: false }, + { key: 'probability', value: 0.5 }, + ], }, }, training: [ diff --git a/web_ui/tests/features/project-models/train-model.spec.ts b/web_ui/tests/features/project-models/train-model.spec.ts index 1738743d63..556ae54e7d 100644 --- a/web_ui/tests/features/project-models/train-model.spec.ts +++ b/web_ui/tests/features/project-models/train-model.spec.ts @@ -154,8 +154,11 @@ test.describe('Train model', () => { trainingSize: 71, }); - await trainModelPage.changeSubsetRange('start', 10); - await trainModelPage.changeSubsetRange('end', 5); + const distributionSlider = page.getByText('Distribution 70/20/10%'); + await trainModelPage.changeSubsetRange(distributionSlider, 'start', 10); + + const refreshedDistributionSlider = page.getByText('Distribution 60/30/10%'); + await trainModelPage.changeSubsetRange(refreshedDistributionSlider, 'end', 5); await expectTrainingSubsetsDistribution(page, { testSubset: 15, @@ -168,9 +171,42 @@ test.describe('Train model', () => { trainingSize: 61, }); + await page.getByRole('button', { name: 'Custom' }).click(); await trainModelPage.changeNumberParameter('Tile size', 128); await expect(trainModelPage.getNumberParameter('Tile size')).toHaveValue('128'); + await expect(trainModelPage.getBooleanParameter('Enable random IoU crop')).toBeChecked(); + + await trainModelPage.toggleEnableParameter('Enable color jitter'); + + await expect(page.getByRole('slider', { name: 'Minimum Change Brightness' })).toHaveValue('0.875'); + await expect(page.getByRole('slider', { name: 'Maximum Change Brightness' })).toHaveValue('1.125'); + + const brightnessStart = page.getByRole('button', { + name: 'Increase Change Brightness range start range value', + }); + + await brightnessStart.click(); + + const brightnessSlider = page.getByLabel('Change Brightness range value'); + + await brightnessSlider.click(); + + const endRange = brightnessSlider.getByRole('presentation').nth(2); + + await endRange.click(); + + const box = await endRange.boundingBox(); + + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 - 30, box.y + box.height / 2); + await page.mouse.up(); + } else { + throw new Error('Could not get bounding box for endRange handle'); + } + await expect(trainModelPage.getBooleanParameter('Enable center crop')).toBeChecked(); await expect(trainModelPage.getNumberParameter('Crop ratio')).toBeEnabled(); @@ -268,7 +304,9 @@ test.describe('Train model', () => { await trainModelPage.selectTab('Data management'); - await trainModelPage.changeSubsetRange('start', 10); + const distributionSlider = page.getByText('Distribution 70/20/10%'); + await trainModelPage.changeSubsetRange(distributionSlider, 'start', 10); + await expectTrainingSubsetsDistribution(page, { testSubset: 10, validationSubset: 30, @@ -380,8 +418,11 @@ test.describe('Train model', () => { trainingSize: 71, }); - await trainModelPage.changeSubsetRange('start', 10); - await trainModelPage.changeSubsetRange('end', 5); + const distributionSlider = page.getByText('Distribution 70/20/10%'); + await trainModelPage.changeSubsetRange(distributionSlider, 'start', 10); + + const refreshedDistributionSlider = page.getByText('Distribution 60/30/10%'); + await trainModelPage.changeSubsetRange(refreshedDistributionSlider, 'end', 5); await expectTrainingSubsetsDistribution(page, { testSubset: 15, @@ -397,6 +438,35 @@ test.describe('Train model', () => { await trainModelPage.changeNumberParameter('Tile size', 128); await expect(trainModelPage.getNumberParameter('Tile size')).toHaveValue('128'); + await trainModelPage.toggleEnableParameter('Enable color jitter'); + + await expect(page.getByRole('slider', { name: 'Minimum Change Brightness' })).toHaveValue('0.875'); + await expect(page.getByRole('slider', { name: 'Maximum Change Brightness' })).toHaveValue('1.125'); + + const brightnessStart = page.getByRole('button', { + name: 'Increase Change Brightness range start range value', + }); + + await brightnessStart.click(); + + const brightnessSlider = page.getByLabel('Change Brightness range value'); + + await brightnessSlider.click(); + + const endRange = brightnessSlider.getByRole('presentation').nth(2); + + await endRange.click(); + const box = await endRange.boundingBox(); + + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 - 30, box.y + box.height / 2); + await page.mouse.up(); + } else { + throw new Error('Could not get bounding box for endRange handle'); + } + await expect(trainModelPage.getBooleanParameter('Enable center crop')).toBeChecked(); await expect(trainModelPage.getNumberParameter('Crop ratio')).toBeEnabled(); @@ -497,7 +567,8 @@ test.describe('Train model', () => { await trainModelPage.selectTab('Data management'); - await trainModelPage.changeSubsetRange('start', 10); + const distributionSlider = page.getByText('Distribution 70/20/10%'); + await trainModelPage.changeSubsetRange(distributionSlider, 'start', 10); await expectTrainingSubsetsDistribution(page, { testSubset: 10, validationSubset: 30, diff --git a/web_ui/tests/fixtures/page-objects/train-model-dialog-page.ts b/web_ui/tests/fixtures/page-objects/train-model-dialog-page.ts index 4aa27ebd2d..9835f28ac0 100644 --- a/web_ui/tests/fixtures/page-objects/train-model-dialog-page.ts +++ b/web_ui/tests/fixtures/page-objects/train-model-dialog-page.ts @@ -1,7 +1,7 @@ // Copyright (C) 2022-2025 Intel Corporation // LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE -import { Page } from '@playwright/test'; +import { Locator, Page } from '@playwright/test'; export class TrainModelDialogPage { constructor(private page: Page) {} @@ -14,8 +14,8 @@ export class TrainModelDialogPage { await this.page.getByRole('button', { name: /advanced settings/i }).click(); } - async changeSubsetRange(range: 'start' | 'end', iterations: number) { - await this.page.getByLabel(range === 'start' ? 'Start range' : 'End range').click(); + async changeSubsetRange(slider: Locator, range: 'start' | 'end', iterations: number) { + await slider.getByLabel(range === 'start' ? 'Start range' : 'End range').click(); for (let i = 0; i < iterations; i++) { await this.page.keyboard.press('ArrowLeft');