Exemple #1
0
        private void LoadGrid(String filename)
        {
            if (File.Exists(filename))
            {
                stopwatch grid_timer = new stopwatch();

                // read the data into a byte array
                grid_timer.Start();
                Byte[] grid_data = File.ReadAllBytes(filename);
                grid_timer.Stop();
                lstBenchmarks.Items.Add("Disk read time     " + Convert.ToString(grid_timer.time_elapsed_mS) + " mS");

                // decompress the data
                grid_timer.Start();
                Byte[] decompressed_grid_data =
                    AcedInflator.Instance.Decompress(grid_data, 0, 0, 0);
                grid_timer.Stop();
                lstBenchmarks.Items.Add("Decompression time " + Convert.ToString(grid_timer.time_elapsed_mS) + " mS");

                grid_timer.Start();
                sim.rob.LoadGrid(decompressed_grid_data);
                grid_timer.Stop();
                lstBenchmarks.Items.Add("Creation time      " + Convert.ToString(grid_timer.time_elapsed_mS) + " mS");
            }
        }
Exemple #2
0
        /// <summary>
        /// save the occupancy grid to file and show some benchmarks
        /// </summary>
        /// <param name="filename"></param>
        private void SaveGrid(String filename)
        {
            FileStream   fp      = new FileStream(filename, FileMode.Create);
            BinaryWriter binfile = new BinaryWriter(fp);

            lstBenchmarks.Items.Clear();

            stopwatch grid_timer = new stopwatch();

            grid_timer.Start();
            Byte[] grid_data = sim.rob.SaveGrid();
            grid_timer.Stop();
            lstBenchmarks.Items.Add("Distillation time  " + Convert.ToString(grid_timer.time_elapsed_mS) + " mS");

            grid_timer.Start();
            Byte[] compressed_grid_data =
                AcedDeflator.Instance.Compress(grid_data, 0, grid_data.Length,
                                               AcedCompressionLevel.Fast, 0, 0);
            grid_timer.Stop();

            lstBenchmarks.Items.Add("Compression ratio  " + Convert.ToString(100 - (int)(compressed_grid_data.Length * 100 / grid_data.Length)) + " %");
            lstBenchmarks.Items.Add("Compression time   " + Convert.ToString(grid_timer.time_elapsed_mS) + " mS");

            grid_timer.Start();
            binfile.Write(compressed_grid_data);
            grid_timer.Stop();
            lstBenchmarks.Items.Add("Disk write time    " + Convert.ToString(grid_timer.time_elapsed_mS) + " mS");

            binfile.Close();
            fp.Close();
        }
        /// <summary>
        /// update the robot's map
        /// </summary>
        /// <param name="state">robot state</param>
        private static void Update(ThreadMappingState state)
        {
            // object used for taking benchmark timings
            stopwatch clock = new stopwatch();

            clock.Start();

            // update all current poses with the observed rays
            state.motion.LocalGrid = state.grid;
            state.motion.AddObservation(state.stereo_rays, false);

            clock.Stop();
            state.benchmark_observation_update = clock.time_elapsed_mS;

            // what's the relative position of the robot inside the grid ?
            pos3D relative_position = new pos3D(state.pose.x - state.grid.x, state.pose.y - state.grid.y, 0);

            relative_position.pan = state.pose.pan - state.grid.pan;

            clock.Start();

            // garbage collect dead occupancy hypotheses
            state.grid.GarbageCollect();

            clock.Stop();
            state.benchmark_garbage_collection = clock.time_elapsed_mS;
        }
