Disclaimer: The software provided in this repository was developed without the use of generative AI. Generative AI may only be used to verify grammatical correctness and syntax.
The application was originally developed as an R&D project between 2017 and 2019.
The original purpose was to investigate the potential advantages of optimizing the contrast of grayscale images using a normal distribution compared with a uniform distribution. Two parameters - the expectation and standard derivation - allow to the relative luminance and contrast, respectively, to be controlled.
Microkernel Guide&Demo for WPF (SDI), Winforms (MDI/SDI/TDI) and Console processes
- Overview
- Microkernel
- Application
- Integration tests
- Managing Grayscale Images
- Created With
- Benchmarks
- NuGet
The application consists of a desktop window with a main viewport implemented as a PictureBox where the sequentially transformed images are rendered. The menu on the top allows users to generate views, which are appended to the bottom. The toolbar below the menu allows to undo or redo transformations, restore the originally loaded image, and capture a custom area within the viewport. Two trackbars apply rotation or scaling transformations without rendering an image.
Each generated view produces a tab below the viewport, allowing users to apply algorithms or manage related parameters.
Fig. 1 - The main view and transient/singleton views are displayed as tabs. The opened affine transformation tab is a transient view. The settings tab is a singleton view. The frame is taken from the "Thomas the Tank Engine" series and processed on CPU using the following algorithm chain: Grayscale->Inversion->Laplacian Operator 5x5->Inversion->Shear Rotation 20°->Bicubic Interpolation (0.2, 0.2)->Cyclic Translation (20, 20).
The project is built around two major namespaces: Microkernel and App . As discussed below, this hierarchy allows an application to scale vertically, from views to presenters to service/provider/factory components, and horizontally, by adding subsystems within these categories. Decoupling the UI does not limit the application to static forms; it also allows to apply XML/XAML-based interfaces or console applications to use the same set of event handlers and domain modules. Additionally, as demonstrated in this application, the hierarchy supports comprehensive integration testing of any component, including the verification of incoming arguments and function calls.
Fig. 2 Hierarchy of modules.
Following the microkernel concept, the first step is to introduce a registry that manages the lifecycle of its components and resolves them on demand. In a programming language, a dependency injection (DI) container can serve as this registry, mapping specific implementations to their interfaces or base classes. Since multiple container implementations may be available, the namespace introduces IContainerAdapter, which provides access to the APIs of the underlying microkernel implementations.
Furthermore, the module provides a fluent IComponentProvider builder that allows clients to register components at the application level within the IStartup implementation. The central component responsible for managing the execution flow within the engine is the IAppController interface, which is exposed as a singleton through a static property. The application controller composes the IComponentProvider and IEventAggregator, which connects presenters and views and provides a mechanism for publishing domain events through unicast and broadcast messaging.
To build a fluent workflow with desktop forms, it is convenient to provide a common routing mechanism. The router acts as a generator, resolving the dependency tree for a specific component.
The first generator is built around the BasePresenter<TView, TViewModel> abstract class. It provides access to the UI layer through the IView virtual property and allows a concrete presenter to be executed via the Run method. To navigate to a presenter, the IComponentProvider checks whether a transient instance is registered in the DI container and, if not, registers it. Afterwards, it resolves a new presenter and executes it through the relevant method call.
The second generator is ILoggerFactory from the Microsoft.Extensions.Logging.Abstractions namespace, which creates logging categories for transient presenters. It follows a similar approach, generating an ILogger<TPresenter> instance through the factory interface and registering it as a transient service in the container.
It is possible to control the scope of a view. The UI module provides a second Startup instance, which allows a static or XML/XAML form to be linked to the interface declared at the presentation level. When the View property is accessed for the first time, the form is elevated to the presentation level through its interface, providing access to UI components at the presenter level.
At the UI level, the IEventAggregator instance is accessed through the static AppController property, allowing a view to unsubscribe when Dispose or Hide is called, depending on whether the view has a transient or singleton scope.
If a view has a singleton scope, it is possible to associate multiple presenters with it. The virtual Run method can be used to compose multiple presenters through calls to the base implementation, allowing them to subscribe to the same view:
Fig. 3 Mapping several presenters to a singleton view via the Run composition.
The application is divided into three layers: UI, presentation, and domain.
Most forms in the UI module are reduced to passive views, with minimal behavior implemented. The base form exposes IEventAggregator, IAppController, and ApplicationContext. Additionally, it provides two generic methods for reading from and writing to UI components while taking the synchronization context of the current thread into account. Fields and events generated by the designer are exposed through public properties and subscribed to the event aggregator within the corresponding service.
The presentation layer prolongs the synchronization context from the UI module to the handlers. The views used to communicate with a form are combined through interface inheritance, and the PublishFromAll method of the event aggregator allows messages to be sent from one presenter to another.
The render is built using the MessageLoop.Common module and composes the long-running operation and messaging services. The domain services injected into the presenter are used to create a render block containing a chain of closures. The block is published through the event aggregator to the main form, where it is registered for rendering. The dedicated channel reads incoming tasks, starts them, and awaits their completion one by one.
Initially, a group of underexposed images was chosen for experimental puproses.
Fig. 4 - The original underexposed image.
After optimization using a uniform distribution, there is redundancy in bright areas of the relative luminance. However, using a normal distribution it's possible to minimize this effect and achieve better detail distinctiveness.
Fig. 5 - Histogram transformation using a uniform distribution.
Fig. 6 - Histogram transformation using a normal distribution, where µ = 90 and σ = 60.
To determine which image has better contrast, one may use the definition of conditional variance:
where [z1, z2] is an interval of relative luminance.
By splitting the interval [0, 255] to 16 subintervals, we can use the definition above. Since contrast is defined as statistical scattering, conditional variance can be used to measure the level of contrast within each specified interval.
Fig. 7 - Using the definition of conditional variance over 16 intervals of relative luminance.
Thus, one may conclude that a normal distribution may produce better results than a uniform distribution for a group of underexposed images.
Serilog, Microsoft.Extensions.Logging
Microsoft.Extensions.Configuration







