Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/dotnet-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,19 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0

# Install the .NET workload
- name: Install .NET
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v4
with:
dotnet-version: 6.0.x
dotnet-version: 9.0.x

# Add MSBuild to the PATH: https://github.com/microsoft/setup-msbuild
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@2008f912f56e61277eefaac6d1888b750582aa16
uses: microsoft/setup-msbuild@v2
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'

Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2020 Alan Barr
Copyright (c) 2020-2024 Alan Barr

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
27 changes: 14 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Image Processing Library
This image processing library is a lightweight open source library targeting **.NET Standard v2.0**.
This image processing library is a lightweight open source library targeting [.NET Standard v2.0](https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0).

This library may be used as an educational tool on how such image processing methods can be implemented, or used within your own projects that require some form of image processing.

Expand All @@ -8,33 +8,34 @@ See the appropriate section for details on each image processing class and their
You are welcome to use/update this software under the terms of the **MIT license**.

Notes:
1. **.NET Standard** libraries can be used in **.NET Full Framework** and **.NET Core** (now **.NET 6.0**) projects.
1. **.NET Standard v2.0** libraries can be used in **.NET Full Framework** and **.NET Core** (now known simply as **.NET**) projects.
2. The image processing library is only supported on **Windows OS** due to the dependency on Microsoft's [System.Drawing.Common](https://www.nuget.org/packages/System.Drawing.Common) NuGet package.
<br />

