16 Commits

127 changed files with 757 additions and 112 deletions
+4
View File
@@ -1,5 +1,9 @@
# Track Trends Alpha 1.0
# Documentation
[Lien vers le repo de doc](https://gitea.parano.ch/Maxluli/TrackTrendsDoc)
# Installation
As this project is a little finnicky, installing it and making it run is quite a job. Grab a coffe and lets begin.
+6
View File
@@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrackTrends", "TrackTrends\
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BAFC8496-36AE-4539-94DE-0E505E7A8F93}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrackTrendsTests", "TrackTrendsTests\TrackTrendsTests.csproj", "{9BE68783-F589-479A-8CFA-9652C3B23A37}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -17,6 +19,10 @@ Global
{F6694884-3B45-4017-9C9A-A0AFFC508245}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F6694884-3B45-4017-9C9A-A0AFFC508245}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F6694884-3B45-4017-9C9A-A0AFFC508245}.Release|Any CPU.Build.0 = Release|Any CPU
{9BE68783-F589-479A-8CFA-9652C3B23A37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9BE68783-F589-479A-8CFA-9652C3B23A37}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9BE68783-F589-479A-8CFA-9652C3B23A37}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9BE68783-F589-479A-8CFA-9652C3B23A37}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
+2 -2
View File
@@ -1,8 +1,8 @@
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// Date : 09/06/2023
/// File : DataWrapper.cs
/// Brief : Class that is used to interface between the main Form (vue) and the Storage (wich is a class that wraps the sqlite database, so the model) its almost MVC :D
/// Version : Alpha 1.0
/// Version : Beta 1.0
using System;
using System.Collections.Generic;
+2 -2
View File
@@ -1,8 +1,8 @@
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// Date : 09/06/2023
/// File : DriverData.cs
/// Brief : File containing classes that behave just like structures to store data about drivers
/// Version : Alpha 1.0
/// Version : Beta 1.0
using System;
using System.Collections.Generic;
+7 -3
View File
@@ -1,8 +1,8 @@
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// Date : 09/06/2023
/// File : DriverDrsWindow.cs
/// Brief : Window containing DRS related method and infos
/// Version : Alpha 1.0
/// Version : Beta 1.0
using System;
using System.Collections.Generic;
@@ -15,7 +15,7 @@ using Tesseract;
namespace TrackTrends
{
internal class DriverDrsWindow:Window
public class DriverDrsWindow:Window
{
private static int EmptyDrsGreenValue = -1;
private static Random rnd = new Random();
@@ -30,6 +30,10 @@ namespace TrackTrends
public override object DecodePng()
{
bool result = false;
//DEBUG
//WindowImage.Save("./DRS/"+rnd.Next(0,99999)+".png");
int greenValue = GetGreenPixels();
if (EmptyDrsGreenValue == -1)
EmptyDrsGreenValue = greenValue;
+1 -1
View File
@@ -13,7 +13,7 @@ using System.Threading.Tasks;
namespace TrackTrends
{
internal class DriverGapToLeaderWindow:Window
public class DriverGapToLeaderWindow:Window
{
public DriverGapToLeaderWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine)
{
+3 -3
View File
@@ -1,8 +1,8 @@
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// Date : 09/06/2023
/// File : DriverLapTimeWindow
/// Brief : Window containing infos about the lap time of a driver
/// Version : Alpha 1.0
/// Version : Beta 1.0
using System;
using System.Collections.Generic;
@@ -13,7 +13,7 @@ using System.Drawing;
namespace TrackTrends
{
internal class DriverLapTimeWindow:Window
public class DriverLapTimeWindow:Window
{
public DriverLapTimeWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine)
{
+2 -2
View File
@@ -1,8 +1,8 @@
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// Date : 09/06/2023
/// File : DriverNameWindow
/// Brief : Window containing infos about the name of the driver
/// Version : Alpha 1.0
/// Version : Beta 1.0
using System;
using System.Collections.Generic;
+2 -2
View File
@@ -1,8 +1,8 @@
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// Date : 09/06/2023
/// File : DriverPositionWindow.cs
/// Brief : Window containing infos about the position of a driver.
/// Version : Alpha 1.0
/// Version : Beta 1.0
using System;
using System.Collections.Generic;
+3 -3
View File
@@ -1,8 +1,8 @@
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// Date : 09/06/2023
/// File : DriverSectorWindow.cs
/// Brief : Window containing infos about a driver sector time. Can be the first second or third, does not matter.
/// Version : Alpha 1.0
/// Version : Beta 1.0
using System;
using System.Collections.Generic;
@@ -13,7 +13,7 @@ using System.Drawing;
namespace TrackTrends
{
internal class DriverSectorWindow:Window
public class DriverSectorWindow:Window
{
public DriverSectorWindow(Bitmap image, Rectangle bounds, int sectorId, bool generateEngine = true) : base(image, bounds,generateEngine)
{
+6 -2
View File
@@ -1,8 +1,8 @@
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// Date : 09/06/2023
/// File : DriverTyresWindow.cs
/// Brief : Window containing infos about a driver's tyre
/// Version : Alpha 1.0
/// Version : Beta 1.0
using System;
using System.Collections.Generic;
@@ -15,6 +15,7 @@ namespace TrackTrends
{
public class DriverTyresWindow:Window
{
private static Random rnd = new Random();
//Those are the colors I found but you can change them if they change in the future like in 2019
public static Color SOFT_TYRE_COLOR = Color.FromArgb(0xff, 0x00, 0x00);
public static Color MEDIUM_TYRE_COLOR = Color.FromArgb(0xf5, 0xbf, 0x00);
@@ -41,6 +42,9 @@ namespace TrackTrends
/// <returns>A tyre object containing tyre infos</returns>
private Tyre GetTyreInfos()
{
//DEBUG
//WindowImage.Save("./Tyre/raw_"+rnd.Next(0,99999)+".png");
Bitmap tyreZone = GetSmallBitmapFromBigOne(WindowImage, FindTyreZone());
Tyre.Type type = Tyre.Type.Undefined;
type = GetTyreTypeFromColor(OcrImage.GetAvgColorFromBitmap(tyreZone));
+14 -2
View File
@@ -1,8 +1,8 @@
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// Date : 09/06/2023
/// File : F1TVEmulator.cs
/// Brief : Class that contains methods to emulate a browser and navigate the F1TV website
/// Version : Alpha 1.0
/// Version : Beta 1.0
using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;
@@ -187,6 +187,18 @@ namespace TrackTrends
return 104;
}
try
{
IWebElement LiveButton = Driver.FindElement(By.ClassName("btn-manage-account"));
//By.ClassName("btn btn-controls btn-main btn-manage-account no-redirect")
LiveButton.Click();
}
catch
{
Console.Write("Going for a rediff");
}
//Again waits for the page to fully load (when you accept cookies it takes a little time for the page to load)
//Cannot use The timeout because the feed loading is not really loading so there is not event or anything
Thread.Sleep(5000);
+90 -8
View File
@@ -1,4 +1,10 @@
using System;
/// Author : Maxime Rohmer
/// Date : 09/06/2023
/// File : Form1.cs
/// Brief : Class that controls the main view of the app
/// Version : Beta 1.0
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
@@ -15,7 +21,6 @@ namespace TrackTrends
{
public partial class Main : Form
{
//private Reader Reader = null;
private F1TVEmulator Emulator = null;
private DataWrapper Wrapper = null;
private bool cancelRequested = false;
@@ -47,7 +52,10 @@ namespace TrackTrends
{
InitializeComponent();
}
public async void RefreshUI()
/// <summary>
/// Will update everything that is not data related
/// </summary>
public void RefreshUI()
{
if (Directory.Exists(ConfigurationTool.CONFIGS_FOLDER_NAME))
{
@@ -55,7 +63,12 @@ namespace TrackTrends
lsbPresets.DataSource = Directory.GetFiles(ConfigurationTool.CONFIGS_FOLDER_NAME);
}
}
private async void btnSettings_Click(object sender, EventArgs e)
/// <summary>
/// Opens the settings page. Also disposes of the browser if there is one opened and all thos things
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnSettings_Click(object sender, EventArgs e)
{
if (Emulator != null)
Emulator.ResetDriver();
@@ -88,6 +101,7 @@ namespace TrackTrends
}
else
{
//Should technically never show up but we never know
MessageBox.Show("The config file has not been found please return to the config and change it");
}
}
@@ -97,7 +111,11 @@ namespace TrackTrends
//MessageBox.Show("There is no URL for the Grand Prix you want to decode. Please return to the config and add a valid one");
}
}
/// <summary>
/// Will do everything that needs to be done at the first start of the app
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Form1_Load(object sender, EventArgs e)
{
//Those are the default values but they will need to be changed later when the configuration has been done
@@ -106,6 +124,8 @@ namespace TrackTrends
tbxGpUrl.Text = GpUrl;
this.DoubleBuffered = true;
oldSize = this.Size;
oldRankingSize = gpbxRanking.Size;
oldLapTimesSize = gpbxLapTimes.Size;
@@ -134,7 +154,11 @@ namespace TrackTrends
RefreshUI();
}
/// <summary>
/// Will start or stop the process of decoding
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void btnUpdate_Click(object sender, EventArgs e)
{
cancelRequested = false;
@@ -192,18 +216,32 @@ namespace TrackTrends
btnSettings.Enabled = true;
}
}
/// <summary>
/// Will display the overtakes in the overtakes list box
/// </summary>
private void DisplayOvertakes()
{
Wrapper.DisplayOvertakes(lsbOvertakes);
}
/// <summary>
/// Will display the battles in the battles pannel
/// </summary>
private void DisplayBattles()
{
Wrapper.DisplayBattles(pnlBattles, this);
}
/// <summary>
/// Will display the time differences in the faster and slowest pannels
/// </summary>
private void DisplayDeltas()
{
Wrapper.DisplayTimesDeltas(pnlFastest, pnlSlowest, this);
}
/// <summary>
/// Will try to stop the emulator (usually does not work please do not count on it)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (Emulator != null)
@@ -211,6 +249,12 @@ namespace TrackTrends
Emulator.Stop();
}
}
/// <summary>
/// Will display the live ranking on the live ranking pannel. Its called like this because historically it was the method that just recovered the bare results from the OCR
/// </summary>
/// <param name="errorCode"></param>
/// <param name="sw"></param>
/// <param name="screen"></param>
private void DisplayResults(int errorCode, Stopwatch sw, Bitmap screen)
{
if (errorCode != 0)
@@ -224,6 +268,11 @@ namespace TrackTrends
}
}
/// <summary>
/// Will stop the data recovering operation and resets some buttons and text
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnStopUpdating_Click(object sender, EventArgs e)
{
// Set the cancellation flag
@@ -232,6 +281,11 @@ namespace TrackTrends
btnResetEmulator.Enabled = false;
btnStopUpdating.Text = "Stopping";
}
/// <summary>
/// Will start the F1TVEmulator, again this name is historical because back at the start of this project this button did not have a name
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void button1_Click(object sender, EventArgs e)
{
lsbOvertakes.Items.Clear();
@@ -301,6 +355,11 @@ namespace TrackTrends
btnStartDecoding.Enabled = true;
}
}
/// <summary>
/// Silly way to remove borders from groupbox and make them look like pannels with titles
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void removeBorders(object sender, PaintEventArgs e)
{
GroupBox gpbx = (GroupBox)sender;
@@ -317,17 +376,31 @@ namespace TrackTrends
e.Graphics.DrawString(gpbx.Text, gpbx.Font, brush, textPosition);
}
}
/// <summary>
/// Will change the preset to use when starting the emulator
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void lsbPresets_SelectedIndexChanged(object sender, EventArgs e)
{
if (lsbPresets.SelectedIndex >= 0)
ConfigFile = lsbPresets.Items[lsbPresets.SelectedIndex].ToString();
}
/// <summary>
/// Will change the URL the emulator will use, historical name again
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void textBox1_TextChanged(object sender, EventArgs e)
{
if (tbxGpUrl.Text != "")
GpUrl = tbxGpUrl.Text;
}
/// <summary>
/// This is called by the automatically generated buttons. Its here to fill in the driver info tab whenever the user clicks on a button that contains the name of a driver
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void btnDriver_Click(object sender, EventArgs e)
{
//Removes the cover
@@ -381,6 +454,11 @@ namespace TrackTrends
break;
}
}
/// <summary>
/// This is supposed to be called by an automatically generated button. It should be any button with a laptime info on it
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void btnLapTime_Click(object sender, EventArgs e)
{
//Happens when a lapTime has been clicked
@@ -388,7 +466,11 @@ namespace TrackTrends
string[] parts = btn.Name.Split('_');
Wrapper.DisplayLapTimeInfos(parts[0], Convert.ToInt32(parts[1]), btn.Text);
}
/// <summary>
/// Will trigger responsive calculation everytime the form changes size
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Main_Resize(object sender, EventArgs e)
{
int xDiff = this.Width - oldSize.Width;
+57 -43
View File
@@ -1,8 +1,8 @@
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// Date : 09/06/2023
/// File : OcrImage.cs
/// Brief : Class containing all the methods used to enhance images for OCR
/// Version : Alpha 1.0
/// Version : Beta 1.0
using System;
using System.Collections.Generic;
@@ -19,6 +19,8 @@ namespace TrackTrends
//Any color that has any of its R,G or B channel higher than the treshold will be considered as being usefull information
public static Color F1TV_BACKGROUND_TRESHOLD = Color.FromArgb(0x50, 0x50, 0x50);
Bitmap InputBitmap;
Random rnd = new Random();
public enum WindowType
{
LapTime,
@@ -41,7 +43,7 @@ namespace TrackTrends
/// </summary>
/// <param name="type">The type of the window. Depending on it different enhancing features will be applied</param>
/// <returns>The enhanced Bitmap</returns>
public Bitmap Enhance(int id,WindowType type = WindowType.Text)
public Bitmap Enhance(WindowType type = WindowType.Text)
{
Bitmap outputBitmap = (Bitmap)InputBitmap.Clone();
//Note : If you plan to activate all the comments that I used to debug the OCR I would advise to make sure that the debug folder exists
@@ -71,7 +73,7 @@ namespace TrackTrends
case WindowType.LapTime:
//outputBitmap.Save(Window.LAPTIME_DEBUG_FOLDER + @"\raw_" + id + ".png");
outputBitmap = Tresholding(outputBitmap,185);
outputBitmap = Tresholding(outputBitmap, 185);
//outputBitmap.Save(Window.LAPTIME_DEBUG_FOLDER + @"\Treshold_" + id + ".png");
outputBitmap = SobelEdgeDetection(outputBitmap);
@@ -108,42 +110,53 @@ namespace TrackTrends
public static Bitmap Grayscale(Bitmap inputBitmap)
{
Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadOnly, inputBitmap.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
Bitmap resultBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height);
unsafe
{
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < inputBitmap.Height; y++)
byte* inputPtr = (byte*)bmpData.Scan0.ToPointer();
byte* resultPtr = (byte*)resultBitmap.LockBits(rect, ImageLockMode.WriteOnly, resultBitmap.PixelFormat).Scan0.ToPointer();
Parallel.For(0, inputBitmap.Height, y =>
{
byte* currentLine = ptr + (y * bmpData.Stride);
byte* currentLine = inputPtr + (y * bmpData.Stride);
byte* resultLine = resultPtr + (y * resultBitmap.Width * bytesPerPixel);
for (int x = 0; x < inputBitmap.Width; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
byte* inputPixel = currentLine + (x * bytesPerPixel);
byte* resultPixel = resultLine + (x * bytesPerPixel);
byte blue = pixel[0];
byte green = pixel[1];
byte red = pixel[2];
byte blue = inputPixel[0];
byte green = inputPixel[1];
byte red = inputPixel[2];
//Those a specific values to correct the weights so its more pleasing to the human eye
int gray = (int)(red * 0.3 + green * 0.59 + blue * 0.11);
//This is not a proper treshold method but it is helping the sobel edge detection
if(gray <= F1TV_BACKGROUND_TRESHOLD.R)
if (gray <= F1TV_BACKGROUND_TRESHOLD.R)
{
pixel[0] = pixel[1] = pixel[2] = 0;
resultPixel[0] = resultPixel[1] = resultPixel[2] = 0;
}
else
{
pixel[0] = pixel[1] = pixel[2] = (byte)gray;
}
resultPixel[0] = resultPixel[1] = resultPixel[2] = (byte)gray;
}
}
});
resultBitmap.UnlockBits(resultBitmap.LockBits(rect, ImageLockMode.WriteOnly, resultBitmap.PixelFormat));
}
inputBitmap.UnlockBits(bmpData);
return inputBitmap;
return resultBitmap;
}
/// <summary>
/// Method that uses the Sobel Edge detection to outline the edges of the characters to help with the OCR
/// </summary>
@@ -161,6 +174,7 @@ namespace TrackTrends
// Apply the Sobel operators and normalize the gradients
// NOTE: I dont know how easy or hard it would be to make this paralel but it could be a good idea to do so if possible.
//Parallel.For(1, grayscaleImage.Height - 1, y =>
for (int y = 1; y < grayscaleImage.Height - 1; y++)
{
for (int x = 1; x < grayscaleImage.Width - 1; x++)
@@ -175,7 +189,7 @@ namespace TrackTrends
edgesImage.SetPixel(x, y, Color.FromArgb(gradient, gradient, gradient));
}
}
}//);
return edgesImage;
}
@@ -207,38 +221,36 @@ namespace TrackTrends
/// </summary>
/// <param name="inputBitmap">The bitmap to vanish</param>
/// <returns></returns>
public static Bitmap VanishOxyAction(Bitmap inputBitmap)
public Bitmap VanishOxyAction(Bitmap inputBitmap)
{
Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
unsafe
{
//Note : MAKE THIS PARALELL OMG WY DID I LEFT IT LIKE THAT
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < inputBitmap.Height; y++)
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < inputBitmap.Width; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
BitmapData bitmapData = inputBitmap.LockBits(new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height), ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
int blue = (int)pixel[0];
int green = (int)pixel[1];
int red = (int)pixel[2];
int bytesPerPixel = System.Drawing.Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
int heightInPixels = bitmapData.Height;
int widthInBytes = bitmapData.Width * bytesPerPixel;
byte* PtrFirstPixel = (byte*)bitmapData.Scan0;
Parallel.For(0, heightInPixels, y =>
{
byte* currentLine = PtrFirstPixel + (y * bitmapData.Stride);
for (int x = 0; x < widthInBytes; x = x + bytesPerPixel)
{
int blue = currentLine[x];
int green = currentLine[x + 1];
int red = currentLine[x + 2];
int max = Math.Max(Math.Max(blue, green), red);
if (max > 255 / 3)
max = 255;
pixel[0] = pixel[1] = pixel[2] = (byte)max;
currentLine[x] = currentLine[x + 1] = currentLine[x + 2] = (byte)max;
}
});
inputBitmap.UnlockBits(bitmapData);
}
}
inputBitmap.UnlockBits(bmpData);
return inputBitmap;
}
/// <summary>
@@ -294,10 +306,12 @@ namespace TrackTrends
unsafe
{
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < inputBitmap.Height; y++)
int bmpHeight = inputBitmap.Height;
int bmpWidth = inputBitmap.Width;
Parallel.For(0, bmpHeight, y =>
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < inputBitmap.Width; x++)
for (int x = 0; x < bmpWidth; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
@@ -308,7 +322,7 @@ namespace TrackTrends
if (R <= F1TV_BACKGROUND_TRESHOLD.R && G <= F1TV_BACKGROUND_TRESHOLD.G && B <= F1TV_BACKGROUND_TRESHOLD.B)
pixel[0] = pixel[1] = pixel[2] = 0;
}
}
});
}
inputBitmap.UnlockBits(bmpData);
@@ -401,7 +415,7 @@ namespace TrackTrends
int G = pixel[1];
int R = pixel[2];
if (R >= F1TV_BACKGROUND_TRESHOLD.R +25|| G >= F1TV_BACKGROUND_TRESHOLD.G +25|| B >= F1TV_BACKGROUND_TRESHOLD.B +25)
if (R >= F1TV_BACKGROUND_TRESHOLD.R + 25 || G >= F1TV_BACKGROUND_TRESHOLD.G + 25 || B >= F1TV_BACKGROUND_TRESHOLD.B + 25)
{
pixel[0] = 0xFF;
pixel[1] = 0xFF;
@@ -458,7 +472,7 @@ namespace TrackTrends
}
inputBitmap.UnlockBits(bmpData);
return Color.FromArgb(255,Math.Min(Convert.ToInt32((float)totR / (float)totPixels),255), Math.Min(Convert.ToInt32((float)totG / (float)totPixels),255), Math.Min(Convert.ToInt32((float)totB / (float)totPixels),255));
return Color.FromArgb(255, Math.Min(Convert.ToInt32((float)totR / (float)totPixels), 255), Math.Min(Convert.ToInt32((float)totG / (float)totPixels), 255), Math.Min(Convert.ToInt32((float)totB / (float)totPixels), 255));
}
/// <summary>
/// This method simply inverts all the colors in a Bitmap
+2 -2
View File
@@ -1,8 +1,8 @@
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// Date : 09/06/2023
/// File : Reader.cs
/// Brief : Class used to Read the config file for the OCR
/// Version : Alpha 1.0
/// Version : Beta 1.0
using System;
using System.Collections.Generic;
-2
View File
@@ -294,7 +294,6 @@
this.pbxPreview.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
this.pbxPreview.TabIndex = 0;
this.pbxPreview.TabStop = false;
this.pbxPreview.Click += new System.EventHandler(this.pbxMain_Click);
this.pbxPreview.MouseClick += new System.Windows.Forms.MouseEventHandler(this.pbxMain_MouseClick);
//
// btnCreatZone
@@ -337,7 +336,6 @@
this.pbxWindowPreview.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
this.pbxWindowPreview.TabIndex = 0;
this.pbxWindowPreview.TabStop = false;
this.pbxWindowPreview.Click += new System.EventHandler(this.pbxDriverZone_Click);
this.pbxWindowPreview.MouseClick += new System.Windows.Forms.MouseEventHandler(this.pbxDriverZone_MouseClick);
//
// btnLoadPreset
+138 -28
View File
@@ -1,4 +1,10 @@
using System;
/// Author : Maxime Rohmer
/// Date : 09/06/2023
/// File : Settings.cs
/// Brief : Class that controls the settings view
/// Version : Beta 1.0
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
@@ -48,6 +54,9 @@ namespace TrackTrends
InitializeComponent();
Load();
}
/// <summary>
/// This methods regroups all the actions that the forms need to be doing at the first launch
/// </summary>
private void Load()
{
RefreshUI();
@@ -57,6 +66,11 @@ namespace TrackTrends
oldPbxPreviewSize = pbxPreview.Size;
oldPbxWindowPreviewSize = pbxWindowPreview.Size;
btnLoadPreset.Enabled = false;
btnDeletePreset.Enabled = false;
btnSavePreset.Enabled = false;
// I prefered regrouping all the tooltips here to make it easier to edit (there is 100% of thoses sentences containing typos so if you see one dont hesitate to edit those messages)
tip1.SetToolTip(btnCreatZone, "After clicking you can select two points in the image to set the bounds of the important data");
tip1.SetToolTip(btnCreateWindow, "After clicking this you will have to select all the windows that are important on the lower image. Refer to the documentation for more infos");
tip1.SetToolTip(btnRefresh, "Starts the emulator or refreshes the images if its already running");
@@ -66,6 +80,10 @@ namespace TrackTrends
tip1.SetToolTip(pbxPreview, "What the emulator returns");
tip1.SetToolTip(pbxWindowPreview, "One of the driver zones that the program managed to slice from the main zone");
}
/// <summary>
/// This is the main method that will be called anytime something changes on the view
/// It can be called at any time and will adapt the UI taking into account the state of the app
/// </summary>
private void RefreshUI()
{
lsbDrivers.DataSource = null;
@@ -116,12 +134,21 @@ namespace TrackTrends
pbxWindowPreview.Image = Config.MainZone.Zones[0].Draw();
}
}
/// <summary>
/// This will create a new zone but will require two points (one at each opposing sides and corners)
/// </summary>
/// <param name="p1">The first corner (usually top left)</param>
/// <param name="p2">The second corner (usually bottom right)</param>
private void CreateNewZone(Point p1, Point p2)
{
Rectangle dimensions = CreateAbsoluteRectangle(p1, p2);
Config = new ConfigurationTool((Bitmap)pbxPreview.Image, dimensions);
RefreshUI();
}
/// <summary>
/// Creates all the windows with an array of rectangles
/// </summary>
/// <param name="dimensions">An array that contains all the windows bounds and position (expects 9)</param>
private void CreateWindows(List<Rectangle> dimensions)
{
if (Config != null)
@@ -129,11 +156,20 @@ namespace TrackTrends
Config.AddWindows(dimensions);
}
}
/// <summary>
/// Will just change the main URL
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void tbxGpUrl_TextChanged(object sender, EventArgs e)
{
GrandPrixUrl = tbxGpUrl.Text;
}
/// <summary>
/// Adds a driver into the driver list
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnAddDriver_Click(object sender, EventArgs e)
{
string newDriver = tbxDriverName.Text;
@@ -141,7 +177,11 @@ namespace TrackTrends
tbxDriverName.Text = "";
RefreshUI();
}
/// <summary>
/// Removes a driver from the drivers list
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnRemoveDriver_Click(object sender, EventArgs e)
{
if (lsbDrivers.SelectedIndex >= 0)
@@ -150,6 +190,9 @@ namespace TrackTrends
}
RefreshUI();
}
/// <summary>
/// Will change everything that needs to be changed for when the users starts or stops creating a zone
/// </summary>
private void SwitchZoneCreation()
{
if (CreatingZone)
@@ -180,6 +223,9 @@ namespace TrackTrends
}
RefreshUI();
}
/// <summary>
/// Will change everything that needs to be changed for when the users starts or stops creating a window
/// </summary>
private void SwitchWindowCreation()
{
if (CreatingWindow)
@@ -211,6 +257,11 @@ namespace TrackTrends
{
SwitchWindowCreation();
}
/// <summary>
/// If the user is supposed to create a zone, will record the position of the clicks
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void pbxMain_MouseClick(object sender, MouseEventArgs e)
{
if (CreatingZone && pbxPreview.Image != null)
@@ -236,10 +287,11 @@ namespace TrackTrends
RefreshUI();
}
}
private void pbxMain_Click(object sender, EventArgs e)
{
//Not the right one to use visibly
}
/// <summary>
/// If the user is supposed to create a window, will record the position of the clicks
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void pbxDriverZone_MouseClick(object sender, MouseEventArgs e)
{
if (CreatingWindow && pbxWindowPreview.Image != null)
@@ -276,15 +328,18 @@ namespace TrackTrends
RefreshUI();
}
}
private void pbxDriverZone_Click(object sender, EventArgs e)
{
//Not the right one to use visibly
}
/// <summary>
/// Creates a rectangle without caring about the order of the points.
/// </summary>
/// <param name="p1">First point. Can be top left or bottom right</param>
/// <param name="p2">Second point. Can be top left or bottom right</param>
/// <returns></returns>
private Rectangle CreateAbsoluteRectangle(Point p1, Point p2)
{
Point newP1 = new Point();
Point newP2 = new Point();
//Kind of a pain to have to do this but this lets the user do stupid things without the app crashing
if (p1.X < p2.X)
{
newP1.X = p1.X;
@@ -308,7 +363,11 @@ namespace TrackTrends
}
return new Rectangle(newP1.X, newP1.Y, newP2.X - newP1.X, newP2.Y - newP1.Y);
}
/// <summary>
/// Will refresh the emulator and will controll some of the controls
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void btnRefresh_Click(object sender, EventArgs e)
{
btnRefresh.Enabled = false;
@@ -355,23 +414,45 @@ namespace TrackTrends
break;
}
MessageBox.Show(message);
btnRefresh.Text = "Retry";
btnLoadPreset.Enabled = false;
btnDeletePreset.Enabled = false;
btnSavePreset.Enabled = false;
btnCreatZone.Enabled = false;
btnCreateWindow.Enabled = false;
btnResetDriver.Enabled = false;
}
else
{
btnRefresh.Text = "Get a newer image";
pbxPreview.Image = Emulator.Screenshot();
}
}
else
{
pbxPreview.Image = Emulator.Screenshot();
}
btnRefresh.Enabled = true;
btnLoadPreset.Enabled = true;
btnDeletePreset.Enabled = true;
btnSavePreset.Enabled = true;
btnCreatZone.Enabled = true;
btnCreateWindow.Enabled = true;
btnResetDriver.Enabled = true;
btnRefresh.Text = "Get a newer image";
}
}
else
{
pbxPreview.Image = Emulator.Screenshot();
//I know im repeating myself. This part could use a bool variable that allows those buttons to be displayed but it was the fastest way to fix a bad behaviour in the app
btnLoadPreset.Enabled = true;
btnDeletePreset.Enabled = true;
btnSavePreset.Enabled = true;
btnCreatZone.Enabled = true;
btnCreateWindow.Enabled = true;
btnResetDriver.Enabled = true;
}
btnRefresh.Enabled = true;
}
/// <summary>
/// Will try to close the headless browser so the main form can launch a new one safely
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Settings_FormClosing(object sender, FormClosingEventArgs e)
{
if (Emulator != null)
@@ -381,7 +462,11 @@ namespace TrackTrends
Emulator = null;
GC.Collect();
}
/// <summary>
/// Will reset the drivers
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnResetDriver_Click(object sender, EventArgs e)
{
if (Emulator != null)
@@ -389,7 +474,11 @@ namespace TrackTrends
Emulator.ResetDriver();
}
}
/// <summary>
/// Saves the current presets as a new JSON file
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnSavePreset_Click(object sender, EventArgs e)
{
string presetName = tbxPresetName.Text;
@@ -399,12 +488,21 @@ namespace TrackTrends
}
RefreshUI();
}
/// <summary>
/// Will change the selected preset. Usefull if you close this page because then the main form will keep in memory your last choice
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void lsbPresets_SelectedIndexChanged(object sender, EventArgs e)
{
if (lsbPresets.SelectedIndex >= 0)
SelectedConfigFile = (string)lsbPresets.Items[lsbPresets.SelectedIndex];
}
/// <summary>
/// Will load an existing presets
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnLoadPreset_Click(object sender, EventArgs e)
{
//MessageBox.Show(lsbPresets.SelectedIndex.ToString());
@@ -427,7 +525,11 @@ namespace TrackTrends
RefreshUI();
}
}
/// <summary>
/// This will be called everytime the form resizes. Here we are making the form responsive
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Settings_Resize(object sender, EventArgs e)
{
int xDiff = this.Width - oldSize.Width;
@@ -438,7 +540,11 @@ namespace TrackTrends
pbxPreview.Size = new Size(oldPbxPreviewSize.Width + xDiff, oldPbxPreviewSize.Height + yDiff);
pbxWindowPreview.Size = new Size(oldPbxWindowPreviewSize.Width + xDiff, oldPbxWindowPreviewSize.Height);
}
/// <summary>
/// Will delete an existing preset
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnDeletePreset_Click(object sender, EventArgs e)
{
int selectedIndex = lsbPresets.SelectedIndex;
@@ -456,7 +562,11 @@ namespace TrackTrends
}
}
}
/// <summary>
/// Sketchy method that is used to remove the borders from groupboxes... Yes its dumb but I dont think there is any other way
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void removeBorders(object sender, PaintEventArgs e)
{
GroupBox gpbx = (GroupBox)sender;
+2 -2
View File
@@ -1,8 +1,8 @@
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// Date : 09/08/2023
/// File : SqliteStorage.cs
/// Brief : Class that controls the sqlite database
/// Version : Alpha 1.0
/// Version : Beta 1.0
using System;
using System.Collections.Generic;
+8 -4
View File
@@ -1,8 +1,8 @@
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// Date : 09/06/2023
/// File : Window.cs
/// Brief : Default Window object that is mainly expected to be inherited.
/// Version : Alpha 1.0
/// Version : Beta 1.0
using System;
using System.Collections.Generic;
@@ -153,7 +153,7 @@ namespace TrackTrends
}
Bitmap enhancedImage = new OcrImage(image).Enhance(salt, windowType);
Bitmap enhancedImage = new OcrImage(image).Enhance(windowType);
var tessImage = Pix.LoadFromMemory(ImageToByte(enhancedImage));
@@ -529,6 +529,10 @@ namespace TrackTrends
// This should be the x:xx.xxx
try
{
//Gaps cant be more than 9 minuts so if there is more than 1 digit it means that the '+' has been understood as an other number
if (rawNumbers[0].Length > 1)
rawNumbers[0] = rawNumbers[0][rawNumbers[0].Length - 1].ToString();
minuts = Convert.ToInt32(rawNumbers[0].ToString());
seconds = Convert.ToInt32(rawNumbers[1].ToString());
miliseconds = Convert.ToInt32(rawNumbers[2].ToString());
@@ -578,7 +582,7 @@ namespace TrackTrends
Engine.SetVariable("tessedit_char_whitelist", allowedChars);
Bitmap rawData = image;
Bitmap enhancedImage = new OcrImage(rawData).Enhance(salt, windowType);
Bitmap enhancedImage = new OcrImage(rawData).Enhance(windowType);
Page page = Engine.Process(enhancedImage);
using (var iter = page.GetIterator())
+2 -2
View File
@@ -1,8 +1,8 @@
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// Date : 09/06/2023
/// File : Zone.cs
/// Brief : Class that contains all the methods and infos for a zone. This is designed to be potentially be inherited.
/// Version : Alpha 1.0
/// Version : Beta 1.0
using System;
using System.Collections.Generic;
+260
View File
@@ -0,0 +1,260 @@
/// Author : Maxime Rohmer
/// Date : 01/06/2023
/// File : OcrImageTests.cs
/// Brief : Class that is here to test the working principle of the OCR. Its not really unit test because we dont test a single class
/// Version : Alpha 1.0
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TrackTrends;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Drawing;
namespace TrackTrends.Tests
{
[TestClass()]
public class OcrImageTests
{
/// <summary>
/// Checks that the OCR on LapTimes is working.
/// </summary>
[TestMethod()]
public void LapTimeOCR_Test()
{
string directory = @"./../../TestImages/LapTimes/";
foreach (string file in Directory.GetFiles(directory))
{
Bitmap image = (Bitmap)Image.FromFile(file);
DriverLapTimeWindow lapTimeWindow = new DriverLapTimeWindow(image, new Rectangle(0, 0, image.Width, image.Height), true);
string[] paths = file.Split('/');
string fileName = paths[paths.Length - 1];
fileName = fileName.Replace(".png", "");
int timeMS = (int)lapTimeWindow.DecodePng();
string time = Reader.ConvertMsToTime(timeMS);
string[] checkDigits = fileName.Split('_');
string[] digitsToCheck = time.Split(':');
//The ConvertMSToTime will always return three chars so we need to make the checkDigits be also three chars
while (checkDigits.Length != 3)
checkDigits = new[] { "0" }.Concat(checkDigits).ToArray();
for (int i = 0; i < checkDigits.Length; i++)
{
Assert.AreEqual(checkDigits[i], digitsToCheck[i]);
}
}
}
/// <summary>
/// Checks that the Driver Position is well detected using the OCR
/// </summary>
[TestMethod()]
public void PositionOCR_Test()
{
string directory = @"./../../TestImages/Positions/";
foreach (string file in Directory.GetFiles(directory))
{
Bitmap image = (Bitmap)Image.FromFile(file);
DriverPositionWindow posWindow = new DriverPositionWindow(image, new Rectangle(0, 0, image.Width, image.Height), true);
string[] paths = file.Split('/');
string fileName = paths[paths.Length - 1];
fileName = fileName.Replace(".png", "");
int foundPos = (int)posWindow.DecodePng();
Assert.AreEqual(Convert.ToInt32(fileName), foundPos);
}
}
/// <summary>
/// Checks that the tyres are correctly recognized using OCR
/// </summary>
[TestMethod()]
public void TyresOCR_Test()
{
string directory = @"./../../TestImages/Tyres/";
foreach (string file in Directory.GetFiles(directory))
{
Bitmap image = (Bitmap)Image.FromFile(file);
DriverTyresWindow tyreWindow = new DriverTyresWindow(image, new Rectangle(0, 0, image.Width, image.Height), true);
string[] paths = file.Split('/');
string fileName = paths[paths.Length - 1];
fileName = fileName.Replace(".png", "");
string[] fileInfos = fileName.Split('_');
string expectedType = fileInfos[0];
int expectedLap = Convert.ToInt32(fileInfos[1]);
Tyre foundTyre = (Tyre)tyreWindow.DecodePng();
switch (foundTyre.Coumpound)
{
case Tyre.Type.Soft:
Assert.AreEqual("SOFT",expectedType.ToUpper());
break;
case Tyre.Type.Inter:
Assert.AreEqual("INTER", expectedType.ToUpper());
break;
case Tyre.Type.Hard:
Assert.AreEqual("HARD", expectedType.ToUpper());
break;
case Tyre.Type.Wet:
Assert.AreEqual("WET", expectedType.ToUpper());
break;
case Tyre.Type.Medium:
Assert.AreEqual("MEDIUM", expectedType.ToUpper());
break;
}
Assert.AreEqual(expectedLap,foundTyre.NumberOfLaps);
}
}
/// <summary>
/// Checks that the Gap to the leader is well decoded from the image using the OCR
/// </summary>
[TestMethod()]
public void GapToLeaderOCR_Test()
{
string directory = @"./../../TestImages/Gaps/";
foreach (string file in Directory.GetFiles(directory))
{
Bitmap image = (Bitmap)Image.FromFile(file);
DriverGapToLeaderWindow gapsWindow = new DriverGapToLeaderWindow(image, new Rectangle(0, 0, image.Width, image.Height), true);
string[] paths = file.Split('/');
string fileName = paths[paths.Length - 1];
fileName = fileName.Replace(".png", "");
int timeMS = (int)gapsWindow.DecodePng();
string time = Reader.ConvertMsToTime(timeMS);
string[] checkDigits = fileName.Split('_');
string[] digitsToCheck = time.Split(':');
if (time == "0:00:000")
{
Assert.AreEqual(0, Convert.ToInt32(checkDigits[0]));
}
else
{
//The ConvertMSToTime will always return three chars so we need to make the checkDigits be also three chars
while (checkDigits.Length != 3)
checkDigits = new[] { "0" }.Concat(checkDigits).ToArray();
for (int i = 0; i < checkDigits.Length; i++)
{
//We need to convert to int first because sometimes we have "08" and "8" and in string its not the same but in int it is
Assert.AreEqual(Convert.ToInt32(checkDigits[i]), Convert.ToInt32(digitsToCheck[i]));
}
}
}
}
/// <summary>
/// Checks that the DRS window is well recognized as open of closed
/// </summary>
[TestMethod()]
public void DRS_OCR_Test()
{
string directory = @"./../../TestImages/DRS/";
DirectoryInfo directoryInfo = new DirectoryInfo(directory);
Bitmap falseExemple = (Bitmap)Image.FromFile(directory + "False.png");
Bitmap trueExemple = (Bitmap)Image.FromFile(directory + "True.png");
DriverDrsWindow falseDRS = new DriverDrsWindow(falseExemple, new Rectangle(0, 0, falseExemple.Width, falseExemple.Height), true);
DriverDrsWindow trueDRS = new DriverDrsWindow(trueExemple, new Rectangle(0, 0, trueExemple.Width, trueExemple.Height), true);
bool falseResult = (bool)falseDRS.DecodePng();
bool trueResult = (bool)trueDRS.DecodePng();
Assert.IsFalse(falseResult);
Assert.IsTrue(trueResult);
}
/// <summary>
/// Checks that the driver names are recognized correctly
/// </summary>
[TestMethod()]
public void DriverNameOCR_Test()
{
string directory = @"./../../TestImages/Names/";
foreach (string file in Directory.GetFiles(directory))
{
Bitmap image = (Bitmap)Image.FromFile(file);
DriverNameWindow nameWindow = new DriverNameWindow(image, new Rectangle(0, 0, image.Width, image.Height), true);
string[] paths = file.Split('/');
string fileName = paths[paths.Length - 1];
fileName = fileName.Replace(".png", "");
List<string> driverList = new List<String>();
driverList.Add("Albon");
driverList.Add("Alonso");
driverList.Add("Bottas");
driverList.Add("De Vries");
driverList.Add("Gasly");
driverList.Add("Hamilton");
driverList.Add("Hulkenberg");
driverList.Add("Leclerc");
driverList.Add("Magnussen");
driverList.Add("Norris");
driverList.Add("Ocon");
driverList.Add("Perez");
driverList.Add("Piastri");
driverList.Add("Russel");
driverList.Add("Sainz");
driverList.Add("Sargeant");
driverList.Add("Stroll");
driverList.Add("Tsunoda");
driverList.Add("Verstappen");
driverList.Add("Zhou");
string foundName = (string)nameWindow.DecodePng(driverList);
//We only compare without the case because it does not really matter
//We remove the '_' from the file name for cases like "De Vries" wich obviously would be "De_Vries.png" as filename
fileName = fileName.Replace('_', ' ');
Assert.AreEqual(fileName.ToUpper(),foundName.ToUpper());
}
}
/// <summary>
/// Checks that the Sectors are correctly decoded using the OCR
/// </summary>
[TestMethod()]
public void SectorOCR_Test()
{
string directory = @"./../../TestImages/Sectors/";
foreach (string file in Directory.GetFiles(directory))
{
Bitmap image = (Bitmap)Image.FromFile(file);
DriverSectorWindow sectorsWindow = new DriverSectorWindow(image, new Rectangle(0, 0, image.Width, image.Height), 1, true);
string[] paths = file.Split('/');
string fileName = paths[paths.Length - 1];
fileName = fileName.Replace(".png", "");
int timeMS = (int)sectorsWindow.DecodePng();
string time = Reader.ConvertMsToTime(timeMS);
string[] checkDigits = fileName.Split('_');
string[] digitsToCheck = time.Split(':');
if (time == "0:00:000")
{
Assert.AreEqual(0, Convert.ToInt32(checkDigits[0]));
}
else
{
//The ConvertMSToTime will always return three chars so we need to make the checkDigits be also three chars
while (checkDigits.Length != 3)
checkDigits = new[] { "0" }.Concat(checkDigits).ToArray();
for (int i = 0; i < checkDigits.Length; i++)
{
//We need to convert to int first because sometimes we have "08" and "8" and in string its not the same but in int it is
Assert.AreEqual(Convert.ToInt32(checkDigits[i]), Convert.ToInt32(digitsToCheck[i]));
}
}
}
}
}
}
@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("TrackTrendsTests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("TrackTrendsTests")]
[assembly: AssemblyCopyright("Copyright © 2023")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("9be68783-f589-479a-8cfa-9652c3b23a37")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Some files were not shown because too many files have changed in this diff Show More