Wednesday, October 25, 2023

Accessing CodeProject's AI Explorer from WPF

I got an email from CodeProject about their AI Explorer that can be hosted locally.

https://www.codeproject.com/Articles/5322557/CodeProject-AI-Server-AI-the-easy-way

It took about an hour to install it and not all the modules are usable and useful. Nevertheless, the modules that are useful are interesting. It comes with a well designed dashboard that lets you interact with the AI services.

Once you have installed this and installed the scene classification module you will see Scene Classification as it looks below. 


These services can also be called from your code. I wanted to write a simple WPF application that could classify images as you can see above. My application won't be as pretty, but it will be mine.

Start a new C#, .Net Core Visual Studio project and call it SceneClassifier.

It will let the user select an image, and when they do it will display the image and call the SceneClassifier AI service. It will then display the scene classification and confidence.

Here's the XAML for MainWindow.xaml. There's nothing clever here. It's all in the code.

<Window x:Class="SceneClassifier.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:SceneClassifier"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        SizeToContent="WidthAndHeight"
        Title="Scene Classifier" Height="450" Width="800">
    <Window.Resources>
        <RoutedCommand x:Key="SelectCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource SelectCommand}" Executed="Select_Executed"/>
    </Window.CommandBindings>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
 
        <Button Grid.Row="0" Grid.Column="0" Content="Select Image" Command="{StaticResource SelectCommand}"/>
        <TextBlock Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding FileName}"/>
 
        <Image Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="4" Height="200" Source="{Binding FileName}"/>
 
        <TextBlock Grid.Row="2" Grid.Column="0" Text="Classification:"/>
        <TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Classification}"/>
 
        <TextBlock Grid.Row="2" Grid.Column="2" Text="Confidence"/>
        <TextBlock Grid.Row="2" Grid.Column="3" Text="{Binding Confidence}"/>
       
        <TextBlock Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="4" Text="{Binding Status}"/>
    </Grid>
</Window>

-------------------------------------------------------------------------------------

The code uses an HttpClient to access the REST service provided by CodeProject's AI server and deserializes the result into an object. The we pull the classification and confidence out of the object. Yes, I know the architecture sucks - this isn't a post about code purity :-)

using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Win32;
using System;
using System.Net.Http;
using System.Windows;
using System.Windows.Input;
using System.Net.Http.Json;
 
namespace SceneClassifier;
public class cResult
{
    public bool success { get; set; }
    public string label { get; set; }
    public float confidence { get; set; }
    public string command { get; set; }
    public string moduleId { get; set; }
    public string executionProvider { get; set; }
    public bool canUseGPU { get; set; }
    public int inferenceMs { get; set; }
    public int processMs { get; set; }
    public int analysisRoundTripMs { get; set; }
}
 
[ObservableObject]
public partial class MainWindow : Window
{
    [ObservableProperty] String _FileName = "";
    [ObservableProperty] String _Status = "";
    [ObservableProperty] string _Classification;
    [ObservableProperty] String _Confidence;
 
    HttpClient httpClient = new HttpClient();
 
    public MainWindow()
    {
        InitializeComponent();
    }
 
    private void Select_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        OpenFileDialog openFileDialog = new OpenFileDialog();
        openFileDialog.Filter = "png Files|*.png";
        openFileDialog.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
        if (openFileDialog.ShowDialog().Value)
        {
            FileName = openFileDialog.FileName;
            ClassifyScene(FileName);
        }
    }
 
    private async void ClassifyScene(string FileName)
    {
        Byte[] imageBytes = System.IO.File.ReadAllBytes(FileName);
        ByteArrayContent content = new ByteArrayContent(imageBytes);
        content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("image/png");
 
        MultipartFormDataContent form = new MultipartFormDataContent();
        form.Add(content, "image", "image.png");
 
        HttpResponseMessage response = await httpClient.PostAsync("http://localhost:32168/v1/vision/scene", form);
        if (response.IsSuccessStatusCode)
        {
            cResult result = await response.Content.ReadFromJsonAsync<cResult>();
            Classification = result.label;
            Confidence = Math.Round(result.confidence * 100).ToString() + "%";
            Status = "Success";
        }
        else
        {
            string Error = await response.Content.ReadAsStringAsync();
            Status = $"Error: {response.StatusCode}:{Error}";
        }
    }
}



 


Tuesday, October 24, 2023

Deferring TabItem creation

Let's suppose you have a TabControl with several TabItems. The definition might look like this.

<Window x:Class="DeferredTabItemContentCreation.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DeferredTabItemContentCreation"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <local:Search x:Key="Search"/>
        <local:Add x:Key="Add"/>
        <local:Edit x:Key="Edit"/>
    </Window.Resources>
    <Grid>
        <TabControl SelectionChanged="TabControl_SelectionChanged">
            <TabItem Header="Search" Content="{StaticResource Search}"/>
            <TabItem Header="Add" Content="{StaticResource Add}"/>
            <TabItem Header="Edit" Content="{StaticResource Edit}"/>
        </TabControl>
    </Grid>
</Window>

The problem here is that when this window is loaded the constructors for Search, Add, and Edit are all called. You only really need the Search UserControl to be created. If you could defer the creation of the Add and Edit UserControls until the user asks for them, that would speed up the initial load.

It's fairly easy to achieve this by not specifying Content for the Add and Edit tabs and adding a SelectionChanged event handler.

<Window x:Class="DeferredTabItemContentCreation.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DeferredTabItemContentCreation"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <local:Search x:Key="Search"/>
    </Window.Resources>
    <Grid>
        <TabControl SelectionChanged="TabControl_SelectionChanged">
            <TabItem Header="Search" Content="{StaticResource Search}"/>
            <TabItem Header="Add"/>
            <TabItem Header="Edit"/>
        </TabControl>
    </Grid>
</Window>

The SelectionChanged event handler detects that the TabItem we are changing to has no Content and creates it on the fly. If we have visited this tab before, we just reuse the Content we created on the first visit.

    private void TabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        TabControl? tc = (sender as TabControl);
        if (tc != null && e.AddedItems != null && e.AddedItems.Count > 0)
        {
            TabItem? ti = e.AddedItems[0] as TabItem;
            if (ti != null)
            {
                if (ti.Content == null)
                {
                    switch (ti.Header?.ToString()?.ToLower())
                    {
                        case "add": ti.Content = new Add();break;
                        case "edit":ti.Content = new Edit();break;
                    }
                }
            }
        }
    }

For more complex scenarios we could subclass the TabItem and add a class property. Then we could use Reflection CreateInstance to dynamically create the Content from the class property.