NewLife/X

LTTB降采样算法分析
智能大石头 authored at 2021-10-25 01:25:52
d8d31ef
Tree
1 Parent(s) 470aa34
Summary: 5 changed files with 159 additions and 36 deletions.
Added +0 -0
Added +0 -0
Modified +4 -4
Modified +87 -10
Modified +68 -22
Added +0 -0
diff --git "a/Doc/\351\231\215\351\207\207\346\240\267/LTTB\351\231\215\351\207\207\346\240\267.png" "b/Doc/\351\231\215\351\207\207\346\240\267/LTTB\351\231\215\351\207\207\346\240\267.png"
new file mode 100644
index 0000000..f9277f0
Binary files /dev/null and "b/Doc/\351\231\215\351\207\207\346\240\267/LTTB\351\231\215\351\207\207\346\240\267.png" differ
Added +0 -0
diff --git "a/Doc/\351\231\215\351\207\207\346\240\267/LTTB\351\231\215\351\207\207\346\240\267.xlsx" "b/Doc/\351\231\215\351\207\207\346\240\267/LTTB\351\231\215\351\207\207\346\240\267.xlsx"
new file mode 100644
index 0000000..4502162
Binary files /dev/null and "b/Doc/\351\231\215\351\207\207\346\240\267/LTTB\351\231\215\351\207\207\346\240\267.xlsx" differ
Modified +4 -4
diff --git a/NewLife.Core/Algorithms/LTOBDownSampling.cs b/NewLife.Core/Algorithms/LTOBDownSampling.cs
index 7883a66..c461396 100644
--- a/NewLife.Core/Algorithms/LTOBDownSampling.cs
+++ b/NewLife.Core/Algorithms/LTOBDownSampling.cs
@@ -42,14 +42,14 @@ namespace NewLife.Algorithms
             var step = (Double)(data_length - 2) / (threshold - 2);
 
             // 三角形选择当前同相邻三个ABC点,选择B,使得三角形有效面积最大
-            for (var i = 1; i < threshold - 1; i++)
+            for (var i = 0; i < threshold - 2; i++)
             {
                 // 计算每个点的有效区域,并选取有效区域最大的点作为桶的代表点
                 TimePoint point = default;
 
                 // 获取当前桶的范围
-                var start = (Int32)Math.Round((i - 1 + 0) * step) + 1;
-                var end = (Int32)Math.Round((i - 1 + 1) * step) + 1;
+                var start = (Int32)Math.Round((i + 0) * step) + 1;
+                var end = (Int32)Math.Round((i + 1) * step) + 1;
                 end = end < data_length ? end : data_length;
 
                 var max_area = -1.0;
@@ -70,7 +70,7 @@ namespace NewLife.Algorithms
                     }
                 }
 
-                sampled[i] = point;
+                sampled[i + 1] = point;
             }
 
             // 第一个点和最后一个点
Modified +87 -10
diff --git a/NewLife.Core/Algorithms/LTTBDownSampling.cs b/NewLife.Core/Algorithms/LTTBDownSampling.cs
index f047995..81b1456 100644
--- a/NewLife.Core/Algorithms/LTTBDownSampling.cs
+++ b/NewLife.Core/Algorithms/LTTBDownSampling.cs
@@ -33,15 +33,14 @@ namespace NewLife.Algorithms
             if (data == null || data.Length < 2) return data;
             if (threshold < 2 || threshold >= data.Length) return data;
 
+            if (AlignMode != AlignModes.None) return ProcessByAlign(data, threshold);
+
             var data_length = data.Length;
             var sampled = new TimePoint[threshold];
 
             // 桶大小,预留开始结束位置
             var step = (Double)(data_length - 2) / (threshold - 2);
 
-            // 第一个点
-            sampled[0] = data[0];
-
             // 三角形选择相邻三个桶的ABC点,A是前一个桶选择点,C是后一个桶平均点,当前桶选择B,使得三角形有效面积最大
             TimePoint pointA = default;
             for (var i = 0; i < threshold - 2; i++)
