Skip to content

Commit 917e152

Browse files
committed
image previews performance improvement
1 parent 621fbed commit 917e152

File tree

1 file changed

+105
-69
lines changed

1 file changed

+105
-69
lines changed

src/DatasetCrop/MainWindow.axaml.cs

Lines changed: 105 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Avalonia.Media;
77
using Avalonia.Media.Imaging;
88
using Avalonia.Platform.Storage;
9+
using Avalonia.Threading;
910
using DatasetCrop.MVVM;
1011
using MessageBox.Avalonia;
1112
using MessageBox.Avalonia.Enums;
@@ -134,7 +135,7 @@ private async Task BrowseInputAsync()
134135
{
135136
var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions() { AllowMultiple = false, Title = "Choose dataset images directory" });
136137
if (result.Count > 0 && result[0].TryGetUri(out Uri? directory))
137-
InputPath = directory.AbsolutePath;
138+
InputPath = directory.LocalPath;
138139
}
139140

140141
/// <summary>
@@ -144,7 +145,7 @@ private async Task BrowseOutputAsync()
144145
{
145146
var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions() { AllowMultiple = false, Title = "Choose cropped images directory" });
146147
if (result.Count > 0 && result[0].TryGetUri(out Uri? directory))
147-
OutputPath = directory.AbsolutePath;
148+
OutputPath = directory.LocalPath;
148149
}
149150

