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
45 changes: 38 additions & 7 deletions src/ImageJCsharp.App/Form1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public partial class Form1 : Form
private ImageDocument? _document;
private Bitmap? _displayBitmap;
private RectRoi? _roi;
private RoiShape _roiShape = RoiShape.Rectangle;
private RoiShape _selectionTool = RoiShape.Rectangle;
private Point? _dragStartImagePoint;
private RectRoi? _resizeStartRoi;
private RoiResizeHandle _activeResizeHandle = RoiResizeHandle.None;
Expand Down Expand Up @@ -54,7 +56,8 @@ private void BuildUi()
AddItem(file, "E&xit", () => Close(), Keys.Alt | Keys.F4);

var edit = AddMenu(menu, "&Edit");
AddDisabledItem(edit, "No Edit commands yet");
AddItem(edit, "&Rectangle Selection", () => _selectionTool = RoiShape.Rectangle);
AddItem(edit, "&Oval Selection", () => _selectionTool = RoiShape.Oval);

var image = AddMenu(menu, "&Image");
AddActiveImageItem(image, "&Brightness/Contrast...", AdjustBrightnessContrast);
Expand Down Expand Up @@ -198,6 +201,7 @@ private void OpenImage()
using var bitmap = new Bitmap(dialog.FileName);
_document = new ImageDocument(dialog.FileName, BitmapConversion.ToGrayImage(bitmap));
_roi = null;
_roiShape = RoiShape.Rectangle;
_displayAdjustment = null;
_zoom = 1d;
RefreshDisplay();
Expand Down Expand Up @@ -232,6 +236,7 @@ private void CloseImage()
{
_document = null;
_roi = null;
_roiShape = RoiShape.Rectangle;
_displayAdjustment = null;
_displayBitmap?.Dispose();
_displayBitmap = null;
Expand Down Expand Up @@ -291,8 +296,10 @@ private void MeasureCurrentRoi()
return;
}

var roi = _roi ?? new RectRoi(0, 0, _document.Image.Width, _document.Image.Height);
var result = Measurements.Measure(_document.Image, roi, _document.Calibration);
var fullImageRoi = new RectRoi(0, 0, _document.Image.Width, _document.Image.Height);
var result = _roi is null || _roiShape == RoiShape.Rectangle
? Measurements.Measure(_document.Image, _roi ?? fullImageRoi, _document.Calibration)
: Measurements.Measure(_document.Image, new OvalRoi(_roi.Value.X, _roi.Value.Y, _roi.Value.Width, _roi.Value.Height), _document.Calibration);
_resultsGrid.Rows.Add(CreateMeasurementRow(result));
UpdateCommandStates();
}
Expand Down Expand Up @@ -440,7 +447,15 @@ private void ShowHistogram()

var histogram = _roi is null
? Histogram.Calculate(_document.Image)
: Histogram.Calculate(_document.Image, _roi.Value);
: _roiShape == RoiShape.Rectangle
? Histogram.Calculate(_document.Image, _roi.Value)
: null;

