public static readonly int PIXEL_BYTE_WIDTH = 4; // determined by PixelFormat.Format32bppArgb; http://www.bobpowell.net/lockingbits.htm public override void DoWork() { /* TODO: OPTIMIZATIONS: * - compute surfaceNoiseLevel based on image analysis * - make debug output to log optional * - surface/transition decorations (biggest problem is that there can be a variable number...only for first edge to start?) * - for marked image, save decorations...don't copy/paint_on image */ DateTime startTime = DateTime.Now; TestExecution().LogMessageWithTimeFromTrigger("[" + Name + "] started at " + startTime + Environment.NewLine); int resultX = -1; int resultY = -1; if (mSourceImage.Bitmap == null) { TestExecution().LogMessage("ERROR: source image for '" + Name + "' does not exist."); } else { Bitmap sourceBitmap = SourceImage.Bitmap; BitmapData sourceBitmapData = null; try { sourceBitmapData = sourceBitmap.LockBits(new Rectangle(0, 0, sourceBitmap.Width, sourceBitmap.Height), ImageLockMode.ReadOnly, PIXEL_FORMAT); int sourceStride = sourceBitmapData.Stride; int sourceStrideOffset = sourceStride - (sourceBitmapData.Width * PIXEL_BYTE_WIDTH); int brightnessThreshold = (int)mBrightnessThreshold.ValueAsLong(); Point currentPoint = new Point(-1, -1); mROI.GetFirstPointOnXAxis(mSourceImage, ref currentPoint); ValueGrouper xGrouper = new ValueGrouper(0, 255, 50); ValueGrouper yGrouper = new ValueGrouper(0, 255, 50); unsafe // see http://www.codeproject.com/csharp/quickgrayscale.asp?df=100&forumid=293759&select=2214623&msg=2214623 { byte *sourcePointer; while (currentPoint.X != -1 && currentPoint.Y != -1) { sourcePointer = (byte *)sourceBitmapData.Scan0; // init to first byte of image sourcePointer += (currentPoint.Y * sourceStride) + (currentPoint.X * PIXEL_BYTE_WIDTH); // adjust to current point pixelGrayValue = (int)(0.3 * sourcePointer[2] + 0.59 * sourcePointer[1] + 0.11 * sourcePointer[0]); // Then, add 30% of the red value, 59% of the green value, and 11% of the blue value, together. .... These percentages are chosen due to the different relative sensitivity of the normal human eye to each of the primary colors (less sensitive to green, more to blue). // http://www.bobpowell.net/grayscale.htm // https://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=440425&SiteID=1 //TestExecution().LogMessage(currentPoint.X + "," + currentPoint.Y + " " + pixelGrayValue + " " + brightnessThreshold); if (pixelGrayValue >= brightnessThreshold) { xGrouper.AddValue(currentPoint.X); yGrouper.AddValue(currentPoint.Y); } mROI.GetNextPointOnXAxis(mSourceImage, ref currentPoint); } TestExecution().LogMessageWithTimeFromTrigger("[" + Name + "] finished analyzing pixels"); } // end unsafe block for (int z = 0; z < xGrouper.NumGroups; z++) { ValueGrouper.GroupStats groupStats = xGrouper.GetGroup(z); TestExecution().LogMessage(groupStats.start + " " + groupStats.end + " " + groupStats.count + " " + groupStats.Average()); } for (int z = 0; z < yGrouper.NumGroups; z++) { ValueGrouper.GroupStats groupStats = yGrouper.GetGroup(z); TestExecution().LogMessage(groupStats.start + " " + groupStats.end + " " + groupStats.count + " " + groupStats.Average()); } ValueGrouper.GroupStats biggestXGroup = xGrouper.BiggestGroupWithNeighbors(); if (biggestXGroup != null) { resultX = biggestXGroup.Average(); } ValueGrouper.GroupStats biggestYGroup = yGrouper.BiggestGroupWithNeighbors(); if (biggestXGroup != null) { resultY = biggestYGroup.Average(); } } catch (Exception e) { TestExecution().LogMessageWithTimeFromTrigger("ERROR: Failure in " + Name + "; msg=" + e.Message + " " + Environment.NewLine + e.StackTrace); } finally { sourceBitmap.UnlockBits(sourceBitmapData); } } // end main block ("else" after all initial setup error checks) mBrightSpot_X.SetValue(resultX); mBrightSpot_Y.SetValue(resultY); mBrightSpot_X.SetIsComplete(); mBrightSpot_Y.SetIsComplete(); DateTime doneTime = DateTime.Now; TimeSpan computeTime = doneTime - startTime; TestExecution().LogMessageWithTimeFromTrigger(Name + " computed bright spot at " + resultX + "," + resultY); if (mAutoSave) { try { string filePath = ((FindBrightestSpotDefinition)Definition()).AutoSavePath; mSourceImage.Save(filePath, Name, true); TestExecution().LogMessageWithTimeFromTrigger("Snapshot saved"); } catch (ArgumentException e) { Project().Window().logMessage("ERROR: " + e.Message); TestExecution().LogErrorWithTimeFromTrigger(e.Message); } catch (Exception e) { Project().Window().logMessage("ERROR: Unable to AutoSave snapshot from " + Name + ". Ensure path valid and disk not full. Low-level message=" + e.Message); TestExecution().LogErrorWithTimeFromTrigger("Unable to AutoSave snapshot from " + Name + ". Ensure path valid and disk not full."); } } TestExecution().LogMessageWithTimeFromTrigger(Name + " finished at " + doneTime + " | took " + computeTime.TotalMilliseconds + "ms"); }
public static readonly int PIXEL_BYTE_WIDTH = 4; // determined by PixelFormat.Format32bppArgb; http://www.bobpowell.net/lockingbits.htm public override void DoWork() { DateTime startTime = DateTime.Now; TestExecution().LogMessageWithTimeFromTrigger("[" + Name + "] started at " + startTime + Environment.NewLine); int resultantAngle = -1; if (mPrerequisite == null || mPrerequisite.ValueAsBoolean()) { int centerX = (int)mCenterX.ValueAsLong(); int centerY = (int)mCenterY.ValueAsLong(); int outerRadius = (int)mOuterSearchRadius.ValueAsLong(); int innerRadius = (int)mInnerSearchRadius.ValueAsLong(); if (mSourceImage.Bitmap == null) { TestExecution().LogMessage("ERROR: source image for '" + Name + "' does not exist."); } else if (centerX < outerRadius || centerX + outerRadius >= mSourceImage.Bitmap.Width || centerY < outerRadius || centerY + outerRadius >= mSourceImage.Bitmap.Height) { TestExecution().LogMessage("ERROR: OuterSearchBounds for '" + Name + "' isn't completely within the image bounds; center=" + centerX + "," + centerY + "; outer search radius=" + outerRadius + "; image size=" + mSourceImage.Bitmap.Width + "x" + mSourceImage.Bitmap.Height); } else if (innerRadius > outerRadius) { TestExecution().LogMessage("ERROR: The inner search radius for '" + Name + "' greater than the outer radius: inner radius=" + innerRadius + "; outer radius=" + outerRadius); } else { int MaxDegDist = (int)mMarkMergeDistance_Deg.ValueAsLong(); int numTests = (int)mNumberOfTestsInDonut.ValueAsLong(); Bitmap sourceBitmap = SourceImage.Bitmap; Bitmap markedBitmap = null; BitmapData sourceBitmapData = null; BitmapData markedBitmapData = null; if (mCreateMarkedImage && mImageToMark != null && mImageToMark.Bitmap != null) { markedBitmap = mImageToMark.Bitmap; } try { sourceBitmapData = sourceBitmap.LockBits(new Rectangle(0, 0, sourceBitmap.Width, sourceBitmap.Height), ImageLockMode.ReadOnly, PIXEL_FORMAT); if (markedBitmap != null) { markedBitmapData = markedBitmap.LockBits(new Rectangle(0, 0, markedBitmap.Width, markedBitmap.Height), ImageLockMode.ReadWrite, PIXEL_FORMAT); } int sourceStride = sourceBitmapData.Stride; int sourceStrideOffset = sourceStride - (sourceBitmapData.Width * PIXEL_BYTE_WIDTH); unsafe // see http://www.codeproject.com/csharp/quickgrayscale.asp?df=100&forumid=293759&select=2214623&msg=2214623 { byte *sourcePointer; byte *markedPointer; ValueGrouper grouper = new ValueGrouper(0, 360, 24); // 12 groups = 30 degress each, 24 groups = 15 degrees each int stepSize = Math.Max(1, (outerRadius - innerRadius) / numTests); for (int radius = innerRadius; radius <= outerRadius; radius += stepSize) { TestExecution().LogMessage(""); TestExecution().LogMessageWithTimeFromTrigger(Name + " searching for mark at radius " + radius); //************************************************* // // COLLECT GRAY VALUES AROUND THE CIRCLE (stored in "samples" array) // AND COMPUTE AVERAGE GREY VALUE // //************************************************* int x = 0; int y = 0; double rad = 0; /* * for (x = centerX - outerRadius; x <= centerX + outerRadius; x++) * { * xdist = (x-centerX); * sqrt = (int)Math.Sqrt(outerRadius * outerRadius - xdist * xdist); * y = centerY + sqrt; * mSourceImage.Image.SetPixel(x, y, Color.Magenta); * mSourceImage.Image.SetPixel(x, centerY - sqrt, Color.Magenta); * }*/ int lastX = 0; int lastY = 0; int change; int biggestChange = 0; bool inited = false; int degFraction = 1; int numberOfSamples = 360 * degFraction; double inc = (Math.PI / 180) / degFraction; // 1 deg = Pi/180 int[] samples = new int[numberOfSamples]; long sum = 0; int avg = -1; // http://en.wikipedia.org/wiki/Cirlce // http://en.wikipedia.org/wiki/Radians for (int a = 0; a < numberOfSamples; a++) { rad += inc; x = (int)(centerX + radius * Math.Cos(rad)); y = (int)(centerY + radius * Math.Sin(rad)); //mSourceImage.Image.SetPixel(x, y, Color.Magenta); sourcePointer = (byte *)sourceBitmapData.Scan0; // init to first byte of image sourcePointer += (y * sourceStride) + (x * PIXEL_BYTE_WIDTH); // adjust to current point samples[a] = (int)(0.3 * sourcePointer[2] + 0.59 * sourcePointer[1] + 0.11 * sourcePointer[0]); // Then, add 30% of the red value, 59% of the green value, and 11% of the blue value, together. .... These percentages are chosen due to the different relative sensitivity of the normal human eye to each of the primary colors (less sensitive to green, more to blue). sum += samples[a]; change = Math.Abs(x - lastX); if (change > biggestChange && inited) { biggestChange = change; } change = Math.Abs(y - lastY); if (change > biggestChange && inited) { biggestChange = change; } lastX = x; lastY = y; inited = true; } avg = (int)(sum / numberOfSamples); //************************************************* // // COMPUTE STD DEVIATION OF GRAY VALUES // //************************************************* // compute std dev: sum squares of deviations http://en.wikipedia.org/wiki/Standard_deviation long sumSqDev = 0; for (int a = 0; a < numberOfSamples; a++) { sumSqDev += (long)Math.Pow(avg - samples[a], 2); } int stdDev = Math.Max(1, (int)Math.Sqrt(sumSqDev / numberOfSamples)); //************************************************* // // SEARCH FOR MARKS BY LOOKING FOR DARK SPOTS (at least 2 std dev below avg) // WE SCORE MARKS BASED ON HOW "WIDE" THEY ARE // //************************************************* List <Mark> marks = new List <Mark>(); Mark lastMark = null; TestExecution().LogMessageWithTimeFromTrigger(Name + ": std dev=" + stdDev); for (int a = 0; a < numberOfSamples; a++) { if (samples[a] < avg - 2 * stdDev) { if (lastMark != null && lastMark.DegreesFromEnd(a, inc) <= MaxDegDist) { lastMark.endPos = a; lastMark.score += 1; } else { lastMark = new Mark(); marks.Add(lastMark); lastMark.startPos = a; lastMark.endPos = a; lastMark.score = 1; } rad = a * inc; x = (int)(centerX + radius * Math.Cos(rad)); y = (int)(centerY + radius * Math.Sin(rad)); if (markedBitmap != null) { markedPointer = (byte *)markedBitmapData.Scan0; // init to first byte of image markedPointer += (y * sourceStride) + (x * PIXEL_BYTE_WIDTH); // adjust to current point markedPointer[3] = mMarkColor.A; markedPointer[2] = mMarkColor.R; markedPointer[1] = mMarkColor.G; markedPointer[0] = mMarkColor.B; } TestExecution().LogMessageWithTimeFromTrigger(Name + ": dark at " + x + "," + y); } } if (marks.Count > 1) // if mark is crosses 0/360deg, then merge the two marks into one { if (lastMark.DegreesFromEnd(marks[0].startPos, inc) < MaxDegDist) { TestExecution().LogMessageWithTimeFromTrigger(Name + ": MERGING MARKS AROUND 0/360."); lastMark.endPos = marks[0].endPos; lastMark.score += marks[0].score; marks.RemoveAt(0); } } double highestConcentrationScore = 0; Mark markWithHighestConcentrationScore = null; foreach (Mark mark in marks) { if (mark.score > 2) { if (mark.Width(numberOfSamples) > highestConcentrationScore) { highestConcentrationScore = mark.Width(numberOfSamples); markWithHighestConcentrationScore = mark; } TestExecution().LogMessageWithTimeFromTrigger(Name + ": mark: deg=" + mark.Middle_Deg(numberOfSamples, inc) + "(" + mark.Start_Deg(inc) + " to " + mark.End_Deg(inc) + ") score=" + mark.score + " width=" + mark.Width(numberOfSamples) + "samples start=" + mark.startPos + " end=" + mark.endPos); } } TestExecution().LogMessageWithTimeFromTrigger(Name + ": biggest change=" + biggestChange); if (markWithHighestConcentrationScore != null) { int markAngle = markWithHighestConcentrationScore.Middle_Deg(numberOfSamples, inc); grouper.AddValue(markAngle); TestExecution().LogMessageWithTimeFromTrigger(Name + ": at radius " + radius + " using angle " + markAngle); } } // end for loop (to test each radius) ValueGrouper.GroupStats biggestGroup = grouper.BiggestGroup(); if (biggestGroup == null) { TestExecution().LogMessageWithTimeFromTrigger(Name + " no radius found (no biggest in grouper)"); } else { TestExecution().LogMessageWithTimeFromTrigger(Name + " biggest group: count=" + biggestGroup.count + " ndx=" + biggestGroup.groupNdx + " avg=" + biggestGroup.Average() + " min=" + biggestGroup.min + " max=" + biggestGroup.max); ValueGrouper.GroupStats cw_group = null; ValueGrouper.GroupStats ccw_group = null; if (biggestGroup.groupNdx == 0) { ccw_group = grouper.GetGroup(grouper.NumGroups - 1); ccw_group.sum = ccw_group.sum - (360 * ccw_group.count); // HACK: adjust angles to compensate for 0/360 switch } else { ccw_group = grouper.GetGroup(biggestGroup.groupNdx - 1); } if (biggestGroup.groupNdx == grouper.NumGroups - 1) { cw_group = grouper.GetGroup(0); cw_group.sum = cw_group.sum + (360 * cw_group.count); // HACK: adjust angles to compensate for 0/360 switch } else { ccw_group = grouper.GetGroup(biggestGroup.groupNdx + 1); } long overallSum = biggestGroup.sum; int overallCount = biggestGroup.count; if (ccw_group != null && ccw_group.count > 0) { TestExecution().LogMessageWithTimeFromTrigger(Name + " using CCW group; count=" + ccw_group.count + " ndx=" + ccw_group.groupNdx + " avg=" + ccw_group.Average() + " min=" + ccw_group.min + " max=" + ccw_group.max); overallSum += ccw_group.sum; overallCount += ccw_group.count; } if (cw_group != null && cw_group.count > 0) { TestExecution().LogMessageWithTimeFromTrigger(Name + " using CW group; count=" + cw_group.count + " ndx=" + cw_group.groupNdx + " avg=" + cw_group.Average() + " min=" + cw_group.min + " max=" + cw_group.max); overallSum += cw_group.sum; overallCount += cw_group.count; } resultantAngle = (int)(overallSum / overallCount); if (resultantAngle >= 360) { resultantAngle -= 360; } else if (resultantAngle < 0) { resultantAngle += 360; } double overallRad = (resultantAngle * Math.PI) / 180; // deg = rad*180/PI rad=deg*PI/180 mResultantRay.SetStartX(centerX); mResultantRay.SetStartY(centerY); mResultantRay.SetEndX((int)(centerX + outerRadius * Math.Cos(overallRad))); mResultantRay.SetEndY((int)(centerY + outerRadius * Math.Sin(overallRad))); mResultantRay.SetIsComplete(); } } // end unsafe block } catch (Exception e) { TestExecution().LogMessageWithTimeFromTrigger("ERROR: Failure in " + Name + "; msg=" + e.Message + " " + Environment.NewLine + e.StackTrace); } finally { sourceBitmap.UnlockBits(sourceBitmapData); if (markedBitmap != null) { markedBitmap.UnlockBits(markedBitmapData); } } } // end main block ("else" after all initial setup error checks) } else { TestExecution().LogMessageWithTimeFromTrigger(Name + ": prerequisites not met."); } mResultantAngle.SetValue(resultantAngle); mResultantAngle.SetIsComplete(); DateTime doneTime = DateTime.Now; TimeSpan computeTime = doneTime - startTime; TestExecution().LogMessageWithTimeFromTrigger(Name + " computed angle at " + resultantAngle); if (mAutoSave) { try { string filePath = ((FindRadialLineDefinition)Definition()).AutoSavePath; mSourceImage.Save(filePath, Name, true); if (mImageToMark != null) { mImageToMark.Save(filePath, Name, "_marked_" + resultantAngle); } TestExecution().LogMessageWithTimeFromTrigger("Snapshot saved"); } catch (ArgumentException e) { Project().Window().logMessage("ERROR: " + e.Message); TestExecution().LogErrorWithTimeFromTrigger(e.Message); } catch (Exception e) { Project().Window().logMessage("ERROR: Unable to AutoSave snapshot from " + Name + ". Ensure path valid and disk not full. Low-level message=" + e.Message); TestExecution().LogErrorWithTimeFromTrigger("Unable to AutoSave snapshot from " + Name + ". Ensure path valid and disk not full."); } } TestExecution().LogMessageWithTimeFromTrigger(Name + " finished at " + doneTime + " | took " + computeTime.TotalMilliseconds + "ms"); }