Get the Color Code from an Image Pixel

Keywords:  Xamarin.Forms, Image Class, Pan Gesture, Pinch Gesture, SkiaSharp, SKBitmap, SKBitmp.GetPixel, Slider



I developed an App to easily get the color code from a image pixel, because when I found an App with good coloring sense, I wanted to know some color codes in the App.  Getting the color from image is very common function, and there are many Apps and Webs.  But I wanted to make it by myself, I wanted to learn image data handling in Xamarin.Forms, so I have tried to do it anyway.

The screenshot just after launching this App (left), a screenshot (middle) when the image is dragged by Pan Gesture, and a screenshot (right) when the image is scaled by Pinch Gesture.  You can see a figure like a rifle sight.  The color code of the pixel on the center of the figure are shown below the image.





MainPage.xaml
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             xmlns:local="clr-namespace:ColorPicker" 
             xmlns:views="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="ColorPicker.MainPage">

    <Grid RowSpacing="0">
        
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>


        <Image x:Name="image" Source="testchart.gif" Grid.Row="1"/>

        
        <!-- Draw a Cross-Hairs to sight the pixel -->
        <views:SKCanvasView Grid.Row="1" PaintSurface="CrossHairs" 
                            InputTransparent="true" />

        <!-- Image Canvas -->
        <views:SKCanvasView x:Name="skCanvasView" Grid.Row="1" PaintSurface="OnCanvasViewPaintSurface" 
                            InputTransparent="true" SizeChanged="CanvasSizeChanged"/>

        <!-- App Title -->
        <StackLayout Grid.Row="0" BackgroundColor="Gray" Padding="0,0,0,1" HeightRequest="50">
            <StackLayout BackgroundColor="White" HeightRequest="50">
                <Label Text="Color Picker Demo" Grid.Row="0" VerticalOptions="EndAndExpand"/>
            </StackLayout>
        </StackLayout>

        <!-- Image Scale Slider -->
        <StackLayout Grid.Row="2" BackgroundColor="White" Padding="20, 0, 20, 0">
            <Slider x:Name="ScaleSlider" Minimum="0" Maximum="10" ValueChanged="ScaleSliderChanged"/>
        </StackLayout>

        <!-- Display the Color of the center pixel of the skCanvasView -->
        <StackLayout Grid.Row="3" BackgroundColor="White" Orientation="Horizontal" Padding="20,5,0,5">
            <StackLayout BackgroundColor="Gray" WidthRequest="80" HeightRequest="80" Padding="2">
                <Grid>
                    <BoxView x:Name="ColorMonitor" WidthRequest="80" HeightRequest="80"/>
                    <Label x:Name="ColorCode" FontSize="16" VerticalOptions="Center" HorizontalOptions="Center"/>
                </Grid>
            </StackLayout>
            <StackLayout>
                <Label x:Name="Red" Text="R: "/>
                <Label x:Name="Green" Text="G: "/>
                <Label x:Name="Blue" Text="B: "/>
            </StackLayout>
        </StackLayout>
        
    </Grid>
    
</ContentPage>
(The source code is also here)

A Grid with five rows and one columns are on the whole screen.  This App's title in the first row (Grid.Row=0).  In the second row (Grid.Row=1), the image data, the figure like a rifle sight indicating the pixel which color code should be gotten, and SkiaSharp canvas explained later, are placed.  The slider scaling the image are in the third row (Grid.Row=2).  In the final row (Grid.Row=3), the BoxView and the some Labels showing the color code are placed.

Below is the C# code-behind.

MainPage.xaml.cs
using System;
using System.IO;
using System.Reflection;

using Xamarin.Forms;

using SkiaSharp;
using SkiaSharp.Views.Forms;


namespace ColorPicker
{
    public partial class MainPage : ContentPage
    {
        string ImageFile = "ColorPicker.Media.testchart.gif";
        SKBitmap skBitmap;
        SKColor skColor;

        double x0 = 0;
        double y0 = 0;
        double width = 0;
        double height = 0;
        double currentScale = 1;
        double startScale = 1;
        double xOffset = 0;
        double yOffset = 0;
        double centerX = 0;
        double centerY = 0;
        int w0 = 0;
        int h0 = 0;
        int skScale = 2;    // for iOS

        double WidthRatio = 1;
        double HeightRatio = 1;
        bool VerticalLong = true;