@@ -49,8 +48,8 @@ namespace NewLife.Algorithms
                 // 计算下一个桶的平均点作为C
                 TimePoint pointC = default;
                 {
-                    var start = (Int32)Math.Floor((i + 1) * step) + 1;
-                    var end = (Int32)Math.Floor((i + 2) * step) + 1;
+                    var start = (Int32)Math.Round((i + 1) * step) + 1;
+                    var end = (Int32)Math.Round((i + 2) * step) + 1;
                     end = end < data_length ? end : data_length;
 
                     var length = end - start;
@@ -67,14 +66,14 @@ namespace NewLife.Algorithms
                 TimePoint point = default;
                 {
                     // 获取当前桶的范围
-                    var start = (Int32)Math.Floor((i + 0) * step) + 1;
-                    var end = (Int32)Math.Floor((i + 1) * step) + 1;
+                    var start = (Int32)Math.Round((i + 0) * step) + 1;
+                    var end = (Int32)Math.Round((i + 1) * step) + 1;
 
                     var max_area = -1.0;
-                    for (; start < end; start++)
+                    for (var j = start; j < end; j++)
                     {
                         // 选择一个点B,计算ABC三角形面积
-                        var pointB = data[start];
+                        var pointB = data[j];
                         var area = Math.Abs(
                             (pointA.Time - pointC.Time) * (pointB.Value - pointA.Value) -
                             (pointA.Time - pointB.Time) * (pointC.Value - pointA.Value)
@@ -91,10 +90,88 @@ namespace NewLife.Algorithms
                 pointA = point;
             }
 
-            // 最后一个点
+            // 第一个和最后一个点
+            sampled[0] = data[0];
             sampled[threshold - 1] = data[data_length - 1];
 
             return sampled;
         }
+
+        private TimePoint[] ProcessByAlign(TimePoint[] data, Int32 threshold)
+        {
+            var data_length = data.Length;
+            var sampled = new TimePoint[threshold];
+
+            var step = (Double)data_length / threshold;
+
+            // 三角形选择相邻三个桶的ABC点,A是前一个桶选择点,C是后一个桶平均点,当前桶选择B,使得三角形有效面积最大
+            TimePoint pointA = default;
+            for (var i = 0; i < threshold; i++)
+            {
+                // 计算下一个桶的平均点作为C
+                TimePoint pointC = default;
+                if (i == threshold - 1)
+                    pointC = data[data.Length - 1];
+                else
+                {
+                    var start = (Int32)Math.Round((i + 1) * step);
+                    var end = (Int32)Math.Round((i + 2) * step);
+                    end = end < data_length ? end : data_length;
+
+                    var length = end - start;
+                    for (; start < end; start++)
+                    {
+                        pointC.Time += data[start].Time;
+                        pointC.Value += data[start].Value;
+                    }
+                    pointC.Time /= length;
+                    pointC.Value /= length;
+                }
+
+                // 计算每个点的有效区域,并选取有效区域最大的点作为桶的代表点
+                TimePoint point = default;
+                {
+                    // 获取当前桶的范围
+                    var start = (Int32)Math.Round((i + 0) * step);
+                    var end = (Int32)Math.Round((i + 1) * step);
+
+                    var max_area = -1.0;
+                    for (var j = start; j < end; j++)
+                    {
+                        // 选择一个点B,计算ABC三角形面积
+                        var pointB = data[j];
+                        var area = Math.Abs(
+                            (pointA.Time - pointC.Time) * (pointB.Value - pointA.Value) -
+                            (pointA.Time - pointB.Time) * (pointC.Value - pointA.Value)
+                            ) / 2;
+                        if (area > max_area)
+                        {
+                            max_area = area;
+                            point = pointB;
+                        }
+                    }
+
+                    pointA = point;
+
+                    // 对齐
+                    switch (AlignMode)
+                    {
+                        case AlignModes.Left:
+                            point.Time = data[start].Time;
+                            break;
+                        case AlignModes.Right:
+                            point.Time = data[end - 1].Time;
+                            break;
+                        case AlignModes.Center:
+                            point.Time = data[(Int32)Math.Round((start + end) / 2.0)].Time;
+                            break;
+                    }
+
+                    sampled[i] = point;
+                }
+            }
+
+            return sampled;
+        }
     }
 }
