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");
        }