        public MainPage()
        {
            InitializeComponent();


            // Add a Pan Gesture Recognizer
            var panGesture = new PanGestureRecognizer();
            panGesture.PanUpdated += OnPanUpdated;
            image.GestureRecognizers.Add(panGesture);


            // Add a Pinch Gesture Recognizer
            var pinchGesture = new PinchGestureRecognizer();
            pinchGesture.PinchUpdated += OnPinchUpdated;
            image.GestureRecognizers.Add(pinchGesture);


            // Get Image Data
            // for iOS
            Assembly assembly = GetType().GetTypeInfo().Assembly;
            using (Stream stream = assembly.GetManifestResourceStream(ImageFile))
            using (SKManagedStream skStream = new SKManagedStream(stream))
            {
                skBitmap = SKBitmap.Decode(skStream);
            }
            /*
            // for UWP
            skBitmap = new SKBitmap(100, 100, false);
            skBitmap = SKBitmap.Decode("test.png");
            */
        }


        // Detect SizeChanged of canvas and get its size
        void CanvasSizeChanged(object sender, EventArgs e)
        {
            // Check the aspect ratio of the image
            WidthRatio = skBitmap.Width / skCanvasView.Width;
            HeightRatio = skBitmap.Height / skCanvasView.Height;
            if (HeightRatio > WidthRatio) VerticalLong = true;
            else VerticalLong = false;

            // Pre-Calculate some values
            centerX = skCanvasView.Width / 2 * skScale;
            centerY = skCanvasView.Height / 2 * skScale;
            w0 = skBitmap.Width;
            h0 = skBitmap.Height;


            // Calculate the Coordinates of the image RectAngle
            if (VerticalLong)
            {
                x0 = (image.TranslationX + image.Width * image.Scale * (1 - WidthRatio / HeightRatio) / 2) * skScale;
                y0 = image.TranslationY * skScale;
                width = image.Width * image.Scale * WidthRatio / HeightRatio * skScale;
                height = image.Height * image.Scale * skScale;
            }
            else
            {
                x0 = image.TranslationX * skScale;
                y0 = (image.TranslationY + image.Height * image.Scale * (1 - HeightRatio / WidthRatio) / 2) * skScale;
                width = image.Width * image.Scale * skScale;
                height = image.Height * image.Scale * HeightRatio / WidthRatio * skScale;
            }

            // Get Color from the center pixel of the skCanvasView
            GetColor();


            // After launching App, if the ScaleSlider is changed without any PinchGesture, 
            // the image.TranslationX & Y can not be gotten correctly.
            // But the initialization below solves the problem.  I don't know why....
            image.AnchorX = 0;
            image.AnchorY = 0;
        }


        // Modified a little bit the Pan Gesture Recognizer sample program
        //  ref. https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/gestures/pan
        void OnPanUpdated(object sender, PanUpdatedEventArgs e)
        {
            switch (e.StatusType)
            {
                case GestureStatus.Running:
                    // Translate and ensure we don't pan beyond the wrapped user interface element bounds
                    image.TranslationX = xOffset + e.TotalX;
                    image.TranslationY = yOffset + e.TotalY;

                    // Clear and Update skCanvasView
                    skCanvasView.InvalidateSurface();

                    // Get Color from the center pixel of the skCanvasView
                    GetColor();

                    break;

                case GestureStatus.Completed:
                    // Store the translation applied during the pan
                    xOffset = image.TranslationX;
                    yOffset = image.TranslationY;
                    break;
            }
        }