Exemple #4
0
        /// <summary>
        /// detects square shapes within the given mono image
        /// </summary>
        /// <param name="img_mono">
        /// mono image data <see cref="System.Byte"/>
        /// </param>
        /// <param name="img_width">
        /// image width <see cref="System.Int32"/>
        /// </param>
        /// <param name="img_height">
        /// image height <see cref="System.Int32"/>
        /// </param>
        /// <param name="ignore_periphery">
        /// whether to ignore edge features which stray into the border of the image <see cref="System.Boolean"/>
        /// </param>
        /// <param name="image_border_percent">
        /// percentage of the image considred to be the surrounding border <see cref="System.Int32"/>
        /// </param>
        /// <param name="grouping_radius_percent">
        /// radius used to group adjacent edge features <see cref="System.Int32"/>
        /// </param>
        /// <param name="erosion_dilation">
        /// erosion dilation level <see cref="System.Int32"/>
        /// </param>
        /// <param name="black_on_white">
        /// whether this image contains dark markings on a lighter background <see cref="System.Boolean"/>
        /// </param>
        /// <param name="minimum_volume_percent">
        /// minimum volume of the square as a percentage of the image volume (prevents very small stuff being detected) <see cref="System.Int32"/>
        /// </param>
        /// <param name="use_original_image">
        /// whether to allow img_mono to be altered
        /// </param>
        /// <param name="minimum_aspect_ratio">
        /// minimum aspect ratio when searching for valid regions
        /// </param>
        /// <param name="maximum_aspect_ratio">
        /// maximum aspect ratio when searching for valid regions
        /// </param>
        /// <returns>
        /// list of polygons representing the square regions <see cref="List`1"/>
        /// </returns>
    	public static List<polygon2D> DetectSquaresMono(byte[] mono_img, 
                                                        int img_width, int img_height,
                                                        bool ignore_periphery,
                                                        int image_border_percent,
                                                        int[] grouping_radius_percent,
                                                        int[] erosion_dilation,
                                                        bool black_on_white,
                                                        int minimum_volume_percent,
                                                        bool use_original_image,
                                                        float minimum_aspect_ratio,
                                                        float maximum_aspect_ratio,
                                                        bool squares_only,
                                                        bool debug,
                                                        int circular_ROI_radius,
                                                        ref List<int> edges)
        {
#if SHOW_TIMINGS
            stopwatch timer_square_detection = new stopwatch();
            timer_square_detection.Start();
#endif
            const int minimum_size_percent = 5;
            const int connect_edges_radius = 5;
            
            // maximum recursion depth when joining edges
            const int max_search_depth = 8000;
            
            // create a list to store the results
            List<polygon2D> square_shapes = new List<polygon2D>();
            
            // create edge detection object
            CannyEdgeDetector edge_detector = new CannyEdgeDetector();
            edge_detector.automatic_thresholds = true;  
            edge_detector.enable_edge_orientations = false;

#if SHOW_TIMINGS
            stopwatch timer_copy_invert = new stopwatch();
            timer_copy_invert.Start();
#endif
            
            byte[] img_mono = mono_img;
            if (!use_original_image)
            {
                // make a copy of the original image (we don't want to alter it)
                img_mono = (byte[])mono_img.Clone();
            }

            // if the image contains light markings on a darker background invert the pixels            
            if (!black_on_white)
            {
                for (int i = img_mono.Length-1; i >= 0; i--)
                {
                    img_mono[i] = (byte)(255 - img_mono[i]);
                }
            }

#if SHOW_TIMINGS
            timer_copy_invert.Stop();
            if (timer_copy_invert.time_elapsed_mS > 20)
                Console.WriteLine("DetectSquaresMono: copy/invert  " + timer_copy_invert.time_elapsed_mS.ToString() );
#endif
                            
            int image_border = img_width * image_border_percent / 100;

            byte[] img_mono2 = null;                        
            byte[] eroded = null;
            byte[] dilated = null;
            bool previous_eroded = false;
            bool previous_dilated = false;
            
            // try each erosion/dilation level
            for (int erosion_dilation_level = 0; erosion_dilation_level < erosion_dilation.Length; erosion_dilation_level++)
            {

#if SHOW_TIMINGS
                stopwatch timer_erode_dilate = new stopwatch();
                timer_erode_dilate.Start();
#endif
                
                // erode
                if (erosion_dilation[erosion_dilation_level] > 0)
                {                    
                    if (previous_eroded)
                        eroded = image.Erode(eroded, img_width, img_height, 
                                             erosion_dilation[erosion_dilation_level] - erosion_dilation[erosion_dilation_level-1], 
                                             eroded);
                    else
                        eroded = image.Erode(img_mono, img_width, img_height, 
                                             erosion_dilation[erosion_dilation_level], 
                                             null);
                    img_mono2 = eroded;
                    previous_eroded = true;
                }
                
                // dilate
                if (erosion_dilation[erosion_dilation_level] < 0)
                {
                    if (previous_dilated)
                        dilated = image.Dilate(dilated, img_width, img_height, 
                                               -erosion_dilation[erosion_dilation_level] + erosion_dilation[erosion_dilation_level-1], 
                                               dilated);
                    else
                        dilated = image.Dilate(img_mono, img_width, img_height, 
                                               -erosion_dilation[erosion_dilation_level], 
                                               null);
                    img_mono2 = dilated;
                    previous_dilated = true;
                }
                
                // just copy the original image
                if (erosion_dilation[erosion_dilation_level] == 0)
                {
                    if (erosion_dilation_level > 0)
                        img_mono2 = (byte[])img_mono.Clone();
                    else
                        img_mono2 = img_mono;
                    previous_eroded = false;
                    previous_dilated = false;
                }

#if SHOW_TIMINGS
                timer_erode_dilate.Stop();
                if (timer_erode_dilate.time_elapsed_mS > 20)
                    Console.WriteLine("DetectSquaresMono: erode/dilate  " + timer_erode_dilate.time_elapsed_mS.ToString() );

                stopwatch timer_edge_detection = new stopwatch();
                timer_edge_detection.Start();
#endif                
                // detect edges using canny algorithm
                edge_detector.Update(img_mono2, img_width, img_height);

#if SHOW_TIMINGS
                timer_edge_detection.Stop();
                if (timer_edge_detection.time_elapsed_mS > 20)
                    Console.WriteLine("DetectSquaresMono: edge detect  " + timer_edge_detection.time_elapsed_mS.ToString() );
#endif
                
                if (debug)
                {
                    string filename = "canny_magnitudes_" + erosion_dilation_level.ToString() + ".bmp";
                    byte[] magnitudes = new byte[img_width * img_height * 3];
                    edge_detector.ShowMagnitudes(magnitudes, img_width, img_height);
                    Bitmap magnitudes_bmp = new Bitmap(img_width, img_height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
                    BitmapArrayConversions.updatebitmap_unsafe(magnitudes, magnitudes_bmp);
                    magnitudes_bmp.Save(filename, System.Drawing.Imaging.ImageFormat.Bmp);
                }

#if SHOW_TIMINGS
                stopwatch timer_edge_joining = new stopwatch();
                timer_edge_joining.Start();
#endif

                // connect edges which are a short distance apart
                edge_detector.ConnectBrokenEdges(connect_edges_radius);
                
#if SHOW_TIMINGS
                timer_edge_joining.Stop();
                if (timer_edge_joining.time_elapsed_mS > 20)
                    Console.WriteLine("DetectSquaresMono: edge joining  " + timer_edge_joining.time_elapsed_mS.ToString() );
#endif
                
                // remove edges which are outside a circular region of interest
                if (circular_ROI_radius > 0)
                {
                    int circular_ROI_radius_sqr = circular_ROI_radius * circular_ROI_radius;                    
                    int half_width = img_width / 2;
                    int half_height = img_height / 2;
                    for (int i = edge_detector.edges.Count - 2; i >= 0; i -= 2)
                    {
                        int x = edge_detector.edges[i];
                        int y = edge_detector.edges[i+1];
                        int dx = x - half_width;
                        int dy = y - half_height;
                        int dist_sqr = (dx*dx)+(dy*dy);
                        if (dist_sqr > circular_ROI_radius_sqr)
                        {
                            edge_detector.edges.RemoveAt(i+1);
                            edge_detector.edges.RemoveAt(i);
                        }
                    }
                }
                edges = edge_detector.edges;

                // try different groupings
                for (int group_radius_index = 0; group_radius_index < grouping_radius_percent.Length; group_radius_index++)
                {               
#if SHOW_TIMINGS
                    stopwatch timer_grouping = new stopwatch();
                    timer_grouping.Start();
                    stopwatch timer_perim2 = new stopwatch();
                    timer_perim2.Start();
#endif
                    // group edges together into objects
                    List<List<int>> groups = 
                        GetGroups(edge_detector.edges, 
                                  img_width, img_height, image_border, 
                                  minimum_size_percent,
                                  false, max_search_depth, ignore_periphery, 
                                  grouping_radius_percent[group_radius_index]);
                    
#if SHOW_TIMINGS
                    timer_grouping.Stop();
                    if (timer_grouping.time_elapsed_mS > 20)
                        Console.WriteLine("DetectSquaresMono: edge grouping  " + timer_grouping.time_elapsed_mS.ToString() );
#endif
                    
                    if (groups != null)
                    {
#if SHOW_TIMINGS
                        stopwatch timer_aspect = new stopwatch();
                        timer_aspect.Start();
#endif

                        //List<List<int>> squares = groups;
                        
                        // get the set of edges with aspect ratio closest to square

                        List<List<int>> squares = GetValidGroups(groups,
                                                                 img_width, img_height,
                                                                 //minimum_aspect_ratio, maximum_aspect_ratio,
                                                                 minimum_size_percent);

#if SHOW_TIMINGS
                        timer_grouping.Stop();
                        if (timer_aspect.time_elapsed_mS > 20)
                            Console.WriteLine("DetectSquaresMono: aspect checking  " + timer_aspect.time_elapsed_mS.ToString() );
#endif
                        if (squares != null)                 
                        {
                            
                            for (int i = 0; i < squares.Count; i++)
                            {
#if SHOW_TIMINGS
                                stopwatch timer_periphery = new stopwatch();
                                timer_periphery.Start();
#endif

                                List<int> square = squares[i];
                                polygon2D perim = null;
                                List<List<int>> periphery = GetSquarePeriphery(square, erosion_dilation[erosion_dilation_level], ref perim);
                                
#if SHOW_TIMINGS
                                timer_periphery.Stop();
                                //if (timer_periphery.time_elapsed_mS > 20)
                                    Console.WriteLine("DetectSquaresMono: periphery detect  " + timer_periphery.time_elapsed_mS.ToString() );
#endif
                                
                                if (perim != null)
                                {
                                    float longest_side = perim.getLongestSide();
                                    float shortest_side = perim.getShortestSide();
                                    
                                    // not too small
                                    if (shortest_side * 100 / img_width > minimum_volume_percent)
                                    {
                                        float aspect = shortest_side / longest_side;
                                        
                                        if ((aspect > minimum_aspect_ratio) &&
                                            (aspect < maximum_aspect_ratio))
                                        {
                                            // check the angles
                                            bool angle_out_of_range = false;
                                            int vertex = 0;
                                            while ((vertex < perim.x_points.Count) &&
                                                   (!angle_out_of_range) && (vertex < 4))
                                            {
                                                float angle = perim.GetInteriorAngle(vertex);
                                                angle = angle / (float)Math.PI * 180;
                                                if ((angle < 70) || (angle > 110)) angle_out_of_range = true;
                                                vertex++;
                                            }
                                            
                                            if (!angle_out_of_range)
                                            {
                                                float aspect1 = perim.getSideLength(0) / perim.getSideLength(2);
                                                if ((aspect1 > 0.9f) && (aspect1 < 1.1f))
                                                {                                        
                                                    float aspect2 = perim.getSideLength(1) / perim.getSideLength(3);
                                                    if ((aspect2 > 0.9f) && (aspect2 < 1.1f))
                                                        square_shapes.Add(perim);
                                                }
                                                
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
#if SHOW_TIMINGS
                    timer_perim2.Stop();
                    if (timer_perim2.time_elapsed_mS > 20)
                        Console.WriteLine("DetectSquaresMono: grouping and periphery " + timer_perim2.time_elapsed_mS.ToString());
#endif
                }
            }

            // remove any overlapping squares
            square_shapes = SelectSquares(square_shapes, 0);
            
#if SHOW_TIMINGS
            timer_square_detection.Stop();
            if (timer_square_detection.time_elapsed_mS > 20)
                Console.WriteLine("DetectSquaresMono: " + timer_square_detection.time_elapsed_mS.ToString() );
#endif
            return(square_shapes);
        }
Exemple #5
0
        /// <summary>
        /// returns a polygon representing the periphery of a square region
        /// </summary>
        /// <param name="edges">
        /// edges within the square region <see cref="List`1"/>
        /// </param>
        /// <param name="erode_dilate">
        /// erosion or dilation level <see cref="System.Int32"/>
        /// </param>
        /// <param name="perim">
        /// returned periphery <see cref="polygon2D"/>
        /// </param>
        /// <returns>
        /// set of edges around the periphery <see cref="List`1"/>
        /// </returns>
        private static List<List<int>> GetSquarePeriphery(List<int> edges,
                                                          int erode_dilate,
                                                          ref polygon2D perim)
        {
            List<List<int>> result = new List<List<int>>();
            perim = new polygon2D();
            
            // find the bounding box for all edges
            int tx = 99999;
            int ty = 99999;
            int bx = -99999;
            int by = -99999;            
            for (int i = edges.Count-2; i >= 0; i -= 2)
            {
                int x = edges[i];
                int y = edges[i+1];
                if (x < tx) tx = x;
                if (y < ty) ty = y;
                if (x > bx) bx = x;
                if (y > by) by = y;
            }

            int w = bx - tx;
            int h = by - ty;
            
            if ((w > 0) && (h > 0))
            {                
                int[] left = new int[h+1];
                int[] right = new int[h+1];
                int[] top = new int[w+1];
                int[] bottom = new int[w+1];
                for (int i = edges.Count - 2; i >= 0; i -= 2)
                {
                    int x = edges[i];
                    int x2 = x - tx;
                    int y = edges[i+1];
                    int y2 = y - ty;
                    
                    // left side
                    if ((left[y2] == 0) ||
                        (x < left[y2]))
                        left[y2] = x;

                    // right side
                    if ((right[y2] == 0) ||
                        (x > right[y2]))
                        right[y2] = x;
                    
                    // top
                    if ((top[x2] == 0) ||
                        (y < top[x2]))
                        top[x2] = y;

                    // bottom
                    if ((bottom[x2] == 0) ||
                        (y > bottom[x2]))
                        bottom[x2] = y;
                }

#if SHOW_TIMINGS
                stopwatch timer_best_fit = new stopwatch();
                timer_best_fit.Start();
#endif

                // find a best fit line for the left side
                int best_start = 0;
                int best_end = 0;
                int hits = BestFitLine(left, ref best_start, ref best_end);
                float left_x0 = left[best_start];
                float left_y0 = ty + best_start;
                float left_x1 = left[best_end];
                float left_y1 = ty + best_end;
                /*
                BestFitLineAverage(left, 
                                   ref left_x0, ref left_y0, 
                                   ref left_x1, ref left_y1);
                left_y0 += ty;
                left_y1 += ty;
                */

                // find a best fit line for the right side
                best_start = 0;
                best_end = 0;
                hits = BestFitLine(right, ref best_start, ref best_end);
                float right_x0 = right[best_start];
                float right_y0 = ty + best_start;
                float right_x1 = right[best_end];
                float right_y1 = ty + best_end;

                /*
                BestFitLineAverage(right,
                                   ref right_x0, ref right_y0,
                                   ref right_x1, ref right_y1);
                right_y0 += ty;
                right_y1 += ty;
                 */

                // find a best fit line for the top side
                best_start = 0;
                best_end = 0;
                hits = BestFitLine(top, ref best_start, ref best_end);
                float top_x0 = tx + best_start;
                float top_y0 = top[best_start];
                float top_x1 = tx + best_end;
                float top_y1 = top[best_end];

                /*
                BestFitLineAverage(top,
                                   ref top_x0, ref top_y0,
                                   ref top_x1, ref top_y1);
                top_x0 += tx;
                top_x1 += tx;
                */

                // find a best fit line for the bottom side
                best_start = 0;
                best_end = 0;
                hits = BestFitLine(bottom, ref best_start, ref best_end);
                float bottom_x0 = tx + best_start;
                float bottom_y0 = bottom[best_start];
                float bottom_x1 = tx + best_end;
                float bottom_y1 = bottom[best_end];

                /*
                BestFitLineAverage(bottom,
                                   ref bottom_x0, ref bottom_y0,
                                   ref bottom_x1, ref bottom_y1);
                bottom_x0 += tx;
                bottom_x1 += tx;
                 */

#if SHOW_TIMINGS
                timer_best_fit.Stop();
                if (timer_best_fit.time_elapsed_mS > 20)
                    Console.WriteLine("GetSquarePeriphery: best fit  " + timer_best_fit.time_elapsed_mS.ToString() );
#endif                
                
                // find the intersection between the left side and the top side
                float ix=0;
                float iy = 0;
                geometry.intersection(left_x1, left_y1, left_x0, left_y0,
                                      top_x1, top_y1, top_x0, top_y0,
                                      ref ix, ref iy);
                perim.Add(ix, iy);

                // find the intersection between the right side and the top side
                ix = 0;
                iy = 0;
                geometry.intersection(right_x1, right_y1, right_x0, right_y0,
                                      top_x0, top_y0, top_x1, top_y1,
                                      ref ix, ref iy);
                perim.Add(ix, iy);

                // find the intersection between the right side and the bottom side
                ix = 0;
                iy = 0;
                geometry.intersection(right_x1, right_y1, right_x0, right_y0,
                                      bottom_x0, bottom_y0, bottom_x1, bottom_y1,
                                      ref ix, ref iy);
                perim.Add(ix, iy);

                // find the intersection between the left side and the bottom side
                ix = 0;
                iy = 0;
                geometry.intersection(left_x1, left_y1, left_x0, left_y0,
                                      bottom_x0, bottom_y0, bottom_x1, bottom_y1,
                                      ref ix, ref iy);
                perim.Add(ix, iy);
                                
                // left and right
                List<int> left_edges = new List<int>();
                List<int> right_edges = new List<int>();
                for (int y = h; y >= 0; y--)
                {
                    if (left[y] != 0)
                    {
                        left_edges.Add(left[y]);
                        left_edges.Add(ty + y);
                    }
                    if (right[y] != 0)
                    {
                        right_edges.Add(right[y]);
                        right_edges.Add(ty + y);
                    }
                }
                
                // top and bottom
                List<int> top_edges = new List<int>();
                List<int> bottom_edges = new List<int>();
                for (int x = w; x >= 0; x--)
                {
                    if (top[x] != 0)
                    {
                        top_edges.Add(tx + x);
                        top_edges.Add(top[x]);
                    }
                    if (bottom[x] != 0)
                    {
                        bottom_edges.Add(tx + x);
                        bottom_edges.Add(bottom[x]);
                    }
                }
                
                float aspect_check = perim.getShortestSide() / perim.getLongestSide();
                if (aspect_check > 0.2f)
                {                
                    result.Add(left_edges);
                    result.Add(right_edges);
                    result.Add(top_edges);
                    result.Add(bottom_edges);
                }
                else perim = null;
            }

            // shrink the perimeter according to the erosion/dilation value
            if ((perim != null) && (erode_dilate != 0))
            {
                if (perim.x_points != null)
                {
                    float shrink_percent = (erode_dilate*2) / (perim.getPerimeterLength()/4.0f);
                    perim = perim.Scale(1.0f - shrink_percent);
                }
                else perim = null;
            }
            
            return(result);
        }
Exemple #6
0
        public static List<List<int>> GetGroups(List<int>edges,
                                                int img_width, int img_height,
                                                int image_border,
                                                int minimum_size_percent,
                                                bool squares_only,
                                                int max_search_depth,
                                                bool ignore_periphery,
                                                int grouping_radius_percent)
        {
#if SHOW_TIMINGS
                stopwatch timer_grouping = new stopwatch();
                timer_grouping.Start();
#endif
            const int compression = 4;
            
            int grouping_radius = grouping_radius_percent * img_width / (100 * compression);
            List<List<int>> groups = new List<List<int>>();            
                        
            // find line segments of significant length
            List<float> centres = null;
            List<float> bounding_boxes = null;
            List<List<int>> line_segments = 
                DetectLongestPerimeters(edges, 
                                        img_width, img_height, 
                                        image_border, 
                                        minimum_size_percent, 
                                        squares_only,
                                        max_search_depth,
                                        ignore_periphery,
                                        ref centres,
                                        ref bounding_boxes);

            bool[][] grouping_matrix = new bool[line_segments.Count][];
            for (int i = 0; i < line_segments.Count; i++)
                grouping_matrix[i] = new bool[line_segments.Count];
                        
            // map out the line segments
            const int step_size = 4;
            int ty = img_height-1;
            int by = 0;
            int tx = img_width-1;
            int bx = 0;
            int[,] line_segment_map = new int[(img_width/compression)+2, (img_height/compression)+2];
            for (int i = 0; i < line_segments.Count; i++)
            {
                List<int> line_segment = line_segments[i];
                for (int j = 0; j < line_segment.Count; j += step_size)
                {
                    int x = line_segment[j]/compression;
                    int y = line_segment[j+1]/compression;
                    if (line_segment_map[x, y] > 0)
                    {
                        if (line_segment_map[x, y] != i + 1)
                        {
                            int segment_index1 = line_segment_map[x, y]-1; 
                            int segment_index2 = i;
                            
                            // link the two segments
                            grouping_matrix[segment_index1][segment_index2] = true;
                            grouping_matrix[segment_index2][segment_index1] = true;
                        }
                    }
                    line_segment_map[x, y] = i + 1;
                    if (x < tx) tx = x;
                    if (x > bx) bx = x;
                    if (y < ty) ty = y;
                    if (y > by) by = y;
                }
            }
            
            // horizontal grouping
            for (int y = ty; y <= by; y++)
            {
                int prev_segment_index = -1;
                int prev_segment_x = -1;
                for (int x = tx; x <= bx; x++)
                {
                    int segment_index = line_segment_map[x, y];
                    if (segment_index > 0)
                    {
                        if (prev_segment_x > -1)
                        {
                            int dx = x - prev_segment_x;
                            if (dx < grouping_radius)
                            {
                                if (!grouping_matrix[segment_index-1][prev_segment_index])
                                {
                                    // get the line segment indexes
                                    segment_index--;                                    
                                    
                                    if (segment_index != prev_segment_index)
                                    {                                
                                        // link the two segments
                                        grouping_matrix[segment_index][prev_segment_index] = true;
                                        grouping_matrix[prev_segment_index][segment_index] = true;
                                    }
                                }
                            }
                        }
                        prev_segment_x = x;
                        prev_segment_index = line_segment_map[x, y]-1;
                    }
                }
            }

            // horizontal grouping
            for (int x = tx; x <= bx; x++)
            {
                int prev_segment_y = -1;
                int prev_segment_index = -1;
                for (int y = ty; y <= by; y++)
                {
                    int segment_index = line_segment_map[x, y];
                    if (segment_index > 0)
                    {
                        if (prev_segment_y > -1)
                        {
                            int dy = y - prev_segment_y;
                            if (dy < grouping_radius)
                            {
                                if (!grouping_matrix[segment_index-1][prev_segment_index])
                                {
                                    // get the line segment indexes
                                    segment_index--;
                                    
                                    if (segment_index != prev_segment_index)
                                    {                                
                                        // link the two segments
                                        grouping_matrix[segment_index][prev_segment_index] = true;
                                        grouping_matrix[prev_segment_index][segment_index] = true;
                                    }
                                }
                            }                            
                        }
                        prev_segment_y = y;
                        prev_segment_index = line_segment_map[x, y]-1;                        
                    }
                }
            }
                        
            // turn grouping matrix into a hypergraph
            hypergraph graph = new hypergraph(grouping_matrix.Length, 1);
            for (int i = 0; i < grouping_matrix.Length; i++)
            {
                for (int j = 0; j < grouping_matrix[i].Length; j++)
                {
                    if (grouping_matrix[i][j])
                        graph.LinkByIndex(j, i);
                }
            }
            
            // detect connected sets within the hypergraph
            for (int i = 0; i < graph.Nodes.Count; i++)
            {
                bool already_grouped = graph.GetFlagByIndex(i, 0);
                if (!already_grouped)
                {
                    List<hypergraph_node> connected_set = new List<hypergraph_node>();
                    graph.PropogateFlagFromIndex(i, 0, connected_set);
                    
                    List<hypergraph_node> members = new List<hypergraph_node>();
                    for (int j = 0; j < connected_set.Count; j++)
                    {
                        if (!members.Contains(connected_set[j])) members.Add(connected_set[j]);
                    }

                    List<int> group_members = new List<int>();
                    for (int j = 0; j < members.Count; j++)
                    {
                        int line_segment_index = members[j].ID;
                        List<int> perim = line_segments[line_segment_index];
                        for (int k = 0; k < perim.Count; k++)
                            group_members.Add(perim[k]);
                    }
                    groups.Add(group_members);
                    
                }
            }
            
#if SHOW_TIMINGS
            timer_grouping.Stop();
            //if (timer_grouping.time_elapsed_mS > 20)
                Console.WriteLine("GetGroups: " + timer_grouping.time_elapsed_mS.ToString() );
#endif                
            
            return(groups);
        }