## Release History
The published package is available for download on [nuget.org](https://www.nuget.org/packages/Freedom35.ImageProcessing).
|Date|Version|Release Notes|
|:---|:---:|:----|
|2022/06/16|[1.4.1](https://www.nuget.org/packages/Freedom35.ImageProcessing/1.4.1)|Updated System.Drawing.Common package dependency to v6.0.0.|
|2021/07/30|[1.4.0](https://www.nuget.org/packages/Freedom35.ImageProcessing/1.4.0)|Added overload methods for converting color images to grayscale.<br />Added overload methods for converting images to black & white.|
|2021/07/28|[1.3.0](https://www.nuget.org/packages/Freedom35.ImageProcessing/1.3.0)|Added support for applying sepia filter to images.|
|2021/07/08|[1.2.0](https://www.nuget.org/packages/Freedom35.ImageProcessing/1.2.0)|Fixed issue with stride padded images causing an *index out of bounds* exception when enhancing contrast.<br />Fixed issue with RGB color filters not working correctly for images with stride padding and alpha bytes.<br />Fixed issue with processing images with stride padding and alpha bytes.<br />Fixed issue where max threshold value was not correctly applied to the red (RGB) byte for color images.<br />Revision to combine images using bitwise OR.|
|2021/03/24|[1.1.0](https://www.nuget.org/packages/Freedom35.ImageProcessing/1.1.0)|Added support for applying EXIF orientation data to images.|
|2021/01/26|[1.0.2](https://www.nuget.org/packages/Freedom35.ImageProcessing/1.0.2)|Revision to return rounded value for ImageBytes.GetAverageValue.|
|2021/01/25|[1.0.1](https://www.nuget.org/packages/Freedom35.ImageProcessing/1.0.1)|Revision to add enum description attribute for 'Mexican Hat' smoothing filter.|
|2020/10/02|[1.0.0](https://www.nuget.org/packages/Freedom35.ImageProcessing/1.0.0)|Initial release.|
|2024-12-28|[1.5.0](https://www.nuget.org/packages/Freedom35.ImageProcessing/1.5.0)|Updated System.Drawing.Common package dependency to v9.0.0.|
|2022-06-16|[1.4.1](https://www.nuget.org/packages/Freedom35.ImageProcessing/1.4.1)|Updated System.Drawing.Common package dependency to v6.0.0.|
|2021-07-30|[1.4.0](https://www.nuget.org/packages/Freedom35.ImageProcessing/1.4.0)|Added overload methods for converting color images to grayscale.<br />Added overload methods for converting images to black & white.|
|2021-07-28|[1.3.0](https://www.nuget.org/packages/Freedom35.ImageProcessing/1.3.0)|Added support for applying sepia filter to images.|
|2021-07-08|[1.2.0](https://www.nuget.org/packages/Freedom35.ImageProcessing/1.2.0)|Fixed issue with stride padded images causing an *index out of bounds* exception when enhancing contrast.<br />Fixed issue with RGB color filters not working correctly for images with stride padding and alpha bytes.<br />Fixed issue with processing images with stride padding and alpha bytes.<br />Fixed issue where max threshold value was not correctly applied to the red (RGB) byte for color images.<br />Revision to combine images using bitwise OR.|
|2021-03-24|[1.1.0](https://www.nuget.org/packages/Freedom35.ImageProcessing/1.1.0)|Added support for applying EXIF orientation data to images.|
|2021-01-26|[1.0.2](https://www.nuget.org/packages/Freedom35.ImageProcessing/1.0.2)|Revision to return rounded value for ImageBytes.GetAverageValue.|
|2021-01-25|[1.0.1](https://www.nuget.org/packages/Freedom35.ImageProcessing/1.0.1)|Revision to add enum description attribute for 'Mexican Hat' smoothing filter.|
|2020-10-02|[1.0.0](https://www.nuget.org/packages/Freedom35.ImageProcessing/1.0.0)|Initial release.|

<br />
<br />

# Sample Solutions
The repository contains some sample [Visual Studio](https://visualstudio.microsoft.com) solutions described below.
The repository contains some [Visual Studio](https://visualstudio.microsoft.com) solutions described below.

|Name|Description|
|-----|-----|
|Freedom35.ImageProcessing.sln|Base solution containing image processing library and unit tests.|
|Freedom35.ImageProcessing.WindowsDesktop.sln|Extended solution containing projects from base solution, plus an Image Viewer app for Windows desktop.|
|Freedom35.ImageProcessing.WindowsDesktop.sln|Extended solution containing projects from base solution, plus an **Image Viewer app** for Windows desktop.|

<br />
<br />
Expand All @@ -52,7 +53,7 @@ The **Bitmap** class used is **System.Drawing.Bitmap**.
## Usage in Projects
Note: Examples are in C#, but the library may also be used in other .NET language projects.

1. Add the **Image Processing Library** NuGet package to your .NET project.
1. Add the [Image Processing Library](https://www.nuget.org/packages/Freedom35.ImageProcessing) NuGet package to your .NET project.

2. Include the namespace for the Image Processing Library at the top of your code file.
```csharp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Description>.NET Standard library for image processing.</Description>
<Authors>Alan Barr (GitHub: freedom35)</Authors>
<Company>Alan Barr (GitHub: freedom35)</Company>
<Copyright>Copyright © Alan Barr 2022</Copyright>
<Copyright>Copyright © 2022-2024 Alan Barr</Copyright>
<RepositoryUrl>https://github.com/freedom35/image-processing</RepositoryUrl>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
Expand All @@ -15,11 +15,11 @@
<RepositoryType>git</RepositoryType>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>icon.png</PackageIcon>
<Version>1.4.1</Version>
<Version>1.5.0</Version>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
<PackageReference Include="System.Drawing.Common" Version="9.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions src/Freedom35.ImageProcessing/ImageConvolution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,15 @@ public static Bitmap ApplyKernelsThenCombine(Bitmap bitmap, params ConvolutionTy
}

// Combine to create a new image
bitmap = ImageCombine.All(bitmaps);
Bitmap combinedBitmap = ImageCombine.All(bitmaps);

// No longer needed
foreach (Bitmap bmp in bitmaps)
{
bmp.Dispose();
}

return bitmap;
return combinedBitmap;
}

/// <summary>
Expand Down
7 changes: 1 addition & 6 deletions src/Freedom35.ImageProcessing/ImageFormatting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,7 @@ public static Image ToJPEG(Image image, long compressionLevel)
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();

// Find JPEG codec
ImageCodecInfo jpegEncoder = codecs.FirstOrDefault(c => c.FormatID == ImageFormat.Jpeg.Guid);

if (jpegEncoder == null)
{
throw new Exception("JPEG image encoder not found.");
}
ImageCodecInfo jpegEncoder = codecs.FirstOrDefault(c => c.FormatID == ImageFormat.Jpeg.Guid) ?? throw new Exception("JPEG image encoder not found.");

// Create compression parameters
EncoderParameters encParams = new EncoderParameters(1);
Expand Down
17 changes: 10 additions & 7 deletions src/ImageViewerApp/EnumConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Reflection;

using Freedom35.ImageProcessing;
using System.Diagnostics.CodeAnalysis;

namespace ImageViewerApp
{
Expand Down Expand Up @@ -35,20 +36,22 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu
throw new NotSupportedException();
}

public static T GetValueFromDescription<T>(string description) where T : Enum
public static bool TryGetValueFromDescription<T>(string description, [NotNullWhen(true)] out T? enumValue) where T : Enum
{
FieldInfo[] fields = typeof(T).GetFields();

FieldInfo field = fields.FirstOrDefault(f => Attribute.GetCustomAttribute(f, typeof(DescriptionAttribute)) is DescriptionAttribute attr && attr.Description == description);
// Note: Matching enum description attribute (not name)
FieldInfo? field = fields.FirstOrDefault(f => Attribute.GetCustomAttribute(f, typeof(DescriptionAttribute)) is DescriptionAttribute attr && attr.Description == description);

if (field != null)
{
return (T)field.GetValue(null);
}
else
{
return default;
enumValue = (T?)field.GetValue(null);
return enumValue != null;
}

// Must init
enumValue = default;
return false;
}
}
}
8 changes: 4 additions & 4 deletions src/ImageViewerApp/ImageConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace ImageViewerApp
[ValueConversion(typeof(Image), typeof(ImageSource))]
internal sealed class ImageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Image img)
{
Expand All @@ -33,7 +33,7 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu
throw new NotSupportedException();
}

public static BitmapImage ConvertImageToBitmapSource(Image image)
public static BitmapImage? ConvertImageToBitmapSource(Image image)
{
// First convert to stream
using MemoryStream stream = new();
Expand All @@ -42,9 +42,9 @@ public static BitmapImage ConvertImageToBitmapSource(Image image)
return ConvertStreamToBitmapSource(stream);
}

public static BitmapImage ConvertStreamToBitmapSource(MemoryStream stream)
public static BitmapImage? ConvertStreamToBitmapSource(MemoryStream stream)
{
BitmapImage bmp = null;
BitmapImage? bmp = null;

if (stream != null && stream.CanSeek && stream.CanRead)
{
Expand Down
5 changes: 3 additions & 2 deletions src/ImageViewerApp/ImageViewerApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Description>.NET Core Windows app for demonstrating image processing functions.</Description>
<Authors>Alan Barr (GitHub: freedom35)</Authors>
<PackageLicenseFile></PackageLicenseFile>
<RepositoryUrl>https://github.com/freedom35/image-processing</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Version>1.6.0</Version>
<Version>2.0.0</Version>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
Expand Down
57 changes: 32 additions & 25 deletions src/ImageViewerApp/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
using System;
using System.Drawing;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Drawing.Imaging;
using System.Linq;

Expand All @@ -23,13 +21,13 @@ public MainWindow()

private const string SaveFileFilter = "Bitmap|*.bmp|JPEG|*.jpg|PNG|*.png|TIFF|*.tif";

private Image originalImage = null;
private Image currentImage = null;
private Image previousImage= null;
private Image? originalImage = null;
private Image? currentImage = null;
private Image? previousImage= null;

private System.Windows.Point zoomStartPoint = new(-1, -1);

private System.Windows.Input.Cursor defaultCursor = null;
private System.Windows.Input.Cursor? defaultCursor = null;

#endregion

Expand All @@ -49,14 +47,17 @@ private void Window_Loaded(object sender, EventArgs e)
{
// Check command line args for any 'Open With...' images.
// (First arg will be app path)
string imageName = Environment.GetCommandLineArgs().Skip(1).FirstOrDefault();
string? imageName = Environment.GetCommandLineArgs().Skip(1).FirstOrDefault();

string[] SupportedImageTypes = { "bmp", "jpg", "png", "tif" };

// Open image if valid file type
if (!string.IsNullOrEmpty(imageName) && SupportedImageTypes.Any(t => imageName.EndsWith(t, StringComparison.OrdinalIgnoreCase)))
if (!string.IsNullOrEmpty(imageName))
{
OpenImage(imageName);
string[] SupportedImageTypes = ["bmp", "jpg", "png", "tif"];

// Open image if valid file type
if (SupportedImageTypes.Any(t => imageName.EndsWith(t, StringComparison.OrdinalIgnoreCase)))
{
OpenImage(imageName);
}
}
}

Expand Down Expand Up @@ -192,8 +193,8 @@ private static void DisplayHistogram(Image sourceImage, System.Windows.Controls.
// Create histogram of image
Image histogram = ImageHistogram.Create(sourceImage,
new System.Drawing.Size((int)targetPictureBox.DesiredSize.Width - 20, (int)targetPictureBox.DesiredSize.Height),
System.Drawing.Color.DodgerBlue,
System.Drawing.Color.White);
Color.DodgerBlue,
Color.White);

// Display histogram
targetPictureBox.Source = ImageConverter.ConvertImageToBitmapSource(histogram);
Expand Down Expand Up @@ -258,7 +259,7 @@ private void Button_RestoreImage_Click(object sender, RoutedEventArgs e)
}
}

private void Button_UndoImageChange_Click(object sender, RoutedEventArgs e)
private void Button_UndoImageChange_Click(object? sender, RoutedEventArgs? e)
{
if (previousImage != null)
{
Expand All @@ -268,19 +269,25 @@ private void Button_UndoImageChange_Click(object sender, RoutedEventArgs e)

private void Button_ApplyConvolution_Click(object sender, RoutedEventArgs e)
{
if (currentImage != null && cmbConvolution.SelectedIndex > -1)
// Check an image is loaded
if (currentImage == null)
{
string strValue = cmbConvolution.SelectedItem as string;

ConvolutionType type = EnumConverter.GetValueFromDescription<ConvolutionType>(strValue);
return;
}

try
{
DisplayImage(ImageConvolution.ApplyKernel(currentImage, type));
}
catch (Exception ex)
// Check a valid filter is selected
if (cmbConvolution.SelectedIndex > -1 && cmbConvolution.SelectedItem is string strValue)
{
if (EnumConverter.TryGetValueFromDescription(strValue, out ConvolutionType type))
{
ReportException(ex);
try
{
DisplayImage(ImageConvolution.ApplyKernel(currentImage, type));
}
catch (Exception ex)
{
ReportException(ex);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<IsPackable>false</IsPackable>
<Company>Alan Barr (GitHub: freedom35)</Company>
<Authors>Alan Barr (GitHub: freedom35)</Authors>
<Description>Unit tests for Freedom35.ImageProcessing.dll.</Description>
<Copyright>Copyright © Alan Barr</Copyright>
<Copyright>Copyright © 2022-2024 Alan Barr</Copyright>
<RepositoryUrl>https://github.com/freedom35/image-processing</RepositoryUrl>
<Version>1.5.0</Version>
<Version>2.0.0</Version>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
Expand All @@ -30,9 +31,9 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.7.0" />
<PackageReference Include="MSTest.TestFramework" Version="3.7.0" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion tests/Freedom35.ImageProcessing.Tests/TestImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ static class TestImage
public static Image FromResource(string resourcePath)
{
// Keep stream open for processing
Stream resourceStream = System.Reflection.Assembly.GetCallingAssembly().GetManifestResourceStream(resourcePath);
Stream? resourceStream = System.Reflection.Assembly.GetCallingAssembly().GetManifestResourceStream(resourcePath) ?? throw new FileNotFoundException($"Unable to load resource: {resourcePath}");

return Image.FromStream(resourceStream);
}
Expand Down
4 changes: 1 addition & 3 deletions tests/Freedom35.ImageProcessing.Tests/TestImageBinary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,13 @@ public void TestAsBytes(string sourceResourcePath)
// Load source image
using Image sourceImage = TestImage.FromResource(sourceResourcePath);

Assert.IsNotNull(sourceImage);

const byte Threshold = 0x7F;

// Get bytes for image
byte[] imageBytes = ImageBinary.AsBytes(sourceImage, Threshold);

// Check we have some bytes
Assert.IsNotNull(imageBytes);
Assert.IsTrue(imageBytes.Length > 0);

// Check all converted to binary (0 or 1)
Assert.IsTrue(imageBytes.All(b => b < 0x02));
Expand Down
Loading