        // Modified a little bit the Pinch Gesture Recognizer sample program
        //  ref. https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/gestures/pinch
        void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e)
        {
            if (e.Status == GestureStatus.Started)
            {
                // Store the current scale factor applied to the wrapped user interface element,
                // and zero the components for the center point of the translate transform.
                startScale = image.Scale;
                image.AnchorX = 0;
                image.AnchorY = 0;

            }
            if (e.Status == GestureStatus.Running)
            {
                // Calculate the scale factor to be applied.
                currentScale += (e.Scale - 1) * startScale;
                currentScale = Math.Max(1, currentScale);

                // The ScaleOrigin is in relative coordinates to the wrapped user interface element,
                // so get the X pixel coordinate.
                double renderedX = image.X + xOffset;
                double deltaX = renderedX / Width;
                double deltaWidth = Width / (image.Width * startScale);
                double originX = (e.ScaleOrigin.X - deltaX) * deltaWidth;


                // The ScaleOrigin is in relative coordinates to the wrapped user interface element,
                // so get the Y pixel coordinate.
                double renderedY = image.Y + yOffset;
                double deltaY = renderedY / Height;
                double deltaHeight = Height / (image.Height * startScale);
                double originY = (e.ScaleOrigin.Y - deltaY) * deltaHeight;

                // Calculate the transformed element pixel coordinates.
                double targetX = xOffset - (originX * image.Width) * (currentScale - startScale);
                double targetY = yOffset - (originY * image.Height) * (currentScale - startScale);

                // Apply translation based on the change in origin.
                if (targetX < -image.Width * (currentScale - 1))
                    image.TranslationX = -image.Width * (currentScale - 1);
                else if (targetX > 0) image.TranslationX = 0;
                else image.TranslationX = targetX;

                if (targetY < -image.Height * (currentScale - 1))
                    image.TranslationY = -image.Height * (currentScale - 1);
                else if (targetY > 0) image.TranslationY = 0;
                else image.TranslationY = targetY;


                // Apply scale factor
                image.Scale = currentScale;

                // Clear and Update skCanvasView
                skCanvasView.InvalidateSurface();

                // Get Color from the center pixel of the skCanvasView
                GetColor();

            }
            if (e.Status == GestureStatus.Completed)
            {
                // Store the translation delta's of the wrapped user interface element.
                xOffset = image.TranslationX;
                yOffset = image.TranslationY;
            }
        }


        // Slider for Scale Change
        void ScaleSliderChanged(object sender, ValueChangedEventArgs e)
        {
            // Set the image scale according to the slider
            image.Scale = 1 + ScaleSlider.Value;

            // Clear and Update skCanvasView
            skCanvasView.InvalidateSurface();

            // Get Color from the center pixel of the skCanvasView
            GetColor();
        }


        // Get Color from the center pixel of the skCanvasView
        void GetColor()
        {
            double dX = centerX - x0;
            double dY = centerY - y0;
            if (dX < 0 || dY < 0) {
                ColorMonitor.BackgroundColor = Color.White;
                ColorCode.Text = " ";
                return;
            }

            int x = (int)(w0 * dX / width);
            int y = (int)(h0 * dY / height);
            if (x > w0 || y > h0) {
                ColorMonitor.BackgroundColor = Color.White;
                ColorCode.Text = " ";
                return;
            }

            skColor = skBitmap.GetPixel(x, y);

            ColorMonitor.BackgroundColor = Color.FromHex(skColor.ToString());
            ColorCode.Text = skColor.ToString();

            // Set TextColor the inverted BackGroundColor
            double r = (int)skColor.Red ^ 0xff;
            double g = (int)skColor.Green ^ 0xff;
            double b = (int)skColor.Blue ^ 0xff;
            ColorCode.TextColor = Color.FromRgb(r, g, b);

            Red.Text    = "R: " + skColor.Red.ToString();
            Green.Text  = "G: " + skColor.Green.ToString();
            Blue.Text   = "B: " + skColor.Blue.ToString();

        }


