fix initialization, throttle operations

day/winter/night switcher fixes
This commit is contained in:
Alex Bezdieniezhnykh
2025-02-19 23:07:16 +02:00
parent c314268d1e
commit d1af7958f8
17 changed files with 170 additions and 88 deletions
+7 -13
View File
@@ -51,7 +51,7 @@ public partial class Annotator
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(100);
private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(300);
private static readonly Guid SaveConfigTaskId = Guid.NewGuid();
public ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new();
public ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new();
@@ -72,6 +72,7 @@ public partial class Annotator
IInferenceService inferenceService)
{
InitializeComponent();
_appConfig = appConfig.Value;
_configUpdater = configUpdater;
_libVLC = libVLC;
@@ -119,9 +120,7 @@ public partial class Annotator
_suspendLayout = false;
TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory;
AnnotationClasses = new ObservableCollection<DetectionClass>(_appConfig.AnnotationConfig.AnnotationClasses);
LvClasses.DetectionDataGrid.ItemsSource = AnnotationClasses;
LvClasses.SelectNum(0);
LvClasses.Init(_appConfig.AnnotationConfig.DetectionClasses);
if (LvFiles.Items.IsEmpty)
BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.Initial]);
@@ -261,7 +260,7 @@ public partial class Annotator
{
_configUpdater.Save(_appConfig);
return Task.CompletedTask;
}, TimeSpan.FromSeconds(5));
}, SaveConfigTaskId, TimeSpan.FromSeconds(5));
}
private void ShowTimeAnnotations(TimeSpan time)
@@ -295,7 +294,7 @@ public partial class Annotator
}
foreach (var detection in annotation.Detections)
{
var annClass = _appConfig.AnnotationConfig.AnnotationClasses[detection.ClassNumber];
var annClass = _appConfig.AnnotationConfig.DetectionClasses[detection.ClassNumber];
var canvasLabel = new CanvasLabel(detection, canvasSize, videoSize, detection.Probability);
Editor.CreateDetectionControl(annClass, annotation.Time, canvasLabel);
}
@@ -312,6 +311,7 @@ public partial class Annotator
var annotations = await _dbFactory.Run(async db =>
await db.Annotations.LoadWith(x => x.Detections)
.Where(x => x.OriginalMediaName == _formState.VideoName)
.OrderBy(x => x.Time)
.ToListAsync(token: MainCancellationSource.Token));
TimedAnnotations.Clear();
@@ -425,12 +425,6 @@ public partial class Annotator
private void SeekTo(TimeSpan time) =>
SeekTo((long)time.TotalMilliseconds);
// private void AddClassBtnClick(object sender, RoutedEventArgs e)
// {
// LvClasses.IsReadOnly = false;
// DetectionClasses.Add(new DetectionClass(DetectionClasses.Count));
// LvClasses.SelectedIndex = DetectionClasses.Count - 1;
// }
private async void OpenFolderItemClick(object sender, RoutedEventArgs e) => await OpenFolder();
private async void OpenFolderButtonClick(object sender, RoutedEventArgs e) => await OpenFolder();
@@ -521,8 +515,8 @@ public partial class Annotator
{
//Take not annotataed medias
files = (LvFiles.ItemsSource as IEnumerable<MediaFileInfo>)?.Skip(LvFiles.SelectedIndex)
.Take(Constants.DETECTION_BATCH_SIZE)
.Where(x => !x.HasAnnotations)
.Take(Constants.DETECTION_BATCH_SIZE)
.Select(x => x.Path)
.ToList() ?? [];
if (files.Count != 0)
+4 -3
View File
@@ -51,7 +51,8 @@
CanUserResizeRows="False"
CanUserResizeColumns="False"
SelectionChanged="DetectionDataGrid_SelectionChanged"
x:FieldModifier="public">
x:FieldModifier="public"
>
<DataGrid.Columns>
<DataGridTemplateColumn Width="50" Header="Клавіша" CanUserSort="False">
<DataGridTemplateColumn.HeaderStyle>
@@ -90,13 +91,13 @@
IsChecked="True"
Style="{StaticResource ButtonRadioButtonStyle}"/>
<RadioButton x:Name="EveningModeRadioButton"
Tag="1"
Tag="20"
Content="Зима"
GroupName="Mode"
Checked="ModeRadioButton_Checked" Margin="10,0,0,0"
Style="{StaticResource ButtonRadioButtonStyle}"/>
<RadioButton x:Name="NightModeRadioButton"
Tag="2"
Tag="40"
Content="Ніч"
GroupName="Mode"
Checked="ModeRadioButton_Checked" Margin="10,0,0,0"
@@ -4,16 +4,26 @@ using Azaion.Common.DTO;
namespace Azaion.Common.Controls;
public class DetectionClassChangedEventArgs(DetectionClass? detectionClass, int classNumber) : EventArgs
public class DetectionClassChangedEventArgs(DetectionClass detectionClass, int classNumber) : EventArgs
{
public DetectionClass? DetectionClass { get; } = detectionClass;
public DetectionClass DetectionClass { get; } = detectionClass;
public int ClassNumber { get; } = classNumber;
}
public partial class DetectionClasses
{
public event EventHandler<DetectionClassChangedEventArgs>? DetectionClassChanged;
public DetectionClasses() => InitializeComponent();
public DetectionClasses()
{
InitializeComponent();
}
public void Init(List<DetectionClass> detectionClasses)
{
DetectionDataGrid.ItemsSource = detectionClasses;
DetectionDataGrid.SelectedIndex = 0;
}
public int CurrentClassNumber { get; private set; } = 0;
public DetectionClass? CurrentDetectionClass { get; set; }
@@ -27,26 +37,25 @@ public partial class DetectionClasses
private void RaiseDetectionClassChanged()
{
var detClass = (DetectionClass)DetectionDataGrid.SelectedItem;
var baseClassNumber = detClass?.Id ?? 0;
if (detClass == null)
return;
var modeAmplifier = 0;
foreach (var child in ModeSwitcherPanel.Children)
if (child is RadioButton { IsChecked: true } rb)
if (int.TryParse(rb.Tag?.ToString(), out int modeIndex))
{
if (detClass != null)
detClass.PhotoMode = (PhotoMode)modeIndex;
modeAmplifier += modeIndex * 20;
detClass.PhotoMode = (PhotoMode)modeIndex;
modeAmplifier += modeIndex;
}
CurrentDetectionClass = detClass;
CurrentClassNumber = baseClassNumber + modeAmplifier;
CurrentClassNumber = detClass.Id + modeAmplifier;
DetectionClassChanged?.Invoke(this, new DetectionClassChangedEventArgs(detClass, CurrentClassNumber));
}
public void SelectNum(int keyNumber)
{
DetectionDataGrid.SelectedIndex = keyNumber;
+2 -2
View File
@@ -50,7 +50,7 @@ public class DetectionControl : Border
_resizeStart = resizeStart;
_classNameLabel = new TextBlock
{
Text = detectionClass.Name,
Text = detectionClass.UIName,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 15, 0, 0),
@@ -120,7 +120,7 @@ public class DetectionControl : Border
public YoloLabel GetLabel(Size canvasSize, Size? videoSize = null)
{
var label = new CanvasLabel(DetectionClass.Id, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
var label = new CanvasLabel(DetectionClass.YoloId, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
return new YoloLabel(label, canvasSize, videoSize);
}
}
+2 -2
View File
@@ -41,8 +41,8 @@ public class AnnotationResult
var detectionClasses = detections.Select(x => x.ClassNumber).Distinct().ToList();
ClassName = detectionClasses.Count > 1
? string.Join(", ", detectionClasses.Select(x => allDetectionClasses[x].ShortName))
: allDetectionClasses[detectionClasses.FirstOrDefault()].Name;
? string.Join(", ", detectionClasses.Select(x => allDetectionClasses[x].UIName))
: allDetectionClasses[detectionClasses.FirstOrDefault()].UIName;
ClassColor0 = GetAnnotationClass(detectionClasses, 0);
ClassColor1 = GetAnnotationClass(detectionClasses, 1);
+22 -2
View File
@@ -4,12 +4,32 @@ namespace Azaion.Common.DTO.Config;
public class AnnotationConfig
{
public List<DetectionClass> AnnotationClasses { get; set; } = null!;
public List<DetectionClass> DetectionClasses { get; set; } = null!;
[JsonIgnore]
private Dictionary<int, DetectionClass>? _detectionClassesDict;
[JsonIgnore]
public Dictionary<int, DetectionClass> DetectionClassesDict => _detectionClassesDict ??= AnnotationClasses.ToDictionary(x => x.Id);
public Dictionary<int, DetectionClass> DetectionClassesDict
{
get
{
if (_detectionClassesDict != null)
return _detectionClassesDict;
var photoModes = Enum.GetValues(typeof(PhotoMode)).Cast<PhotoMode>().ToList();
_detectionClassesDict = DetectionClasses.SelectMany(cls => photoModes.Select(mode => new DetectionClass
{
Id = cls.Id,
Name = cls.Name,
ShortName = cls.ShortName,
PhotoMode = mode
}))
.ToDictionary(x => x.YoloId, x => x);
return _detectionClassesDict;
}
}
public List<string> VideoFormats { get; set; } = null!;
public List<string> ImageFormats { get; set; } = null!;
+1 -1
View File
@@ -41,7 +41,7 @@ public class ConfigUpdater : IConfigUpdater
{
AnnotationConfig = new AnnotationConfig
{
AnnotationClasses = Constants.DefaultAnnotationClasses,
DetectionClasses = Constants.DefaultAnnotationClasses,
VideoFormats = Constants.DefaultVideoFormats,
ImageFormats = Constants.DefaultImageFormats,
+20
View File
@@ -11,6 +11,23 @@ public class DetectionClass
public string Name { get; set; } = null!;
public string ShortName { get; set; } = null!;
public string UIName
{
get
{
var mode = PhotoMode switch
{
PhotoMode.Night => "(ніч)",
PhotoMode.Winter => "(зим)",
PhotoMode.Regular => "",
_ => ""
};
return ShortName + mode;
}
}
[JsonIgnore]
public PhotoMode PhotoMode { get; set; }
[JsonIgnore]
@@ -19,6 +36,9 @@ public class DetectionClass
[JsonIgnore] //For UI
public int ClassNumber => Id + 1;
[JsonIgnore]
public int YoloId => (int)PhotoMode + Id;
[JsonIgnore]
public SolidColorBrush ColorBrush => new(Color);
}
+35 -14
View File
@@ -1,34 +1,55 @@
namespace Azaion.Common.Extensions;
using System.Collections.Concurrent;
namespace Azaion.Common.Extensions;
public static class ThrottleExt
{
private static bool _throttleRunFirstOn;
public static async Task ThrottleRunFirst(this Func<Task> func, TimeSpan? throttleTime = null, CancellationToken cancellationToken = default)
private static ConcurrentDictionary<Guid, bool> _taskStates = new();
public static async Task ThrottleRunFirst(Func<Task> func, Guid actionId, TimeSpan? throttleTime = null, CancellationToken cancellationToken = default)
{
if (_throttleRunFirstOn)
if (_taskStates.ContainsKey(actionId) && _taskStates[actionId])
return;
_throttleRunFirstOn = true;
await func();
_taskStates[actionId] = true;
try
{
await func();
}
catch (Exception e)
{
Console.WriteLine(e);
}
_ = Task.Run(async () =>
{
await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500), cancellationToken);
_throttleRunFirstOn = false;
_taskStates[actionId] = false;
}, cancellationToken);
}
private static bool _throttleRunAfter;
public static async Task ThrottleRunAfter(this Func<Task> func, TimeSpan? throttleTime = null, CancellationToken cancellationToken = default)
public static async Task ThrottleRunAfter(Func<Task> func, Guid actionId, TimeSpan? throttleTime = null, CancellationToken cancellationToken = default)
{
if (_throttleRunAfter)
if (_taskStates.ContainsKey(actionId) && _taskStates[actionId])
return;
_throttleRunAfter = true;
_taskStates[actionId] = true;
_ = Task.Run(async () =>
{
await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500), cancellationToken);
await func();
_throttleRunAfter = false;
try
{
await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500), cancellationToken);
await func();
}
catch (Exception ex)
{
_taskStates[actionId] = false;
}
finally
{
_taskStates[actionId] = false;
}
}, cancellationToken);
await Task.CompletedTask;
}
+3 -2
View File
@@ -30,6 +30,7 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
private readonly IAuthProvider _authProvider;
private readonly QueueConfig _queueConfig;
private Consumer _consumer = null!;
private static readonly Guid SaveTaskId = Guid.NewGuid();
public AnnotationService(
IResourceLoader resourceLoader,
@@ -95,7 +96,7 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
{
_dbFactory.SaveToDisk();
return Task.CompletedTask;
}, TimeSpan.FromSeconds(3), cancellationToken);
}, SaveTaskId, TimeSpan.FromSeconds(3), cancellationToken);
}
});
@@ -184,7 +185,7 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
{
_dbFactory.SaveToDisk();
return Task.CompletedTask;
}, TimeSpan.FromSeconds(5), token);
}, SaveTaskId, TimeSpan.FromSeconds(5), token);
return annotation;
}
@@ -28,7 +28,7 @@ public class PythonResourceLoader : IResourceLoader, IAuthProvider
public PythonResourceLoader(PythonConfig config)
{
//StartPython();
StartPython();
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
_dealer.Connect($"tcp://{config.ZeroMqHost}:{config.ZeroMqPort}");
}
+3 -6
View File
@@ -57,16 +57,13 @@
<RowDefinition Height="32"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="200" />
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<controls:DetectionClasses
x:Name="LvClasses"
<controls:DetectionClasses x:Name="LvClasses"
Grid.Column="0"
Grid.Row="0">
</controls:DetectionClasses>
Grid.Row="0" />
<TabControl
Name="Switcher"
Grid.Column="2"
+34 -22
View File
@@ -22,10 +22,10 @@ public partial class DatasetExplorer
private readonly AnnotationConfig _annotationConfig;
private readonly DirectoriesConfig _directoriesConfig;
private Dictionary<int, List<Annotation>> _annotationsDict = new();
private readonly Dictionary<int, List<Annotation>> _annotationsDict;
private readonly CancellationTokenSource _cts = new();
public ObservableCollection<DetectionClass> AllDetectionClasses { get; set; } = new();
public List<DetectionClass> AllDetectionClasses { get; set; }
public ObservableCollection<AnnotationThumbnail> SelectedAnnotations { get; set; } = new();
public readonly Dictionary<string, AnnotationThumbnail> SelectedAnnotationDict = new();
@@ -34,6 +34,8 @@ public partial class DatasetExplorer
private readonly IDbFactory _dbFactory;
private readonly IMediator _mediator;
public readonly List<DetectionClass> AnnotationsClasses;
public bool ThumbnailLoading { get; set; }
@@ -48,6 +50,8 @@ public partial class DatasetExplorer
IDbFactory dbFactory,
IMediator mediator)
{
InitializeComponent();
_directoriesConfig = directoriesConfig.Value;
_annotationConfig = annotationConfig.Value;
_logger = logger;
@@ -55,7 +59,13 @@ public partial class DatasetExplorer
_dbFactory = dbFactory;
_mediator = mediator;
InitializeComponent();
var photoModes = Enum.GetValues(typeof(PhotoMode)).Cast<PhotoMode>().ToList();
_annotationsDict = _annotationConfig.DetectionClasses.SelectMany(cls => photoModes.Select(mode => (int)mode + cls.Id))
.ToDictionary(x => x, _ => new List<Annotation>());
_annotationsDict.Add(-1, []);
AnnotationsClasses = annotationConfig.Value.DetectionClasses;
Loaded += OnLoaded;
Activated += (_, _) => formState.ActiveWindow = WindowEnum.DatasetExplorer;
@@ -86,15 +96,29 @@ public partial class DatasetExplorer
Dispatcher.Invoke(() => RefreshThumbBar.Value = thumbnailsPercentage);
};
Closing += (_, _) => _cts.Cancel();
AllDetectionClasses = new List<DetectionClass>(
new List<DetectionClass> { new() {Id = -1, Name = "All", ShortName = "All"}}
.Concat(_annotationConfig.DetectionClasses));
LvClasses.Init(AllDetectionClasses);
_dbFactory.Run(async db =>
{
var allAnnotations = await db.Annotations
.LoadWith(x => x.Detections)
.OrderBy(x => x.AnnotationStatus)
.ThenByDescending(x => x.CreatedDate)
.ToListAsync();
foreach (var annotation in allAnnotations)
AddAnnotationToDict(annotation);
}).GetAwaiter().GetResult();
DataContext = this;
}
private async void OnLoaded(object sender, RoutedEventArgs e)
{
AllDetectionClasses = new ObservableCollection<DetectionClass>(
new List<DetectionClass> { new() {Id = -1, Name = "All", ShortName = "All"}}
.Concat(_annotationConfig.AnnotationClasses));
LvClasses.DetectionDataGrid.ItemsSource = AllDetectionClasses;
LvClasses.DetectionClassChanged += async (_, args) =>
{
ExplorerEditor.CurrentAnnClass = args.DetectionClass;
@@ -106,20 +130,8 @@ public partial class DatasetExplorer
ann.DetectionClass = args.DetectionClass;
};
ExplorerEditor.CurrentAnnClass = (DetectionClass)LvClasses.CurrentDetectionClass;
ExplorerEditor.CurrentAnnClass = LvClasses.CurrentDetectionClass ?? _annotationConfig.DetectionClasses.First();
await _dbFactory.Run(async db =>
{
var allAnnotations = await db.Annotations
.LoadWith(x => x.Detections)
.OrderBy(x => x.AnnotationStatus)
.ThenByDescending(x => x.CreatedDate)
.ToListAsync();
_annotationsDict = AllDetectionClasses.ToDictionary(x => x.Id, _ => new List<Annotation>());
foreach (var annotation in allAnnotations)
AddAnnotationToDict(annotation);
});
await ReloadThumbnails();
await LoadClassDistribution();
@@ -239,7 +251,7 @@ public partial class DatasetExplorer
AnnotationsTab.Visibility = Visibility.Collapsed;
EditorTab.Visibility = Visibility.Visible;
_tempSelectedClassIdx = LvClasses.CurrentClassNumber;
LvClasses.DetectionDataGrid.ItemsSource = _annotationConfig.AnnotationClasses;
LvClasses.DetectionDataGrid.ItemsSource = _annotationConfig.DetectionClasses;
Switcher.SelectedIndex = 1;
LvClasses.SelectNum(Math.Max(0, _tempSelectedClassIdx - 1));
+2 -1
View File
@@ -35,6 +35,7 @@ public partial class App
private PythonResourceLoader _resourceLoader = null!;
private Stream _securedConfig = null!;
private static readonly Guid KeyPressTaskId = Guid.NewGuid();
private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
@@ -199,7 +200,7 @@ public partial class App
{
var args = (KeyEventArgs)e;
var keyEvent = new KeyEvent(sender, args, _formState.ActiveWindow);
_ = ThrottleExt.ThrottleRunFirst(() => _mediator.Publish(keyEvent), TimeSpan.FromMilliseconds(50));
_ = ThrottleExt.ThrottleRunFirst(() => _mediator.Publish(keyEvent), KeyPressTaskId, TimeSpan.FromMilliseconds(50));
}
protected override async void OnExit(ExitEventArgs e)
+3 -2
View File
@@ -9,7 +9,6 @@ using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using Azaion.Common.Services;
using Azaion.CommonSecurity.Services;
using Azaion.Dataset;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using SharpVectors.Converters;
@@ -26,6 +25,7 @@ public partial class MainSuite
private readonly IDbFactory _dbFactory;
private readonly Dictionary<WindowEnum, Window> _openedWindows = new();
private readonly IResourceLoader _resourceLoader;
private static readonly Guid SaveConfigTaskId = Guid.NewGuid();
public MainSuite(IOptions<AppConfig> appConfig,
IConfigUpdater configUpdater,
@@ -89,6 +89,7 @@ public partial class MainSuite
};
lvItem.MouseUp += (lv, _) => OpenWindow((lv as ListViewItem)!);
ListView.Items.Add(lvItem);
_ = _sp.GetRequiredService(azaionModule.MainWindowType) as Window;
}
_ = Task.Run(async () => await _galleryService.RefreshThumbnails());
@@ -126,7 +127,7 @@ public partial class MainSuite
{
_configUpdater.Save(_appConfig);
return Task.CompletedTask;
}, TimeSpan.FromSeconds(2));
}, SaveConfigTaskId, TimeSpan.FromSeconds(2));
}
private void OnFormClosed(object? sender, EventArgs e)
+6 -5
View File
@@ -3,7 +3,8 @@
"ZeroMqHost": "127.0.0.1",
"ZeroMqPort": 5128,
"RetryCount": 25,
"TimeoutSeconds": 5
"TimeoutSeconds": 5,
"ResourcesFolder": "stage"
},
"DirectoriesConfig": {
"VideosDirectory": "E:\\Azaion6",
@@ -13,16 +14,16 @@
"ThumbnailsDirectory": "E:\\thumbnails"
},
"AnnotationConfig": {
"AnnotationClasses": [
{ "Id": 0, "Name": "Броньована техніка", "ShortName": "Бронь" },
{ "Id": 1, "Name": "Вантажівка", "ShortName": "Вантаж" },
"DetectionClasses": [
{ "Id": 0, "Name": "Броньована техніка", "ShortName": "Броня" },
{ "Id": 1, "Name": "Вантажівка", "ShortName": "Вантаж." },
{ "Id": 2, "Name": "Машина легкова", "ShortName": "Машина" },
{ "Id": 3, "Name": "Артилерія", "ShortName": "Арта" },
{ "Id": 4, "Name": "Тінь від техніки", "ShortName": "Тінь" },
{ "Id": 5, "Name": "Окопи", "ShortName": "Окопи" },
{ "Id": 6, "Name": "Військовий", "ShortName": "Військов" },
{ "Id": 7, "Name": "Накати", "ShortName": "Накати" },
{ "Id": 8, "Name": "Танк з захистом", "ShortName": "Танк захист" },
{ "Id": 8, "Name": "Танк з захистом", "ShortName": "Танк.захист" },
{ "Id": 9, "Name": "Дим", "ShortName": "Дим" },
{ "Id": 10, "Name": "Літак", "ShortName": "Літак" },
{ "Id": 11, "Name": "Мотоцикл", "ShortName": "Мото" }
+6 -2
View File
@@ -1,6 +1,5 @@
@echo off
cd Azaion.Suite
echo Build .net app
dotnet build -c Release
@@ -17,7 +16,7 @@ move dist\Azaion.Annotator.dll dist\dummy\
move dist\Azaion.Dataset.dll dist\dummy\
echo Build Cython app
cd Azaion.Inference\
cd Azaion.Inference
call ".\venv\Scripts\activate.bat"
pyinstaller --onefile ^
--collect-all jwt ^
@@ -43,3 +42,8 @@ pyinstaller --onefile ^
--hidden-import remote_command_handler ^
start.py
move dist\start.exe ..\dist\azaion-inference.exe
copy config.yaml ..\dist
echo Copy ico
cd ..
copy logo.ico dist\