\ No newline at end of file
Modified +68 -22
diff --git a/XUnitTest.Core/Algorithms/LttbDownSamplingTests.cs b/XUnitTest.Core/Algorithms/LttbDownSamplingTests.cs
index 8e50598..c3b9330 100644
--- a/XUnitTest.Core/Algorithms/LttbDownSamplingTests.cs
+++ b/XUnitTest.Core/Algorithms/LttbDownSamplingTests.cs
@@ -11,7 +11,31 @@ namespace XUnitTest.Algorithms
     public class LttbDownSamplingTests
     {
         [Fact]
-        public void Test1()
+        public void Normal500()
+        {
+            var data = ReadPoints();
+            var lttb = new LTTBDownSampling();
+            var sampled = lttb.Process(data, 500);
+            Assert.NotNull(sampled);
+            Assert.Equal(500, sampled.Length);
+
+            //var k = 0;
+            //using var csv2 = new CsvFile("Algorithms/sampled.csv");
+            //while (true)
+            //{
+            //    var line = csv2.ReadLine();
+            //    if (line == null) break;
+
+            //    Assert.Equal(line[0].ToInt(), sampled[k].Time);
+            //    Assert.True(Math.Abs(line[1].ToDouble() - sampled[k].Value) < 0.0001);
+
+            //    k++;
+            //}
+
+            WritePoints(sampled, lttb.AlignMode);
+        }
+
+        private TimePoint[] ReadPoints()
         {
             using var csv = new CsvFile("Algorithms/source.csv");
             var data = new List<TimePoint>();
@@ -22,33 +46,55 @@ namespace XUnitTest.Algorithms
 
                 data.Add(new TimePoint { Time = line[0].ToInt(), Value = (Single)line[1].ToDouble() });
             }
+            return data.ToArray();
+        }
 
-            var lttb = new LTTBDownSampling();
-            var sampled = lttb.Process(data.ToArray(), 500);
+        private void WritePoints(TimePoint[] data, AlignModes mode)
+        {
+            var f = $"Algorithms/lttb_{mode}_sampled.csv".GetFullPath();
+            if (File.Exists(f)) File.Delete(f);
+            using var csv = new CsvFile(f, true);
+            for (var i = 0; i < data.Length; i++)
+            {
+                csv.WriteLine(data[i].Time, data[i].Value);
+            }
+            csv.Dispose();
+        }
+
+        [Fact]
+        public void AlignLeftTest()
+        {
+            var data = ReadPoints();
+            var lttb = new LTTBDownSampling { AlignMode = AlignModes.Left };
+            var sampled = lttb.Process(data, 100);
             Assert.NotNull(sampled);
-            Assert.Equal(500, sampled.Length);
+            Assert.Equal(100, sampled.Length);
 
-            var k = 0;
-            using var csv2 = new CsvFile("Algorithms/sampled.csv");
-            while (true)
-            {
-                var line = csv2.ReadLine();
-                if (line == null) break;
+            WritePoints(sampled, lttb.AlignMode);
+        }
 
-                Assert.Equal(line[0].ToInt(), sampled[k].Time);
-                Assert.True(Math.Abs(line[1].ToDouble() - sampled[k].Value) < 0.0001);
+        [Fact]
+        public void AlignRightTest()
+        {
+            var data = ReadPoints();
+            var lttb = new LTTBDownSampling { AlignMode = AlignModes.Right };
+            var sampled = lttb.Process(data, 500);
+            Assert.NotNull(sampled);
+            Assert.Equal(500, sampled.Length);
 
-                k++;
-            }
+            WritePoints(sampled, lttb.AlignMode);
+        }
 
-            var f = $"Algorithms/lttb_{lttb.AlignMode}_sampled.csv".GetFullPath();
-            if (File.Exists(f)) File.Delete(f);
-            using var csv3 = new CsvFile(f, true);
-            for (var i = 0; i < sampled.Length; i++)
-            {
-                csv3.WriteLine(sampled[i].Time, sampled[i].Value);
-            }
-            csv3.Dispose();
+        [Fact]
+        public void AlignCenterTest()
+        {
+            var data = ReadPoints();
+            var lttb = new LTTBDownSampling { AlignMode = AlignModes.Center };
+            var sampled = lttb.Process(data, 500);
+            Assert.NotNull(sampled);
+            Assert.Equal(500, sampled.Length);
+
+            WritePoints(sampled, lttb.AlignMode);
         }
     }
 }
\ No newline at end of file