        // Draw two rectangles on the initial skCanvasView and the Image
        // to monitor whether those positions can be correctly traced
        void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs e)
        {
            var surface = e.Surface;
            var canvas = surface.Canvas;

            canvas.Clear();

            // Draw RectAngle of the skCanvasView
            canvas.DrawRect((int)(image.TranslationX * skScale), (int)(image.TranslationY * skScale), 
                            (int)(image.Width * image.Scale * skScale), (int)(image.Height * image.Scale * skScale), rectPaint);

            // Calculate the Coordinates of the image RectAngle
            if(VerticalLong){
                x0 = (image.TranslationX + image.Width * image.Scale * (1 - WidthRatio / HeightRatio) / 2) * skScale;
                y0 = image.TranslationY * skScale;
                width = image.Width * image.Scale * WidthRatio / HeightRatio * skScale;
                height = image.Height * image.Scale * skScale;
            }
            else{
                x0 = image.TranslationX * skScale;
                y0 = (image.TranslationY + image.Height * image.Scale * (1 - HeightRatio / WidthRatio) / 2) * skScale;
                width = image.Width * image.Scale * skScale;
                height = image.Height * image.Scale * HeightRatio / WidthRatio * skScale;
            }


            // Draw RectAngle of the image
            canvas.DrawRect((int)x0, (int)y0, (int)width, (int)height, rectPaint1);

        }
        private SKPaint rectPaint = new SKPaint
        {
            Style = SKPaintStyle.Stroke,
            StrokeWidth = 10,
            Color = SKColors.Blue
        };
        private SKPaint rectPaint1 = new SKPaint
        {
            Style = SKPaintStyle.Stroke,
            StrokeWidth = 10,
            Color = SKColors.Red
        };


        // Draw a Cross-Hairs to target the pixel to get color
        void CrossHairs(object sender, SKPaintSurfaceEventArgs e)
        {
            var surface = e.Surface;
            var canvas = surface.Canvas;

            int w = (int)skCanvasView.Width * skScale;
            int h = (int)skCanvasView.Height * skScale;

            canvas.Clear();

            canvas.DrawLine(0, h/2, w, h/2, linePaint1);
            canvas.DrawLine(0, h/2, w, h/2, linePaint);
            canvas.DrawLine(w/2, 0, w/2, h, linePaint1);
            canvas.DrawLine(w/2, 0, w/2, h, linePaint);

            canvas.DrawCircle(w/2, h/2, 100, circlePaint1);
            canvas.DrawCircle(w/2, h/2, 100, circlePaint);
            canvas.DrawCircle(w/2, h/2, 200, circlePaint1);
            canvas.DrawCircle(w/2, h/2, 200, circlePaint);

        }
        private SKPaint linePaint = new SKPaint
        {
            StrokeWidth = 3,
            IsAntialias = true,
            Color = SKColors.Black
        };
        private SKPaint circlePaint = new SKPaint
        {
            Style = SKPaintStyle.Stroke,
            StrokeWidth = 3,
            Color = SKColors.Black

        };
        private SKPaint linePaint1 = new SKPaint
        {
            StrokeWidth = 7,
            IsAntialias = true,
            Color = SKColors.White
        };
        private SKPaint circlePaint1 = new SKPaint
        {
            Style = SKPaintStyle.Stroke,
            StrokeWidth = 7,
            Color = SKColors.White
        };


    }   // End of public partial class MainPage
}       // End of namespace ColorPicker

In "MainPage()", after setting the events of PanGesture and PinchGesture, read the image data.

"CanvasSizeChanged" method is raised when "skCanvasView" is placed in the Grid (Grid.Row=1) and the Grid size is changed.  As I wrote in the previous post, PCL Storage (2), it seems that "skCanvasView" size is correctly gotten by the event raise.  In this method, after calculating some values of image size, get the color code of the center of the image by "GetColor" method.  "image.AnchorX=0" and "image.AnchorY=0" is to avoid the phenomenon that the image is not scaled properly if the scale slider is operated immediately just after launching the App. (I don't know the cause of the phenomenon and why the zero substitution solve it.)

"OnPanUpdated" method and "OnPinchUpdated" method were developed with reference to the sample program of Xamarin official sites "Adding a Pan Gesture Recognizer" and "Adding a Pinch Gesture Recognizer" respectively.  In both of the methods, "skCanvasView" updating and "GetColor" method are added.

"ScaleSliderChanged" method scales the image.  The pinch gesture also enable image scaling, just tried to use it.

"OnCanvasViewPaintSurface" method draw two rectangles on the image.  One of them is drawn on the borderline of the Grid where the image was placed (Grid.Row=1) with the color of blue.  The color of another rectangle is red, and it's on the borderline of the image.  These rectangles move and scale according to the pan and pinch gesture to the image.  Actually, after debugging, this method is not necessary, but I left them to monitor the position of the image.

Finally, "CrossHairs" method draw a figure like a rifle sight.  The color code of the pixel on the figure's center is picked dynamically.  In this method and "OnCanvasViewPaintSurface" method, SkiaSharp package is used for drawing.  So you need to go to the NuGet Package Management, and add SkiaSharp to your solution.


A movie showing this App running.  You can get the original movie file from here (in "iOS Simulator movie" folder).



コメント

このブログの人気の投稿

PCL Storage (1)

Optimal Rectangle Packing