150151
/// <summary>
@@ -162,78 +163,113 @@ private async Task RefreshImagePreviewsAsync()
162163
int column = 0;
163164
int row = 0;
164165
int margin = 5;
165-
// iterate all files in the input directory
166-
foreach (string file in Directory.GetFiles(InputPath!))
167-
{
168-
string ext = Path.GetExtension(file).ToLower();
169-
if (ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".bmp") // only process images
166+
await Task.Run(async () =>
167+
{
168+
// iterate all files in the input directory
169+
var filePaths = Directory.GetFiles(InputPath!, "*.jpg")
170+
.Concat(Directory.GetFiles(InputPath!, "*.jpeg"))
171+
.Concat(Directory.GetFiles(InputPath!, "*.png"))
172+
.Concat(Directory.GetFiles(InputPath!, "*.bmp"));
173+
var loadTasks = filePaths.Select(async file =>
170174
{
171-
// for each image file, create a grid that contains an image and a drag panel, and add it to the previews list
172-
Grid container = new();
173-
container.Width = previewWidth;
174-
container.Height = previewHeight;
175-
container.Margin = new Thickness(column * (previewWidth + margin), row * (previewWidth + margin), 0, 0);
176-
container.HorizontalAlignment = HorizontalAlignment.Left;
177-
container.VerticalAlignment = VerticalAlignment.Top;
178-
container.Background = new SolidColorBrush(Avalonia.Media.Color.FromRgb(0, 0, 0), 0.1);
179-
180-
var bitmap = new Bitmap(file);
181-
// for each image, ensure the drag panel is not in any way bigger than the visible area of the scaled down image preview
182-
if (!ValidateCropPanelSize(bitmap.Size.Width, bitmap.Size.Height, cropWidth, cropHeight, previewWidth, previewHeight))
183-
{
184-
await MessageBoxManager.GetMessageBoxStandardWindow("Error!", "Specified crop size exceeds the bounds of the scaled image!" + Environment.NewLine + file, ButtonEnum.Ok, MessageBox.Avalonia.Enums.Icon.Error).ShowDialog(this);
185-
ClearImagePreviews();
186-
return;
187-
}
188-
Avalonia.Controls.Image image = new();
189-
image.Source = bitmap;
190-
image.Width = previewWidth;
191-
image.Height = previewHeight;
192-
image.Margin = new Thickness(0);
193-
image.HorizontalAlignment = HorizontalAlignment.Left;
194-
image.VerticalAlignment = VerticalAlignment.Top;
195-
image.Tag = file; // store the path of the original image file
196-
ToolTip.SetTip(image, file);
197-
container.Children.Add(image);
198-
199-
Panel dragPanel = new();
200-
if (usesOriginalScaleSizes)
175+
using (var tempImage = await SixLabors.ImageSharp.Image.LoadAsync(file))
201176
{
202-
// calculate the uniform scaling factor based on the largest dimension
203-
double scaleFactor = Math.Max(bitmap.Size.Width / previewWidth, bitmap.Size.Height / previewHeight);
204-
// scale down the crop size and position uniformly
205-
dragPanel.Width = cropWidth / scaleFactor;
206-
dragPanel.Height = cropHeight / scaleFactor;
207-
dragPanel.Margin = new Thickness(cropX / scaleFactor, cropY / scaleFactor, 0, 0);
177+
var originalSize = new Avalonia.Size(tempImage.Width, tempImage.Height);
178+
var resizedImage = await LoadResizedImageAsync(tempImage, previewWidth, previewHeight);
179+
return new { FilePath = file, Image = resizedImage, OriginalSize = originalSize };
208180
}
209-
else
210-
{
211-
dragPanel.Width = cropWidth;
212-
dragPanel.Height = cropHeight;
213-
dragPanel.Margin = new Thickness(cropX, cropY, 0, 0);
214-
}
215-
dragPanel.HorizontalAlignment = HorizontalAlignment.Left;
216-
dragPanel.VerticalAlignment = VerticalAlignment.Top;
217-
dragPanel.Background = new SolidColorBrush(Avalonia.Media.Color.FromRgb(255, 255, 255), 0.3);
218-
dragPanel.Cursor = new Cursor(StandardCursorType.SizeAll);
219-
dragPanel.PointerMoved += DragPanel_PointerMoved; // subscribe the event handlers used for dragging
220-
dragPanel.PointerPressed += DragPanel_PointerPressed;
221-
dragPanel.PointerReleased += DragPanel_PointerReleased;
222-
dragPanel.Tag = true; // true = "selected" (will be cropped), false = "deselected" (will be ignored when cropping)
223-
224-
container.Children.Add(dragPanel);
225-
grdImages.Children.Add(container);
226-
227-
// increment column until the remaining horizontal space can no longer fit a whole image preview, then reset it and increment the row
228-
if ((column + 2) * (previewWidth + margin) < Width - 12) // 12: 6 pixels for margin on each side for the images dragPanel
229-
column++;
230-
else
181+
});
182+
183+
var loadedImages = await Task.WhenAll(loadTasks);
184+
foreach (var loadedImage in loadedImages)
185+
{
186+
await Dispatcher.UIThread.InvokeAsync(async () =>
231187
{
232-
column = 0;
233-
row++;
234-
}
188+
// for each image file, create a grid that contains an image and a drag panel, and add it to the previews list
189+
Grid container = new();
190+
container.Width = previewWidth;
191+
container.Height = previewHeight;
192+
container.Margin = new Thickness(column * (previewWidth + margin), row * (previewHeight + margin), 0, 0);
193+
container.HorizontalAlignment = HorizontalAlignment.Left;
194+
container.VerticalAlignment = VerticalAlignment.Top;
195+
container.Background = new SolidColorBrush(Avalonia.Media.Color.FromRgb(0, 0, 0), 0.1);
196+
// for each image, ensure the drag panel is not in any way bigger than the visible area of the scaled down image preview
197+
if (!ValidateCropPanelSize(loadedImage.OriginalSize.Width, loadedImage.OriginalSize.Height, cropWidth, cropHeight, previewWidth, previewHeight))
198+
{
199+
await MessageBoxManager.GetMessageBoxStandardWindow("Error!", "Specified crop size exceeds the bounds of the scaled image!" + Environment.NewLine + loadedImage.FilePath, ButtonEnum.Ok, MessageBox.Avalonia.Enums.Icon.Error).ShowDialog(this);
200+
ClearImagePreviews();
201+
return;
202+
}
203+
Avalonia.Controls.Image image = new();
204+
//image.Source = bitmap;
205+
image.Width = previewWidth;
206+
image.Height = previewHeight;
207+
image.Source = loadedImage.Image;
208+
image.Margin = new Thickness(0);
209+
image.HorizontalAlignment = HorizontalAlignment.Left;
210+
image.VerticalAlignment = VerticalAlignment.Top;
211+
image.Tag = loadedImage.FilePath; // store the path of the original image file
212+
ToolTip.SetTip(image, loadedImage.FilePath);
213+
container.Children.Add(image);
214+
215+
Panel dragPanel = new();
216+
if (usesOriginalScaleSizes)
217+
{
218+
// calculate the uniform scaling factor based on the largest dimension
219+
double scaleFactor = Math.Max(loadedImage.OriginalSize.Width / previewWidth, loadedImage.OriginalSize.Height / previewHeight);
220+
// scale down the crop size and position uniformly
221+
dragPanel.Width = cropWidth / scaleFactor;
222+
dragPanel.Height = cropHeight / scaleFactor;
223+
dragPanel.Margin = new Thickness(cropX / scaleFactor, cropY / scaleFactor, 0, 0);
224+
}
225+
else
226+
{
227+
dragPanel.Width = cropWidth;
228+
dragPanel.Height = cropHeight;
229+
dragPanel.Margin = new Thickness(cropX, cropY, 0, 0);
230+
}
231+
dragPanel.HorizontalAlignment = HorizontalAlignment.Left;
232+
dragPanel.VerticalAlignment = VerticalAlignment.Top;
233+
dragPanel.Background = new SolidColorBrush(Avalonia.Media.Color.FromRgb(255, 255, 255), 0.3);
234+
dragPanel.Cursor = new Cursor(StandardCursorType.SizeAll);
235+
dragPanel.PointerMoved += DragPanel_PointerMoved; // subscribe the event handlers used for dragging
236+
dragPanel.PointerPressed += DragPanel_PointerPressed;
237+
dragPanel.PointerReleased += DragPanel_PointerReleased;
238+
dragPanel.Tag = true; // true = "selected" (will be cropped), false = "deselected" (will be ignored when cropping)
239+
240+
container.Children.Add(dragPanel);
241+
grdImages.Children.Add(container);
242+
243+
// increment column until the remaining horizontal space can no longer fit a whole image preview, then reset it and increment the row
244+
if ((column + 2) * (previewWidth + margin) < Width - 12) // 12: 6 pixels for margin on each side for the images dragPanel
245+
column++;
246+
else
247+
{
248+
column = 0;
249+
row++;
250+
}
251+
});
235252
}
236-
}
253+
});
254+
}
255+
256+
/// <summary>
257+
/// Loads an image and returns a scaled down version of it, as Bitmap
258+
/// </summary>
259+
/// <param name="image">The image to load</param>
260+
/// <param name="targetWidth">The width of the scaled down bitmap</param>
261+
/// <param name="targetHeight">The height of the scaled down bitmap</param>
262+
/// <returns>A scaled down bitmap of the original image</returns>
263+
public static async Task<Bitmap> LoadResizedImageAsync(SixLabors.ImageSharp.Image image, int targetWidth, int targetHeight)
264+
{
265+
// Calculate scale ratio to maintain aspect ratio
266+
var scale = Math.Min(targetWidth / (float)image.Width, targetHeight / (float)image.Height);
267+
268+
image.Mutate(x => x.Resize((int)(image.Width * scale), (int)(image.Height * scale)));
269+
var memoryStream = new MemoryStream();
270+
await image.SaveAsBmpAsync(memoryStream);
271+
memoryStream.Seek(0, SeekOrigin.Begin);
272+
return new Bitmap(memoryStream);
237273
}
238274

239275
/// <summary>

0 commit comments

Comments
 (0)