if (histogram is null)
{
MessageBox.Show(this, "Histogram currently supports full image or rectangle ROI selections.", "Unsupported ROI", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}

var form = new HistogramForm(_document.DisplayName, histogram);
form.Show(this);
Expand All @@ -455,7 +470,15 @@ private void ShowProfile()

var profile = _roi is null
? Profile.HorizontalCenterLine(_document.Image)
: Profile.HorizontalCenterLine(_document.Image, _roi.Value);
: _roiShape == RoiShape.Rectangle
? Profile.HorizontalCenterLine(_document.Image, _roi.Value)
: null;

if (profile is null)
{
MessageBox.Show(this, "Plot Profile currently supports full image or rectangle ROI selections.", "Unsupported ROI", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}

var form = new ProfileForm(_document.DisplayName, profile);
form.Show(this);
Expand Down Expand Up @@ -604,6 +627,7 @@ private void ImageBoxMouseDown(object? sender, MouseEventArgs e)
if (_activeResizeHandle == RoiResizeHandle.None)
{
_resizeStartRoi = null;
_roiShape = _selectionTool;
_roi = new RectRoi(_dragStartImagePoint.Value.X, _dragStartImagePoint.Value.Y, 1, 1);
}
else
Expand Down Expand Up @@ -661,12 +685,19 @@ private void ImageBoxPaint(object? sender, PaintEventArgs e)

var roi = _roi.Value;
using var pen = new Pen(Color.Yellow, 1);
e.Graphics.DrawRectangle(
pen,
var bounds = new RectangleF(
(float)(roi.X * _zoom),
(float)(roi.Y * _zoom),
(float)(roi.Width * _zoom),
(float)(roi.Height * _zoom));
if (_roiShape == RoiShape.Oval)
{
e.Graphics.DrawEllipse(pen, bounds);
}
else
{
e.Graphics.DrawRectangle(pen, bounds.X, bounds.Y, bounds.Width, bounds.Height);
}

DrawRoiHandles(e.Graphics, roi);
}
Expand Down
7 changes: 7 additions & 0 deletions src/ImageJCsharp.App/RoiShape.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace ImageJCsharp.App;

public enum RoiShape
{
Rectangle,
Oval
}
54 changes: 49 additions & 5 deletions src/ImageJCsharp.Core/Measurements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,54 @@ public MeasurementResult(int pixelCount, double area, double mean, double min, d
public static class Measurements
{
public static MeasurementResult Measure(GrayImage image, RectRoi roi, PixelCalibration calibration)
{
return Measure(
image,
roi.X,
roi.Y,
roi.Right,
roi.Bottom,
calibration,
(_, _) => true,
nameof(roi));
}

public static MeasurementResult Measure(GrayImage image, OvalRoi roi, PixelCalibration calibration)
{
return Measure(
image,
roi.X,
roi.Y,
roi.Right,
roi.Bottom,
calibration,
roi.ContainsPixel,
nameof(roi));
}

private static MeasurementResult Measure(
GrayImage image,
int roiX,
int roiY,
int roiRight,
int roiBottom,
PixelCalibration calibration,
Func<int, int, bool> containsPixel,
string roiParameterName)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}

var startX = Math.Max(0, roi.X);
var startY = Math.Max(0, roi.Y);
var endX = Math.Min(image.Width, roi.Right);
var endY = Math.Min(image.Height, roi.Bottom);
var startX = Math.Max(0, roiX);
var startY = Math.Max(0, roiY);
var endX = Math.Min(image.Width, roiRight);
var endY = Math.Min(image.Height, roiBottom);

if (startX >= endX || startY >= endY)
{
throw new ArgumentException("ROI does not intersect the image.", nameof(roi));
throw new ArgumentException("ROI does not intersect the image.", roiParameterName);
}

var count = 0;
Expand All @@ -84,6 +118,11 @@ public static MeasurementResult Measure(GrayImage image, RectRoi roi, PixelCalib
{
for (var x = startX; x < endX; x++)
{
if (!containsPixel(x, y))
{
continue;
}

var value = image[x, y];
count++;
sum += value;
Expand All @@ -93,6 +132,11 @@ public static MeasurementResult Measure(GrayImage image, RectRoi roi, PixelCalib
}
}

if (count == 0)
{
throw new ArgumentException("ROI does not contain any image pixels.", roiParameterName);
}

var mean = sum / count;
var variance = Math.Max(0, (sumSquares / count) - (mean * mean));
var area = count * calibration.PixelWidth * calibration.PixelHeight;
Expand Down
47 changes: 47 additions & 0 deletions src/ImageJCsharp.Core/OvalRoi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;

namespace ImageJCsharp.Core;

public readonly struct OvalRoi
{
public OvalRoi(int x, int y, int width, int height)
{
if (width <= 0)
{
throw new ArgumentOutOfRangeException(nameof(width), "Width must be positive.");
}

if (height <= 0)
{
throw new ArgumentOutOfRangeException(nameof(height), "Height must be positive.");
}

X = x;
Y = y;
Width = width;
Height = height;
}

public int X { get; }

public int Y { get; }

public int Width { get; }

public int Height { get; }

public int Right => X + Width;

public int Bottom => Y + Height;

public bool ContainsPixel(int x, int y)
{
var radiusX = (Width - 1) / 2d;
var radiusY = (Height - 1) / 2d;
var centerX = X + radiusX;
var centerY = Y + radiusY;
var normalizedX = radiusX == 0 ? 0 : (x - centerX) / radiusX;
var normalizedY = radiusY == 0 ? 0 : (y - centerY) / radiusY;
return (normalizedX * normalizedX) + (normalizedY * normalizedY) <= 1d;
}
}
17 changes: 17 additions & 0 deletions tests/ImageJCsharp.App.Tests/FormStartupTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,23 @@ public void Form1DisablesImageCommandsWhenNoImageIsActive()
}
}

[Fact]
public void EditMenuCanSelectOvalRoiTool()
{
RoiShape? selectedTool = null;
var capturedException = RunOnStaThread(() =>
{
using var form = new Form1();

FindMenuItem(form, "Edit", "Oval Selection").PerformClick();

selectedTool = GetPrivateField<RoiShape>(form, "_selectionTool");
});

Assert.Null(capturedException);
Assert.Equal(RoiShape.Oval, selectedTool);
}

[Fact]
public void FileCloseClearsActiveImageState()
{
Expand Down
19 changes: 19 additions & 0 deletions tests/ImageJCsharp.Core.Tests/CoreImageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,25 @@ public void MeasureUsesPixelCalibrationForArea()
Assert.Equal(4, result.Area);
}

[Fact]
public void MeasureUsesOnlyPixelsInsideOvalRoi()
{
var image = GrayImage.FromPixels(3, 3, new ushort[]
{
1, 2, 3,
4, 5, 6,
7, 8, 9
});

var result = Measurements.Measure(image, new OvalRoi(0, 0, 3, 3), PixelCalibration.Identity);

Assert.Equal(5, result.PixelCount);
Assert.Equal(5, result.Area);
Assert.Equal(5, result.Mean);
Assert.Equal(2, result.Min);
Assert.Equal(8, result.Max);
}

[Fact]
public void ThresholdCreatesBinaryMask()
{
Expand Down
Loading